feat: complete administration and reporting

This commit is contained in:
2026-08-04 17:40:52 +01:00
parent d829f34580
commit 7a4ac48aaf
42 changed files with 2595 additions and 76 deletions

View File

@@ -10,9 +10,13 @@ const weekdays = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom'];
export default function ReservationCalendar({
month,
reservations,
scope = 'personal',
}: {
month: string;
reservations: Reservation[];
reservations: (Reservation & {
requester?: { name: string; department: string | null };
})[];
scope?: 'personal' | 'global';
}) {
const [year, monthNumber] = month.split('-').map(Number);
const monthStart = new Date(year, monthNumber - 1, 1);
@@ -32,21 +36,35 @@ export default function ReservationCalendar({
}).format(monthStart);
const previousMonth = monthKey(new Date(year, monthNumber - 2, 1));
const nextMonth = monthKey(new Date(year, monthNumber, 1));
const global = scope === 'global';
const calendarUrl = global
? '/calendario-global'
: '/requisicoes/calendario';
return (
<>
<Head title="O meu calendário" />
<Head title={global ? 'Calendário global' : 'O meu calendário'} />
<main className="mx-auto flex w-full max-w-7xl flex-1 flex-col gap-7 p-5 sm:p-8">
<PageHeading
eyebrow="Planeamento pessoal"
title="O meu calendário"
description="Consulte as suas requisições no mês e identifique rapidamente períodos sobrepostos."
eyebrow={
global
? 'Planeamento institucional'
: 'Planeamento pessoal'
}
title={global ? 'Calendário global' : 'O meu calendário'}
description={
global
? 'Visão consolidada de todas as requisições, requisitantes e equipamentos.'
: 'Consulte as suas requisições no mês e identifique rapidamente períodos sobrepostos.'
}
action={
<Button asChild>
<Link href="/requisicoes/nova">
<Plus /> Nova requisição
</Link>
</Button>
!global ? (
<Button asChild>
<Link href="/requisicoes/nova">
<Plus /> Nova requisição
</Link>
</Button>
) : undefined
}
/>
@@ -54,7 +72,7 @@ export default function ReservationCalendar({
<header className="flex items-center justify-between border-b p-4">
<Button asChild variant="outline" size="sm">
<Link
href={`/requisicoes/calendario?month=${previousMonth}`}
href={`${calendarUrl}?month=${previousMonth}`}
>
<ArrowLeft /> Anterior
</Link>
@@ -63,9 +81,7 @@ export default function ReservationCalendar({
{title}
</h2>
<Button asChild variant="outline" size="sm">
<Link
href={`/requisicoes/calendario?month=${nextMonth}`}
>
<Link href={`${calendarUrl}?month=${nextMonth}`}>
Seguinte <ArrowRight />
</Link>
</Button>
@@ -132,6 +148,10 @@ export default function ReservationCalendar({
)}
>
{reservation.reference}
{global &&
reservation.requester
? ` · ${reservation.requester.name}`
: ''}
</div>
))}
{entries.length > 3 && (
@@ -202,8 +222,5 @@ function statusBorder(status: string): string {
}
ReservationCalendar.layout = {
breadcrumbs: [
{ title: 'Requisições', href: '/requisicoes' },
{ title: 'Calendário', href: '/requisicoes/calendario' },
],
breadcrumbs: [{ title: 'Calendário', href: '/requisicoes/calendario' }],
};

View File

@@ -21,11 +21,18 @@ export default function CreateReservation({
selectedAssetId,
period,
periodSelected,
policy,
}: {
assets: Asset[];
selectedAssetId: number;
period: { starts_at?: string; ends_at?: string };
periodSelected: boolean;
policy: {
minNoticeHours: number;
maxDurationHours: number;
turnaroundMinutes: number;
maxAssets: number;
};
}) {
const form = useForm({
purpose: '',
@@ -70,6 +77,15 @@ export default function CreateReservation({
</Button>
}
/>
<p className="rounded-xl border bg-muted/40 px-4 py-3 text-sm text-muted-foreground">
Regras atuais: antecedência mínima de{' '}
{policy.minNoticeHours} h, duração máxima de{' '}
{policy.maxDurationHours} h, até {policy.maxAssets}{' '}
equipamentos
{policy.turnaroundMinutes > 0
? ` e margem operacional de ${policy.turnaroundMinutes} min.`
: '.'}
</p>
<form
onSubmit={submit}
className="grid gap-6 lg:grid-cols-[1fr_320px]"