Files
nala 63d20b10fc
tests / ci (push) Canceled after 0s
commit de armazens
2026-11-04 09:40:26 +02:00

202 lines
8.3 KiB
TypeScript

import { Head, Link, useForm } from '@inertiajs/react';
import {
PageTitle,
Panel,
FormErrors,
controlClass,
} from '@/components/procurement';
import { Button } from '@/components/ui/button';
type Props = {
filters: {
action?: string;
actor_id?: number | string;
from?: string;
to?: string;
page?: number;
};
events: {
data: {
id: number;
action: string;
created_at: string;
user_id: number | null;
}[];
total: number;
current_page: number;
last_page: number;
prev_page_url: string | null;
next_page_url: string | null;
};
};
export default function AuditMetadata({ filters, events }: Props) {
const form = useForm({
action: filters.action ?? '',
actor_id: String(filters.actor_id ?? ''),
from: filters.from ?? '',
to: filters.to ?? '',
});
const query = new URLSearchParams(
Object.entries(filters)
.filter(
([key, value]) =>
key !== 'page' && value !== null && value !== '',
)
.map(([key, value]) => [key, String(value)]),
);
return (
<>
<Head title="Eventos dos módulos" />
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6 p-5 lg:p-8">
<PageTitle
eyebrow="Auditoria"
title="Eventos dos módulos"
description="Metadados registados na empresa. Esta consulta não concede acesso ao conteúdo dos processos."
/>
<Panel title="Consulta e exportação">
<p className="text-sm">
Os eventos são apresentados pela ordem de registo,
incluindo os mais antigos. A exportação inclui todos os
resultados dos filtros, sem conteúdos privados,
documentos ou valores anteriores e posteriores.
</p>
<details className="my-4 rounded border p-3">
<summary>Filtrar eventos</summary>
<form
className="mt-4 grid gap-4 sm:grid-cols-2"
onSubmit={(event) => {
event.preventDefault();
form.get('/auditoria/eventos', {
preserveState: true,
});
}}
>
<FormErrors errors={form.errors} />
<label>
Acção exacta
<input
className={controlClass}
maxLength={100}
value={form.data.action}
onChange={(event) =>
form.setData(
'action',
event.target.value,
)
}
/>
</label>
<label>
Utilizador (id)
<input
className={controlClass}
type="number"
min="1"
value={form.data.actor_id}
onChange={(event) =>
form.setData(
'actor_id',
event.target.value,
)
}
/>
</label>
<label>
Desde
<input
className={controlClass}
type="date"
value={form.data.from}
onChange={(event) =>
form.setData('from', event.target.value)
}
/>
</label>
<label>
Até
<input
className={controlClass}
type="date"
value={form.data.to}
onChange={(event) =>
form.setData('to', event.target.value)
}
/>
</label>
<Button disabled={form.processing}>
Aplicar filtros
</Button>
<Link href="/auditoria/eventos">
Limpar filtros
</Link>
</form>
</details>
<a
className="text-sm underline"
href={`/auditoria/eventos.csv?${query.toString()}`}
>
Descarregar metadados filtrados em CSV
</a>
</Panel>
<Panel title={`${events.total} eventos encontrados`}>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr>
<th className="p-2">Registo</th>
<th className="p-2">Data</th>
<th className="p-2">Acção</th>
<th className="p-2">Utilizador (id)</th>
</tr>
</thead>
<tbody>
{events.data.map((event) => (
<tr key={event.id} className="border-t">
<td className="p-2">{event.id}</td>
<td className="p-2">
{event.created_at}
</td>
<td className="p-2 break-all">
{event.action}
</td>
<td className="p-2">
{event.user_id ?? '—'}
</td>
</tr>
))}
</tbody>
</table>
</div>
{events.data.length === 0 && (
<p className="mt-4">Sem eventos para estes filtros.</p>
)}
<nav
aria-label="Páginas de eventos"
className="mt-5 flex flex-wrap gap-4 text-sm"
>
<span>
Página {events.current_page} de {events.last_page}
</span>
{events.prev_page_url && (
<Link href={events.prev_page_url} preserveScroll>
Anterior
</Link>
)}
{events.next_page_url && (
<Link href={events.next_page_url} preserveScroll>
Seguinte
</Link>
)}
</nav>
</Panel>
</div>
</>
);
}
AuditMetadata.layout = {
breadcrumbs: [{ title: 'Auditoria', href: '/auditoria/eventos' }],
};