Files
compras/resources/js/pages/conflicts/index.tsx
T
nala 4d42f083ce
tests / ci (push) Canceled after 0s
commit
2026-10-28 12:06:05 +02:00

308 lines
15 KiB
TypeScript

import { Head, Link, useForm, usePage } from '@inertiajs/react';
import {
FormErrors,
PageTitle,
Panel,
controlClass,
} from '@/components/procurement';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { conflictScope } from '@/types/conflict';
import type { ConflictDeclaration } from '@/types/conflict';
import { date } from '@/types/procurement';
type Props = {
declarations: {
data: ConflictDeclaration[];
prev_page_url: string | null;
next_page_url: string | null;
};
suppliers: { id: number; name: string }[];
requisitions: { id: number; number: string }[];
};
export default function Conflicts({
declarations,
suppliers,
requisitions,
}: Props) {
const { auth } = usePage().props;
const form = useForm({
scope: 'supplier',
supplier_id: '',
requisition_id: '',
has_conflict: '1',
known_since: '',
reason: '',
});
return (
<>
<Head title="Conflitos de interesses" />
<div className="mx-auto flex w-full max-w-6xl flex-col gap-7 p-5 lg:p-8">
<PageTitle
eyebrow="Integridade nas compras · RF-AF-01"
title="Conflitos de interesses"
description="Registe a sua situação pessoal perante um fornecedor ou processo. Administração e Finanças podem consultar o registo da empresa; os restantes perfis consultam apenas as suas declarações."
/>
<div className="rounded-xl border border-amber-200 bg-amber-50 p-5 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-100">
Um conflito registado mantém o impedimento, mesmo que exista
uma declaração posterior de ausência. Não há caducidade
automática nem retirada de conflitos nesta etapa: essas
condições dependem da política da empresa. A data de
conhecimento descreve o facto e não adia o bloqueio.
</div>
<Panel
title="Nova declaração pessoal"
description={`Autor: ${auth.user.name}. A data e a hora do registo são guardadas pelo servidor.`}
>
<form
className="space-y-5"
onSubmit={(e) => {
e.preventDefault();
form.transform((data) => ({
...data,
has_conflict: data.has_conflict === '1',
supplier_id:
data.scope === 'process'
? null
: data.supplier_id || null,
requisition_id:
data.scope === 'supplier'
? null
: data.requisition_id || null,
}));
form.post('/conflitos', {
preserveScroll: true,
onSuccess: () => form.reset(),
});
}}
>
<FormErrors errors={form.errors} />
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="conflict-scope">Âmbito</Label>
<select
id="conflict-scope"
className={controlClass}
value={form.data.scope}
onChange={(e) =>
form.setData('scope', e.target.value)
}
>
<option value="supplier">
Fornecedor, em qualquer processo
</option>
<option value="process">
Processo, independentemente do
fornecedor
</option>
<option value="process_supplier">
Fornecedor neste processo
</option>
</select>
</div>
<div className="space-y-2">
<Label htmlFor="conflict-result">
Situação declarada
</Label>
<select
id="conflict-result"
className={controlClass}
value={form.data.has_conflict}
onChange={(e) =>
form.setData(
'has_conflict',
e.target.value,
)
}
>
<option value="1">
Existe conflito de interesses
</option>
<option value="0">
Não identifico conflito de interesses
</option>
</select>
</div>
{form.data.scope !== 'process' && (
<div className="space-y-2">
<Label htmlFor="conflict-supplier">
Fornecedor
</Label>
<select
id="conflict-supplier"
className={controlClass}
required
value={form.data.supplier_id}
onChange={(e) =>
form.setData(
'supplier_id',
e.target.value,
)
}
>
<option value="">Seleccione</option>
{suppliers.map((s) => (
<option key={s.id} value={s.id}>
{s.name}
</option>
))}
</select>
</div>
)}
{form.data.scope !== 'supplier' && (
<div className="space-y-2">
<Label htmlFor="conflict-process">
Processo / requisição
</Label>
<select
id="conflict-process"
className={controlClass}
required
value={form.data.requisition_id}
onChange={(e) =>
form.setData(
'requisition_id',
e.target.value,
)
}
>
<option value="">Seleccione</option>
{requisitions.map((r) => (
<option key={r.id} value={r.id}>
{r.number}
</option>
))}
</select>
</div>
)}
<div className="space-y-2">
<Label htmlFor="conflict-known">
Data de conhecimento da situação
</Label>
<Input
id="conflict-known"
type="date"
required
value={form.data.known_since}
onChange={(e) =>
form.setData(
'known_since',
e.target.value,
)
}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="conflict-reason">
Fundamento da declaração
</Label>
<textarea
id="conflict-reason"
className={`${controlClass} h-28 py-3`}
required
minLength={10}
maxLength={5000}
value={form.data.reason}
onChange={(e) =>
form.setData('reason', e.target.value)
}
/>
</div>
<Button disabled={form.processing}>
Registar declaração pessoal
</Button>
</form>
</Panel>
<Panel
title="Declarações e correcções"
description="O histórico é preservado. Só o autor pode corrigir o fundamento e a data de conhecimento, sem mudar o âmbito ou retirar o conflito."
>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="border-b text-muted-foreground">
<tr>
<th className="pr-4 pb-3">
Autor e registo
</th>
<th className="pr-4">Âmbito</th>
<th className="pr-4">Situação</th>
<th>
<span className="sr-only">
Histórico
</span>
</th>
</tr>
</thead>
<tbody>
{declarations.data.map((d) => (
<tr
key={d.id}
className="border-b last:border-0"
>
<td className="py-4 pr-4">
<p className="font-medium">
{d.author.name}
</p>
<p className="mt-1 text-xs text-muted-foreground">
{date(d.declared_at)} · #{d.id}
{d.previous_id
? ` · Correcção de #${d.previous_id}`
: ''}
</p>
</td>
<td className="pr-4">
{conflictScope(d)}
</td>
<td className="pr-4">
{d.has_conflict
? 'Conflito registado'
: 'Ausência declarada'}
{d.successor && (
<p className="text-xs text-muted-foreground">
Com correcção posterior
</p>
)}
</td>
<td>
<Link
className="text-teal-700 dark:text-teal-400"
href={`/conflitos/${d.id}`}
>
Ver histórico
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
{declarations.data.length === 0 && (
<p className="py-6 text-sm text-muted-foreground">
Ainda não existem declarações disponíveis para o seu
perfil.
</p>
)}
<div className="mt-4 flex gap-4 text-sm">
{declarations.prev_page_url && (
<Link href={declarations.prev_page_url}>
Anterior
</Link>
)}
{declarations.next_page_url && (
<Link href={declarations.next_page_url}>
Seguinte
</Link>
)}
</div>
</Panel>
</div>
</>
);
}
Conflicts.layout = {
breadcrumbs: [{ title: 'Conflitos de interesses', href: '/conflitos' }],
};