427 lines
19 KiB
TypeScript
427 lines
19 KiB
TypeScript
import { Head, Link, useForm } from '@inertiajs/react';
|
|
import {
|
|
PageTitle,
|
|
Panel,
|
|
FormErrors,
|
|
controlClass,
|
|
} from '@/components/procurement';
|
|
import { Button } from '@/components/ui/button';
|
|
type Line = {
|
|
item: { code: string; name: string; unit: string };
|
|
quantity_milli: number;
|
|
destination: string;
|
|
equipment: { code: string; description: string } | null;
|
|
};
|
|
type Event = {
|
|
id: number;
|
|
version: number;
|
|
action: string;
|
|
actor_id: number;
|
|
reason: string;
|
|
created_at: string;
|
|
data: {
|
|
actor?: { id: number; name: string };
|
|
lines?: {
|
|
index: number;
|
|
quantity_milli: number;
|
|
stock_operation_id: number;
|
|
}[];
|
|
delivery_event_id?: number;
|
|
};
|
|
};
|
|
type Props = {
|
|
record: {
|
|
id: number;
|
|
snapshot: {
|
|
requester: { name: string };
|
|
warehouse: { name: string };
|
|
cost_center: { code: string; name: string };
|
|
destination: string;
|
|
reason: string;
|
|
lines: Line[];
|
|
};
|
|
};
|
|
progress: {
|
|
state: string;
|
|
version: number;
|
|
remaining: number[];
|
|
reserved: number[];
|
|
cancelled: number[];
|
|
reservation_ready: boolean;
|
|
delivered: number[];
|
|
partial_requested: boolean;
|
|
partial_allowed: boolean;
|
|
events: Event[];
|
|
received_deliveries: number[];
|
|
};
|
|
ready: boolean;
|
|
canSubmit: boolean;
|
|
canReview: boolean;
|
|
canDeliver: boolean;
|
|
canReceive: boolean;
|
|
canManageReservation: boolean;
|
|
};
|
|
const labels: Record<string, string> = {
|
|
draft: 'Rascunho',
|
|
submitted: 'Submetida',
|
|
authorized: 'Autorizada',
|
|
partial: 'Parcialmente entregue',
|
|
delivered: 'Entregue',
|
|
closed: 'Encerrada — recepção confirmada',
|
|
rejected: 'Rejeitada',
|
|
submit: 'Submissão',
|
|
authorize: 'Autorização',
|
|
reject: 'Rejeição',
|
|
deliver: 'Entrega física',
|
|
receive: 'Confirmação de recepção',
|
|
reserve: 'Actualizar reserva de stock',
|
|
cancel: 'Cancelar saldo e libertar reserva',
|
|
cancelled: 'Saldo cancelado',
|
|
};
|
|
function Command({
|
|
props,
|
|
action,
|
|
delivery,
|
|
}: {
|
|
props: Props;
|
|
action: string;
|
|
delivery?: number;
|
|
}) {
|
|
const form = useForm({
|
|
operation_key: crypto.randomUUID(),
|
|
version: props.progress.version,
|
|
action,
|
|
reason: '',
|
|
partial_allowed: false,
|
|
delivery_event_id: delivery,
|
|
lines: props.progress.remaining
|
|
.map((qty, index) => ({ index, quantity: '', remaining: qty }))
|
|
.filter((line) => line.remaining > 0),
|
|
});
|
|
return (
|
|
<details className="rounded border p-3 print:hidden">
|
|
<summary className="cursor-pointer font-medium">
|
|
{labels[action]}
|
|
</summary>
|
|
<form
|
|
className="mt-3 space-y-3"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
form.transform((values) => ({
|
|
...values,
|
|
lines:
|
|
action === 'deliver'
|
|
? values.lines
|
|
.filter((line) => line.quantity !== '')
|
|
.map(({ index, quantity }) => ({
|
|
index,
|
|
quantity,
|
|
}))
|
|
: undefined,
|
|
}));
|
|
form.post(`/stock/requisicoes/${props.record.id}/eventos`, {
|
|
preserveState: 'errors',
|
|
});
|
|
}}
|
|
>
|
|
<FormErrors errors={form.errors} />
|
|
{(action === 'submit' || action === 'authorize') && (
|
|
<label className="flex gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={form.data.partial_allowed}
|
|
disabled={
|
|
action === 'authorize' &&
|
|
!props.progress.partial_requested
|
|
}
|
|
onChange={(e) =>
|
|
form.setData(
|
|
'partial_allowed',
|
|
e.target.checked,
|
|
)
|
|
}
|
|
/>
|
|
{action === 'submit'
|
|
? 'Aceito entregas parciais'
|
|
: 'Permitir entregas parciais aceites pelo requisitante'}
|
|
</label>
|
|
)}
|
|
{action === 'deliver' && (
|
|
<>
|
|
<p className="text-sm">
|
|
Preencha só as quantidades entregues agora. A
|
|
requisição é o documento de saída, sem factura. A
|
|
data de registo é a data efectiva da entrega.{' '}
|
|
{props.progress.partial_allowed
|
|
? 'As quantidades não entregues mantêm-se pendentes.'
|
|
: 'Esta autorização exige uma entrega integral.'}
|
|
</p>
|
|
{form.data.lines.map((line, i) => (
|
|
<label className="block" key={line.index}>
|
|
{
|
|
props.record.snapshot.lines[line.index].item
|
|
.code
|
|
}{' '}
|
|
· Pendente {line.remaining / 1000}{' '}
|
|
{
|
|
props.record.snapshot.lines[line.index].item
|
|
.unit
|
|
}
|
|
<input
|
|
inputMode="decimal"
|
|
placeholder="Quantidade efectivamente entregue"
|
|
className={controlClass}
|
|
value={line.quantity}
|
|
onChange={(e) =>
|
|
form.setData(
|
|
'lines',
|
|
form.data.lines.map((row, j) =>
|
|
j === i
|
|
? {
|
|
...row,
|
|
quantity:
|
|
e.target.value,
|
|
}
|
|
: row,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
))}
|
|
</>
|
|
)}
|
|
<label className="block">
|
|
Fundamento / registo da intervenção
|
|
<textarea
|
|
required
|
|
minLength={10}
|
|
maxLength={4000}
|
|
className={controlClass}
|
|
value={form.data.reason}
|
|
onChange={(e) => form.setData('reason', e.target.value)}
|
|
/>
|
|
</label>
|
|
<Button disabled={form.processing}>{labels[action]}</Button>
|
|
</form>
|
|
</details>
|
|
);
|
|
}
|
|
export default function WarehouseRequest(props: Props) {
|
|
const r = props.record;
|
|
const p = props.progress;
|
|
return (
|
|
<>
|
|
<Head title={`Requisição RA-${r.id}`} />
|
|
<div className="space-y-6 p-6">
|
|
<PageTitle
|
|
eyebrow="Documento de saída interna · Sem factura"
|
|
title={`Requisição RA-${r.id}`}
|
|
description={`Estado: ${labels[p.state] ?? p.state}`}
|
|
/>
|
|
<div className="flex gap-4 print:hidden">
|
|
<Link href="/stock/requisicoes">
|
|
Requisições ao armazém
|
|
</Link>
|
|
<Button variant="outline" onClick={() => window.print()}>
|
|
Imprimir requisição e entregas
|
|
</Button>
|
|
</div>
|
|
<Panel title="Utilização e origem">
|
|
<p>Requisitante: {r.snapshot.requester.name}</p>
|
|
<p>Armazém: {r.snapshot.warehouse.name}</p>
|
|
<p>
|
|
Centro de custo: {r.snapshot.cost_center.code} ·{' '}
|
|
{r.snapshot.cost_center.name}
|
|
</p>
|
|
<p>Local de utilização: {r.snapshot.destination}</p>
|
|
<p>Finalidade: {r.snapshot.reason}</p>
|
|
</Panel>
|
|
<Panel title="Artigos e atendimento">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left text-sm">
|
|
<thead>
|
|
<tr>
|
|
{[
|
|
'Artigo / unidade',
|
|
'Destino',
|
|
'Pedido',
|
|
'Entregue',
|
|
'Reservado',
|
|
'Cancelado',
|
|
'Pendente',
|
|
].map((label) => (
|
|
<th
|
|
className="border-b p-3"
|
|
key={label}
|
|
>
|
|
{label}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{r.snapshot.lines.map((line, index) => (
|
|
<tr key={index}>
|
|
<td className="border-b p-3">
|
|
{line.item.code} · {line.item.name}{' '}
|
|
({line.item.unit})
|
|
</td>
|
|
<td className="border-b p-3">
|
|
{line.equipment
|
|
? `${line.equipment.code} · ${line.equipment.description}`
|
|
: line.destination}
|
|
</td>
|
|
<td className="border-b p-3">
|
|
{line.quantity_milli / 1000}
|
|
</td>
|
|
<td className="border-b p-3">
|
|
{p.delivered[index] / 1000}
|
|
</td>
|
|
<td className="border-b p-3">
|
|
{p.reservation_ready
|
|
? p.reserved[index] / 1000
|
|
: 'Por instalar'}
|
|
</td>
|
|
<td className="border-b p-3">
|
|
{p.cancelled[index] / 1000}
|
|
</td>
|
|
<td className="border-b p-3">
|
|
{p.remaining[index] / 1000}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<p className="mt-4 text-sm">
|
|
{p.reservation_ready
|
|
? 'A autorização reserva o material utilizável disponível. Só a entrega confirmada reduz o stock. Retenções de qualidade e validade são revalidadas na entrega.'
|
|
: 'Reserva de stock por instalar. A disponibilidade é revalidada na entrega.'}
|
|
</p>
|
|
</Panel>
|
|
{!props.ready ? (
|
|
<Panel title="Instalação pendente">
|
|
<p>Autorização e entrega ainda não instaladas.</p>
|
|
</Panel>
|
|
) : (
|
|
<div className="space-y-3 print:hidden">
|
|
{props.canSubmit && (
|
|
<Command
|
|
key={`submit:${p.version}`}
|
|
props={props}
|
|
action="submit"
|
|
/>
|
|
)}
|
|
{props.canReview && (
|
|
<>
|
|
<Command
|
|
key={`authorize:${p.version}`}
|
|
props={props}
|
|
action="authorize"
|
|
/>
|
|
<Command
|
|
key={`reject:${p.version}`}
|
|
props={props}
|
|
action="reject"
|
|
/>
|
|
</>
|
|
)}
|
|
{props.canDeliver && (
|
|
<Command
|
|
key={`deliver:${p.version}`}
|
|
props={props}
|
|
action="deliver"
|
|
/>
|
|
)}
|
|
{props.canManageReservation && p.reservation_ready && (
|
|
<>
|
|
<Command
|
|
key={`reserve:${p.version}`}
|
|
props={props}
|
|
action="reserve"
|
|
/>
|
|
<Command
|
|
key={`cancel:${p.version}`}
|
|
props={props}
|
|
action="cancel"
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
<Panel title="Decisões, entregas e recepção">
|
|
{p.events.length === 0 ? (
|
|
<p>Sem intervenções.</p>
|
|
) : (
|
|
p.events.map((event) => (
|
|
<div
|
|
key={event.id}
|
|
className="mb-4 rounded border p-3"
|
|
>
|
|
<p className="font-medium">
|
|
{labels[event.action]} · Registo #{event.id}{' '}
|
|
·{' '}
|
|
{event.data.actor?.name ??
|
|
`Utilizador #${event.actor_id}`}
|
|
</p>
|
|
<p>{event.created_at}</p>
|
|
<p className="whitespace-pre-wrap">
|
|
{event.reason}
|
|
</p>
|
|
{event.action === 'deliver' && (
|
|
<>
|
|
<p>
|
|
Documento de saída: RA-{r.id} ·
|
|
Entrega #{event.id}
|
|
</p>
|
|
{event.data.lines?.map((line) => (
|
|
<p key={line.index}>
|
|
{
|
|
r.snapshot.lines[line.index]
|
|
.item.code
|
|
}{' '}
|
|
· {line.quantity_milli / 1000}{' '}
|
|
{
|
|
r.snapshot.lines[line.index]
|
|
.item.unit
|
|
}{' '}
|
|
· Operação de stock #
|
|
{line.stock_operation_id}
|
|
</p>
|
|
))}
|
|
<p>
|
|
{p.received_deliveries.includes(
|
|
event.id,
|
|
)
|
|
? 'Recepção confirmada pelo requisitante'
|
|
: 'Recepção por confirmar'}
|
|
</p>
|
|
{props.canReceive &&
|
|
!p.received_deliveries.includes(
|
|
event.id,
|
|
) && (
|
|
<Command
|
|
key={`receive:${event.id}:${p.version}`}
|
|
props={props}
|
|
action="receive"
|
|
delivery={event.id}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
{event.action === 'receive' && (
|
|
<p>
|
|
Relativa à entrega #
|
|
{event.data.delivery_event_id}
|
|
</p>
|
|
)}
|
|
</div>
|
|
))
|
|
)}
|
|
</Panel>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|