254 lines
11 KiB
TypeScript
254 lines
11 KiB
TypeScript
import { Head, Link } from '@inertiajs/react';
|
|
import {
|
|
ArrowRight,
|
|
CalendarClock,
|
|
CheckCircle2,
|
|
Clock3,
|
|
PackageCheck,
|
|
Plus,
|
|
TrendingUp,
|
|
} from 'lucide-react';
|
|
import { PageHeading } from '@/components/page-heading';
|
|
import { ReservationStatus } from '@/components/reservation-status';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import type { Reservation } from '@/types';
|
|
|
|
type Props = {
|
|
summary: { pending: number; approved: number; inUse: number };
|
|
nextReservation: Reservation | null;
|
|
recentReservations: Reservation[];
|
|
};
|
|
|
|
const dateTime = new Intl.DateTimeFormat('pt-PT', {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'short',
|
|
});
|
|
|
|
export default function Dashboard({
|
|
summary,
|
|
nextReservation,
|
|
recentReservations,
|
|
}: Props) {
|
|
return (
|
|
<>
|
|
<Head title="Visão geral" />
|
|
<main className="mx-auto flex w-full max-w-7xl flex-1 flex-col gap-8 p-5 sm:p-8">
|
|
<PageHeading
|
|
eyebrow="Área pessoal"
|
|
title="Bom dia. O que precisa de reservar?"
|
|
description="Consulte a disponibilidade dos equipamentos e acompanhe as suas requisições num só lugar."
|
|
action={
|
|
<Button asChild className="shadow-sm">
|
|
<Link href="/requisicoes/nova">
|
|
<Plus /> Nova requisição
|
|
</Link>
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
<section className="grid gap-4 sm:grid-cols-3">
|
|
<SummaryCard
|
|
icon={Clock3}
|
|
label="A aguardar aprovação"
|
|
value={summary.pending}
|
|
tone="amber"
|
|
/>
|
|
<SummaryCard
|
|
icon={CheckCircle2}
|
|
label="Aprovadas"
|
|
value={summary.approved}
|
|
tone="emerald"
|
|
/>
|
|
<SummaryCard
|
|
icon={PackageCheck}
|
|
label="Em utilização"
|
|
value={summary.inUse}
|
|
tone="blue"
|
|
/>
|
|
</section>
|
|
|
|
<section className="grid gap-6 lg:grid-cols-[1.2fr_0.8fr]">
|
|
<Card className="border-slate-200 shadow-none dark:border-slate-800">
|
|
<CardHeader className="flex-row items-center justify-between">
|
|
<div>
|
|
<p className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
|
|
Próxima
|
|
</p>
|
|
<CardTitle className="mt-2">
|
|
Próxima requisição
|
|
</CardTitle>
|
|
</div>
|
|
<CalendarClock className="size-5 text-primary" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
{nextReservation ? (
|
|
<div className="space-y-5">
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<span className="font-mono text-sm font-medium">
|
|
{nextReservation.reference}
|
|
</span>
|
|
<ReservationStatus
|
|
status={nextReservation.status}
|
|
/>
|
|
</div>
|
|
<div className="grid gap-4 rounded-xl bg-slate-50 p-4 sm:grid-cols-2 dark:bg-slate-900">
|
|
<div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Início
|
|
</p>
|
|
<p className="mt-1 text-sm font-medium">
|
|
{dateTime.format(
|
|
new Date(
|
|
nextReservation.starts_at,
|
|
),
|
|
)}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Fim
|
|
</p>
|
|
<p className="mt-1 text-sm font-medium">
|
|
{dateTime.format(
|
|
new Date(
|
|
nextReservation.ends_at,
|
|
),
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Equipamento
|
|
</p>
|
|
<p className="mt-1 text-sm font-medium">
|
|
{nextReservation.items
|
|
.map((item) => item.asset.name)
|
|
.join(', ')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="rounded-xl border border-dashed py-10 text-center">
|
|
<PackageCheck className="mx-auto size-8 text-slate-300" />
|
|
<p className="mt-3 text-sm font-medium">
|
|
Ainda não tem requisições futuras.
|
|
</p>
|
|
<Button
|
|
asChild
|
|
variant="link"
|
|
className="mt-1"
|
|
>
|
|
<Link href="/equipamentos">
|
|
Explorar catálogo
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-slate-200 shadow-none dark:border-slate-800">
|
|
<CardHeader className="flex-row items-center justify-between">
|
|
<CardTitle>Atividade recente</CardTitle>
|
|
<Button asChild variant="ghost" size="sm">
|
|
<Link href="/requisicoes">
|
|
Ver todas <ArrowRight />
|
|
</Link>
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent className="space-y-1">
|
|
{recentReservations.length > 0 ? (
|
|
recentReservations.map((reservation) => (
|
|
<div
|
|
key={reservation.id}
|
|
className="flex items-center justify-between gap-3 border-b py-3 last:border-0"
|
|
>
|
|
<div className="min-w-0">
|
|
<p className="truncate text-sm font-medium">
|
|
{reservation.reference}
|
|
</p>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
{dateTime.format(
|
|
new Date(
|
|
reservation.starts_at,
|
|
),
|
|
)}
|
|
</p>
|
|
</div>
|
|
<ReservationStatus
|
|
status={reservation.status}
|
|
/>
|
|
</div>
|
|
))
|
|
) : (
|
|
<p className="py-8 text-center text-sm text-muted-foreground">
|
|
Sem atividade recente.
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function SummaryCard({
|
|
icon: Icon,
|
|
label,
|
|
value,
|
|
tone,
|
|
}: {
|
|
icon: typeof Clock3;
|
|
label: string;
|
|
value: number;
|
|
tone: 'amber' | 'emerald' | 'blue';
|
|
}) {
|
|
const colors = {
|
|
amber: {
|
|
bg: 'bg-amber-50 dark:bg-amber-950/30',
|
|
text: 'text-amber-700 dark:text-amber-400',
|
|
icon: 'bg-amber-100 dark:bg-amber-900/50',
|
|
},
|
|
emerald: {
|
|
bg: 'bg-emerald-50 dark:bg-emerald-950/30',
|
|
text: 'text-emerald-700 dark:text-emerald-400',
|
|
icon: 'bg-emerald-100 dark:bg-emerald-900/50',
|
|
},
|
|
blue: {
|
|
bg: 'bg-blue-50 dark:bg-blue-950/30',
|
|
text: 'text-blue-700 dark:text-blue-400',
|
|
icon: 'bg-blue-100 dark:bg-blue-900/50',
|
|
},
|
|
};
|
|
|
|
return (
|
|
<Card className="group overflow-hidden border-slate-200 py-0 shadow-none transition-all hover:shadow-md dark:border-slate-800">
|
|
<CardContent className="flex items-center gap-4 p-5">
|
|
<span
|
|
className={`flex size-12 items-center justify-center rounded-2xl ${colors[tone].icon} transition-transform group-hover:scale-110`}
|
|
>
|
|
<Icon className={`size-6 ${colors[tone].text}`} />
|
|
</span>
|
|
<div className="flex-1">
|
|
<p className="text-3xl font-bold tracking-tight">{value}</p>
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
{label}
|
|
</p>
|
|
</div>
|
|
{value > 0 && (
|
|
<TrendingUp
|
|
className={`size-4 ${colors[tone].text} opacity-50`}
|
|
/>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
Dashboard.layout = {
|
|
breadcrumbs: [{ title: 'Visão geral', href: '/dashboard' }],
|
|
};
|