feat: establish reservation portal foundation
This commit is contained in:
233
resources/js/pages/dashboard.tsx
Normal file
233
resources/js/pages/dashboard.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import {
|
||||
ArrowRight,
|
||||
CalendarClock,
|
||||
CheckCircle2,
|
||||
Clock3,
|
||||
PackageCheck,
|
||||
Plus,
|
||||
} 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-amber-50 text-amber-700',
|
||||
emerald: 'bg-emerald-50 text-emerald-700',
|
||||
blue: 'bg-blue-50 text-blue-700',
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="gap-3 border-slate-200 py-5 shadow-none dark:border-slate-800">
|
||||
<CardContent className="flex items-center gap-4">
|
||||
<span
|
||||
className={`flex size-11 items-center justify-center rounded-xl ${colors[tone]}`}
|
||||
>
|
||||
<Icon className="size-5" />
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-2xl font-semibold">{value}</p>
|
||||
<p className="text-sm text-muted-foreground">{label}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
Dashboard.layout = {
|
||||
breadcrumbs: [{ title: 'Visão geral', href: '/dashboard' }],
|
||||
};
|
||||
Reference in New Issue
Block a user