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

334 lines
13 KiB
TypeScript

import { Head, Link, useForm } from '@inertiajs/react';
import {
Panel,
PageTitle,
FormErrors,
controlClass,
} from '@/components/procurement';
import { Button } from '@/components/ui/button';
type Record = {
id: number;
order_number: string;
purchase_order_id: number;
source_href: string;
status: string;
reason: string;
evidence: string;
review_reason: string | null;
revised_requisition_id: number | null;
canDecide: boolean;
canRevise: boolean;
};
type Policy = {
id: number;
revision: number;
status: string;
enabled: boolean;
valid_from: string;
valid_until: string;
canReview: boolean;
};
function Decision({ url }: { url: string }) {
const form = useForm({ decision: 'rejected', reason: '' });
return (
<form
className="mt-3 space-y-3"
onSubmit={(event) => {
event.preventDefault();
if (window.confirm('Confirmar esta decisão definitiva?'))
form.post(url);
}}
>
<FormErrors errors={form.errors} />
<label className="block">
Decisão
<select
className={controlClass}
value={form.data.decision}
onChange={(event) =>
form.setData('decision', event.target.value)
}
>
<option value="rejected">Rejeitar</option>
<option value="approved">Aprovar</option>
</select>
</label>
<label className="block">
Fundamento
<textarea
className={controlClass}
minLength={10}
maxLength={4000}
required
value={form.data.reason}
onChange={(event) =>
form.setData('reason', event.target.value)
}
/>
</label>
<Button disabled={form.processing}>Registar decisão</Button>
</form>
);
}
function Revision({ record }: { record: Record }) {
const form = useForm({ needed_by: '', reason: '' });
return (
<form
className="mt-3 space-y-3"
onSubmit={(event) => {
event.preventDefault();
form.post(`/ordens/cancelamentos/${record.id}/rever`);
}}
>
<FormErrors errors={form.errors} />
<p>
Nova requisição em rascunho, com os artigos reutilizados. Não
copia propostas, decisões, validações ou reserva.
</p>
<label className="block">
Nova data necessária
<input
className={controlClass}
type="date"
required
value={form.data.needed_by}
onChange={(event) =>
form.setData('needed_by', event.target.value)
}
/>
</label>
<label className="block">
Nova justificação
<textarea
className={controlClass}
minLength={10}
maxLength={4000}
required
value={form.data.reason}
onChange={(event) =>
form.setData('reason', event.target.value)
}
/>
</label>
<Button disabled={form.processing}>Preparar rascunho ligado</Button>
</form>
);
}
export default function OrderCancellations({
records,
policies,
canAdminister,
}: {
records: {
data: Record[];
prev_page_url: string | null;
next_page_url: string | null;
};
policies: Policy[];
canAdminister: boolean;
}) {
const policy = useForm({
previous_id: policies[0]?.id ?? 0,
mode: 'issued_order',
enabled: false,
valid_from: '',
valid_until: '',
reason: '',
});
const labels: { [key: string]: string } = {
pending: 'Pendente',
approved: 'Aprovado',
rejected: 'Rejeitado',
};
return (
<>
<Head title="Cancelamento de ordens" />
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 p-5 lg:p-8">
<PageTitle
eyebrow="Compras e Finanças"
title="Cancelamento de ordens emitidas"
description="Circuito integral sem recepção, factura ou pagamento. Os documentos e as aprovações originais são preservados."
/>
<Link className="underline" href="/ordens">
Voltar às ordens
</Link>
<Link className="underline" href="/ordens/cancelamentos">
Ver todos os pedidos de cancelamento
</Link>
{records.data.map((record) => (
<Panel
key={record.id}
title={`${record.order_number} · ${labels[record.status]}`}
>
<div id={`cancellation-${record.id}`}>
<p className="whitespace-pre-wrap">
{record.reason}
</p>
<p className="whitespace-pre-wrap">
Suporte: {record.evidence}
</p>
<Link
className="underline"
href={record.source_href}
>
Consultar origem autorizada
</Link>
{record.review_reason && (
<p className="mt-3 whitespace-pre-wrap">
Decisão: {record.review_reason}
</p>
)}
{record.canDecide && (
<Decision
url={`/ordens/cancelamentos/${record.id}/decidir`}
/>
)}
{record.revised_requisition_id ? (
<Link
className="mt-3 block underline"
href={`/requisicoes/${record.revised_requisition_id}`}
>
Consultar rascunho ligado
</Link>
) : (
record.canRevise && <Revision record={record} />
)}
</div>
</Panel>
))}
{!records.data.length && (
<p>Sem pedidos de cancelamento visíveis.</p>
)}
<nav className="flex gap-4">
{records.prev_page_url && (
<Link href={records.prev_page_url}>Anterior</Link>
)}
{records.next_page_url && (
<Link href={records.next_page_url}>Seguinte</Link>
)}
</nav>
{canAdminister && (
<Panel title="Política própria da empresa">
<p>
Uma proposta e duas aprovações administrativas de
pessoas distintas são necessárias. Publicar uma
política não cancela ordens.
</p>
{policies.map((item) => (
<article
className="mt-3 rounded border p-3"
key={item.id}
id={`policy-${item.id}`}
>
<p>
Versão {item.revision} ·{' '}
{labels[item.status]} ·{' '}
{item.enabled
? 'Permite cancelamento'
: 'Não permite cancelamento'}{' '}
· {item.valid_from} a {item.valid_until}
</p>
{item.canReview && (
<Decision
url={`/ordens/cancelamentos/politicas/${item.id}/decidir`}
/>
)}
</article>
))}
<details className="mt-4">
<summary>Propor nova versão</summary>
<form
className="mt-3 space-y-3"
onSubmit={(event) => {
event.preventDefault();
policy.transform((data) => ({
...data,
previous_id: policies[0]?.id ?? 0,
}));
policy.post(
'/ordens/cancelamentos/politicas',
);
}}
>
<FormErrors errors={policy.errors} />
<label className="block">
<input
type="checkbox"
checked={policy.data.enabled}
onChange={(event) =>
policy.setData(
'enabled',
event.target.checked,
)
}
/>{' '}
Permitir cancelamento de ordens neste âmbito
</label>
<label className="block">
Vigência inicial
<input
className={controlClass}
type="date"
required
value={policy.data.valid_from}
onChange={(event) =>
policy.setData(
'valid_from',
event.target.value,
)
}
/>
</label>
<label className="block">
Vigência final
<input
className={controlClass}
type="date"
required
value={policy.data.valid_until}
onChange={(event) =>
policy.setData(
'valid_until',
event.target.value,
)
}
/>
</label>
<label className="block">
Fundamento
<textarea
className={controlClass}
minLength={10}
maxLength={4000}
required
value={policy.data.reason}
onChange={(event) =>
policy.setData(
'reason',
event.target.value,
)
}
/>
</label>
<Button disabled={policy.processing}>
Propor política
</Button>
</form>
</details>
</Panel>
)}
</div>
</>
);
}
OrderCancellations.layout = {
breadcrumbs: [
{ title: 'Ordens', href: '/ordens' },
{ title: 'Cancelamentos', href: '/ordens/cancelamentos' },
],
};