563 lines
35 KiB
TypeScript
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: '#' },
|
|
],
|
|
};
|