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