Files
compras/resources/js/pages/receipts/order.tsx
T

563 lines
35 KiB
TypeScript

import { Head, Link, useForm } from '@inertiajs/react';
import { InventoryNotice, ReceiptProgressTable } from '@/components/inventory';
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 { quantity, qualityStates, receiptStates } from '@/types/inventory';
import type {
Receipt,
ReceiptProgress,
StockItem,
Warehouse,
} from '@/types/inventory';
type Line = {
include: boolean;
purchase_order_item_id: number;
received_quantity: string;
accepted_quantity: string;
quality_status: string;
notes: string;
stock_item_id: string;
};
type Props = {
order: { id: number; number: string };
supplier_name: string;
progress: ReceiptProgress;
receipts: Receipt[];
warehouses: Warehouse[];
stockItems: StockItem[];
canConfirm: boolean;
};
export default function ReceiveOrder({
order,
supplier_name,
progress,
receipts,
warehouses,
stockItems,
canConfirm,
}: Props) {
const form = useForm({
idempotency_key: crypto.randomUUID(),
delivery_note: '',
received_on: '',
warehouse_id: '',
items: progress.lines.map((line) => ({
include: false,
purchase_order_item_id: line.id,
received_quantity: '',
accepted_quantity: '',
quality_status: 'compliant',
notes: '',
stock_item_id: '',
})),
});
const change = (index: number, updates: Partial<Line>) =>
form.setData(
'items',
form.data.items.map((line, i) =>
i === index ? { ...line, ...updates } : line,
),
);
const outstanding = progress.lines.some(
(line) => line.delivery_pending_milli > 0,
);
return (
<>
<Head title={`Receber ${order.number}`} />
<div className="mx-auto flex w-full max-w-6xl flex-col gap-7 p-5 lg:p-8">
<PageTitle
eyebrow={order.number}
title="Conferir entrega"
description={supplier_name}
/>
<InventoryNotice />
<Panel title="Quantidades da ordem">
<ReceiptProgressTable progress={progress} />
<p className="mt-4 text-xs text-muted-foreground">
As quantidades rejeitadas contam como conferidas, mas
não entram em stock. Substituições ou reposições após
rejeição não são autorizadas automaticamente.
</p>
</Panel>
{canConfirm && outstanding ? (
<Panel
title="Confirmar recepção"
description="A confirmação é definitiva e auditada. O emitente da ordem não pode confirmar a recepção."
>
<form
className="space-y-5"
onSubmit={(e) => {
e.preventDefault();
form.transform((data) => ({
...data,
warehouse_id: data.warehouse_id || null,
items: data.items
.filter((line) => line.include)
.map(
({
include: _include,
...line
}) => ({
...line,
stock_item_id:
line.stock_item_id || null,
}),
),
}));
form.post(`/ordens/${order.id}/recepcoes`);
}}
>
<FormErrors errors={form.errors} />
<div className="grid gap-4 md:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="receipt-note">
Guia de remessa
</Label>
<Input
id="receipt-note"
required
maxLength={100}
value={form.data.delivery_note}
onChange={(e) =>
form.setData(
'delivery_note',
e.target.value,
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="receipt-date">
Data da recepção
</Label>
<Input
id="receipt-date"
type="date"
required
value={form.data.received_on}
onChange={(e) =>
form.setData(
'received_on',
e.target.value,
)
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="receipt-warehouse">
Armazém (para bens aceites)
</Label>
<select
id="receipt-warehouse"
className={controlClass}
value={form.data.warehouse_id}
onChange={(e) =>
form.setData(
'warehouse_id',
e.target.value,
)
}
>
<option value="">Seleccione</option>
{warehouses.map((w) => (
<option key={w.id} value={w.id}>
{w.code} · {w.name}
</option>
))}
</select>
</div>
</div>
<div className="space-y-5">
{progress.lines.map(
(source, index) =>
source.delivery_pending_milli > 0 && (
<fieldset
className="rounded-xl border p-4"
key={source.id}
>
<legend className="px-2 text-sm font-semibold">
{source.description} · Por
entregar{' '}
{quantity(
source.delivery_pending_milli,
)}{' '}
{source.unit}
</legend>
<label className="mb-4 flex items-center gap-3 text-sm">
<input
type="checkbox"
checked={
form.data.items[
index
].include
}
onChange={(e) =>
change(index, {
include:
e.target
.checked,
})
}
/>
Incluir nesta recepção
</label>
{form.data.items[index]
.include && (
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label
htmlFor={`received-${source.id}`}
>
Quantidade
recebida
</Label>
<Input
id={`received-${source.id}`}
type="number"
required
min="0.001"
step="0.001"
max={
source.delivery_pending_milli /
1000
}
value={
form.data
.items[
index
]
.received_quantity
}
onChange={(e) =>
change(
index,
{
received_quantity:
e
.target
.value,
...(form
.data
.items[
index
]
.quality_status ===
'compliant'
? {
accepted_quantity:
e
.target
.value,
}
: {}),
},
)
}
/>
</div>
<div className="space-y-2">
<Label
htmlFor={`quality-${source.id}`}
>
Qualidade
</Label>
<select
id={`quality-${source.id}`}
className={
controlClass
}
value={
form.data
.items[
index
]
.quality_status
}
onChange={(e) =>
change(
index,
{
quality_status:
e
.target
.value,
...(e
.target
.value ===
'nonconforming'
? {
accepted_quantity:
'0',
stock_item_id:
'',
}
: e
.target
.value ===
'compliant'
? {
accepted_quantity:
form
.data
.items[
index
]
.received_quantity,
}
: {}),
},
)
}
>
{Object.entries(
qualityStates,
).map(
([
value,
label,
]) => (
<option
key={
value
}
value={
value
}
>
{
label
}
</option>
),
)}
</select>
</div>
<div className="space-y-2">
<Label
htmlFor={`accepted-${source.id}`}
>
Quantidade
aceite
</Label>
<Input
id={`accepted-${source.id}`}
type="number"
required
min="0"
step="0.001"
readOnly={
form.data
.items[
index
]
.quality_status !==
'reservations'
}
value={
form.data
.items[
index
]
.accepted_quantity
}
onChange={(e) =>
change(
index,
{
accepted_quantity:
e
.target
.value,
},
)
}
/>
</div>
{source.purchase_type !==
'service' ? (
<div className="space-y-2">
<Label
htmlFor={`stock-${source.id}`}
>
Artigo de
stock ·
unidade{' '}
{
source.unit
}
</Label>
<select
id={`stock-${source.id}`}
className={
controlClass
}
value={
form
.data
.items[
index
]
.stock_item_id
}
onChange={(
e,
) =>
change(
index,
{
stock_item_id:
e
.target
.value,
},
)
}
>
<option value="">
Seleccione
para a
quantidade
aceite
</option>
{stockItems
.filter(
(
item,
) =>
item.unit.toLowerCase() ===
source.unit.toLowerCase(),
)
.map(
(
item,
) => (
<option
key={
item.id
}
value={
item.id
}
>
{
item.code
}{' '}
·{' '}
{
item.name
}
</option>
),
)}
</select>
</div>
) : (
<p className="self-center text-sm text-muted-foreground">
Serviço: a
aceitação não
cria existências
físicas.
</p>
)}
<div className="space-y-2 md:col-span-2">
<Label
htmlFor={`notes-${source.id}`}
>
Observações e
divergências
</Label>
<textarea
id={`notes-${source.id}`}
className={`${controlClass} h-24 py-3`}
required={
form.data
.items[
index
]
.quality_status !==
'compliant'
}
maxLength={3000}
value={
form.data
.items[
index
].notes
}
onChange={(e) =>
change(
index,
{
notes: e
.target
.value,
},
)
}
/>
</div>
</div>
)}
</fieldset>
),
)}
</div>
<p className="text-xs text-muted-foreground">
Configure previamente os armazéns e artigos em
Stock. Não existem conversões automáticas de
unidade, tolerâncias de excesso ou saldo inicial
manual.
</p>
<Button disabled={form.processing}>
Confirmar recepção e entradas aceites
</Button>
</form>
</Panel>
) : (
<p className="text-sm text-muted-foreground">
{outstanding
? 'A confirmação exige o perfil Recepção / Armazém e uma pessoa distinta do emitente.'
: 'Não existem quantidades de entrega pendentes nesta ordem.'}
</p>
)}
<Panel title="Recepções anteriores">
<div className="divide-y">
{receipts.map((receipt) => (
<Link
key={receipt.id}
href={`/recepcoes/${receipt.id}`}
className="flex flex-wrap justify-between gap-3 py-3 text-sm"
>
<span>
{receipt.number} · Guia{' '}
{receipt.delivery_note}
</span>
<span>{receiptStates[receipt.status]}</span>
</Link>
))}
</div>
{receipts.length === 0 && (
<p className="text-sm text-muted-foreground">
Ainda não há recepções confirmadas.
</p>
)}
</Panel>
</div>
</>
);
}
ReceiveOrder.layout = {
breadcrumbs: [
{ title: 'Recepção', href: '/recepcoes' },
{ title: 'Conferir ordem', href: '#' },
],
};