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