195 lines
6.5 KiB
TypeScript
195 lines
6.5 KiB
TypeScript
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' }],
|
|
};
|