Files
compras/resources/js/pages/reports/procurement.tsx
T
nala c6373365bf
tests / ci (push) Canceled after 0s
commit9
2026-10-30 20:02:30 +02:00

321 lines
15 KiB
TypeScript

import { Head, Link, useForm } from '@inertiajs/react';
import { Download } from 'lucide-react';
import {
PageTitle,
Panel,
Status,
controlClass,
} from '@/components/procurement';
import { Button } from '@/components/ui/button';
import { date, statusLabels } from '@/types/procurement';
import { ProcurementOperationalInsights } from '@/components/procurement-operational-insights';
import type { ProcurementInsights } from '@/components/procurement-operational-insights';
type Row = {
id: number;
number: string;
status: string;
created_at: string;
department: string | null;
requester: string | null;
total_minor: string;
quotations_count: number;
order_number: string | null;
};
type Filters = {
search?: string;
status?: string;
from?: string;
to?: string;
department_id?: string | number;
};
const estimate = (minor: string) => {
const digits = minor.padStart(3, '0');
return `${digits.slice(0, -2)},${digits.slice(-2)} MZN`;
};
export default function ProcurementReports({
records,
counts,
filters,
departments,
generatedAt,
insights,
}: {
records: {
data: Row[];
total: number;
current_page: number;
last_page: number;
prev_page_url: string | null;
next_page_url: string | null;
};
counts: Record<string, number>;
filters: Filters;
departments: { id: number; name: string }[];
generatedAt: string;
insights?: ProcurementInsights;
}) {
const form = useForm({
search: filters.search ?? '',
status: filters.status ?? '',
from: filters.from ?? '',
to: filters.to ?? '',
department_id: String(filters.department_id ?? ''),
});
const params = new URLSearchParams();
for (const [key, value] of Object.entries(filters))
if (value !== null && value !== undefined && value !== '')
params.set(key, String(value));
return (
<>
<Head title="Relatórios de compras" />
<div className="mx-auto max-w-7xl space-y-6 px-5 py-8 lg:px-8">
<PageTitle
eyebrow="Consulta e relatórios"
title="Relatórios de compras"
description="Localize o pedido, acompanhe o processo e abra o relatório de conferência com as decisões e os documentos disponíveis."
action={
<Button asChild variant="outline">
<a href={`/relatorios/compras.csv?${params}`}>
<Download className="size-4" />
Exportar CSV
</a>
</Button>
}
/>
{insights && <ProcurementOperationalInsights data={insights} />}
<Panel
title="Filtrar requisições"
description="O período refere-se à criação da requisição. Os resultados respeitam o seu acesso à empresa e ao departamento."
>
<form
className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
onSubmit={(event) => {
event.preventDefault();
form.get('/relatorios', { preserveState: true });
}}
>
<label className="grid gap-1 text-sm">
Número da requisição
<input
className={controlClass}
value={form.data.search}
onChange={(event) =>
form.setData('search', event.target.value)
}
/>
</label>
<label className="grid gap-1 text-sm">
Estado da requisição
<select
className={controlClass}
value={form.data.status}
onChange={(event) =>
form.setData('status', event.target.value)
}
>
<option value="">Todos</option>
{Object.entries(statusLabels).map(
([value, label]) => (
<option key={value} value={value}>
{label}
</option>
),
)}
</select>
</label>
<label className="grid gap-1 text-sm">
Departamento
<select
className={controlClass}
value={form.data.department_id}
onChange={(event) =>
form.setData(
'department_id',
event.target.value,
)
}
>
<option value="">Todos os autorizados</option>
{departments.map((item) => (
<option key={item.id} value={item.id}>
{item.name}
</option>
))}
</select>
</label>
<label className="grid gap-1 text-sm">
Criada desde
<input
type="date"
className={controlClass}
value={form.data.from}
onChange={(event) =>
form.setData('from', event.target.value)
}
/>
</label>
<label className="grid gap-1 text-sm">
Criada até
<input
type="date"
className={controlClass}
value={form.data.to}
onChange={(event) =>
form.setData('to', event.target.value)
}
/>
</label>
<div className="flex items-end gap-3">
<Button type="submit" disabled={form.processing}>
Aplicar filtros
</Button>
<Link
className="py-2 text-sm underline"
href="/relatorios"
>
Limpar
</Link>
</div>
{Object.entries(form.errors).map(([key, message]) => (
<p
key={key}
role="alert"
className="text-sm text-red-700"
>
{message}
</p>
))}
</form>
</Panel>
<section
aria-label="Resumo dos resultados"
className="flex flex-wrap gap-3"
>
<p className="rounded-lg border bg-card px-4 py-3 font-medium">
{records.total} requisições
</p>
{Object.entries(counts).map(([status, count]) => (
<p
key={status}
className="rounded-lg border bg-card px-4 py-3 text-sm"
>
{statusLabels[status] ?? status}: {count}
</p>
))}
</section>
<Panel
title="Do pedido ao relatório"
description="A estimativa da requisição, a adjudicação, a recepção e o pagamento têm estados distintos. Propostas registadas não significam propostas elegíveis; uma ordem emitida não comprova liquidação."
>
<p className="mb-4 text-sm text-muted-foreground">
O CSV inclui todos os resultados dos filtros aplicados,
não apenas esta página. Consulta gerada em{' '}
{new Date(generatedAt).toLocaleString('pt-MZ')}.
</p>
<div className="overflow-x-auto">
<table className="w-full min-w-240 text-left text-sm">
<thead>
<tr>
<th className="p-2">Requisição</th>
<th className="p-2">
Departamento / solicitante
</th>
<th className="p-2">Estado</th>
<th className="p-2">Estimativa total</th>
<th className="p-2">Propostas / ordem</th>
<th className="p-2">Continuar</th>
</tr>
</thead>
<tbody>
{records.data.map((row) => (
<tr key={row.id} className="border-t">
<td className="p-2">
<Link
className="font-medium underline"
href={`/requisicoes/${row.id}`}
>
{row.number}
</Link>
<span className="block text-muted-foreground">
{date(row.created_at)}
</span>
</td>
<td className="p-2">
{row.department ??
'Departamento indisponível'}
<span className="block text-muted-foreground">
{row.requester ??
'Solicitante indisponível'}
</span>
</td>
<td className="p-2">
<Status value={row.status} />
</td>
<td className="p-2 whitespace-nowrap">
{estimate(row.total_minor)}
</td>
<td className="p-2">
{row.quotations_count} propostas
<span className="block text-muted-foreground">
{row.order_number ??
'Sem ordem de compra'}
</span>
</td>
<td className="space-y-2 p-2">
<Link
className="block underline"
href={`/requisicoes/${row.id}/processo`}
>
Acompanhar processo
</Link>
<Link
className="block underline"
href={`/requisicoes/${row.id}/relatorio`}
>
Gerar relatório de conferência
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
{records.data.length === 0 && (
<p className="py-6 text-sm">
Nenhuma requisição encontrada nos filtros e no
âmbito autorizado.
</p>
)}
<nav
aria-label="Paginação dos relatórios"
className="mt-4 flex items-center justify-between gap-3"
>
<span className="text-sm">
Página {records.current_page} de {records.last_page}
</span>
<div className="flex gap-4">
{records.prev_page_url && (
<Link
className="text-sm underline"
href={records.prev_page_url}
>
Anterior
</Link>
)}
{records.next_page_url && (
<Link
className="text-sm underline"
href={records.next_page_url}
>
Seguinte
</Link>
)}
</div>
</nav>
</Panel>
</div>
</>
);
}