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

@@ -0,0 +1,194 @@
import { Head, router } from '@inertiajs/react';
import {
AlertTriangle,
CheckCircle2,
Clock3,
Download,
FileText,
} from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import type { FormEvent } from 'react';
import { useState } from 'react';
import { PageHeading } from '@/components/page-heading';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
type Summary = {
total: number;
completed: number;
incidents: number;
overdue: number;
utilizationHours: number;
};
export default function ReportsIndex({
filters,
summary,
byStatus,
byCategory,
byDepartment,
topAssets,
}: {
filters: { date_from: string; date_to: string };
summary: Summary;
byStatus: Record<string, number>;
byCategory: Record<string, number>;
byDepartment: Record<string, number>;
topAssets: Record<string, number>;
}) {
const [dateFrom, setDateFrom] = useState(filters.date_from);
const [dateTo, setDateTo] = useState(filters.date_to);
function filter(event: FormEvent) {
event.preventDefault();
router.get('/relatorios', { date_from: dateFrom, date_to: dateTo });
}
const exportUrl = `/relatorios/exportar?${new URLSearchParams({ date_from: dateFrom, date_to: dateTo })}`;
return (
<>
<Head title="Relatórios" />
<main className="mx-auto flex w-full max-w-7xl flex-1 flex-col gap-7 p-5 sm:p-8">
<PageHeading
eyebrow="Gestão e análise"
title="Relatórios operacionais"
description="Indicadores consolidados por período, com exportação detalhada para tratamento externo."
action={
<Button asChild variant="outline">
<a href={exportUrl}>
<Download /> Exportar CSV
</a>
</Button>
}
/>
<form
onSubmit={filter}
className="flex flex-wrap items-end gap-3 rounded-xl border bg-card p-4"
>
<label className="space-y-1 text-sm font-medium">
De
<Input
type="date"
value={dateFrom}
onChange={(event) =>
setDateFrom(event.target.value)
}
/>
</label>
<label className="space-y-1 text-sm font-medium">
Até
<Input
type="date"
value={dateTo}
onChange={(event) => setDateTo(event.target.value)}
/>
</label>
<Button type="submit" variant="secondary">
Atualizar
</Button>
</form>
<section className="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
<Metric
icon={FileText}
label="Requisições"
value={summary.total}
/>
<Metric
icon={CheckCircle2}
label="Concluídas"
value={summary.completed}
/>
<Metric
icon={Clock3}
label="Horas de utilização"
value={summary.utilizationHours}
/>
<Metric
icon={AlertTriangle}
label="Incidentes"
value={summary.incidents}
/>
<Metric
icon={AlertTriangle}
label="Atrasos"
value={summary.overdue}
/>
</section>
<section className="grid gap-4 lg:grid-cols-2">
<Breakdown title="Por estado" values={byStatus} />
<Breakdown title="Por categoria" values={byCategory} />
<Breakdown title="Por departamento" values={byDepartment} />
<Breakdown
title="Equipamentos mais requisitados"
values={topAssets}
/>
</section>
</main>
</>
);
}
function Metric({
icon: Icon,
label,
value,
}: {
icon: LucideIcon;
label: string;
value: number;
}) {
return (
<Card className="gap-3 py-5 shadow-none">
<CardContent>
<Icon className="mb-3 size-5 text-primary" />
<p className="text-2xl font-semibold">{value}</p>
<p className="text-sm text-muted-foreground">{label}</p>
</CardContent>
</Card>
);
}
function Breakdown({
title,
values,
}: {
title: string;
values: Record<string, number>;
}) {
const max = Math.max(1, ...Object.values(values));
return (
<Card className="shadow-none">
<CardHeader>
<CardTitle className="text-base">{title}</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{Object.keys(values).length === 0 && (
<p className="text-sm text-muted-foreground">
Sem dados no período.
</p>
)}
{Object.entries(values).map(([label, value]) => (
<div key={label}>
<div className="mb-1 flex justify-between gap-3 text-sm">
<span>{label}</span>
<strong>{value}</strong>
</div>
<div className="h-2 rounded bg-muted">
<div
className="h-2 rounded bg-primary"
style={{ width: `${(value / max) * 100}%` }}
/>
</div>
</div>
))}
</CardContent>
</Card>
);
}
ReportsIndex.layout = {
breadcrumbs: [{ title: 'Relatórios', href: '/relatorios' }],
};