Files
compras/resources/js/components/inventory-lot-count.tsx
T
2026-11-02 08:08:05 +02:00

212 lines
8.7 KiB
TypeScript

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { quantity } from '@/types/inventory';
import { controlClass } from '@/components/procurement';
export type CountInput = {
lot_id: number | null;
new_key: string | null;
quantity: string;
code: string;
expires_on: string;
no_expiry: boolean;
};
export type LotCountSource = {
stock_item_id: number;
warehouse_id: number;
hash: string;
lots: {
lot_id: number;
metadata_id: number;
quantity_milli: number;
code: string | null;
expires_on: string | null;
no_expiry: boolean;
}[];
};
export function countTotal(rows: CountInput[]): string | null {
let total = 0;
for (const row of rows) {
if (!/^\d{1,9}(\.\d{1,3})?$/.test(row.quantity)) return null;
const [whole, fraction = ''] = row.quantity.split('.');
total += Number(whole) * 1000 + Number(fraction.padEnd(3, '0'));
}
return `${Math.floor(total / 1000)}.${String(total % 1000).padStart(3, '0')}`;
}
export function LotCountFields({
source,
rows,
setRows,
}: {
source: LotCountSource;
rows: CountInput[];
setRows: (rows: CountInput[]) => void;
}) {
const change = (index: number, update: Partial<CountInput>) =>
setRows(
rows.map((row, i) => (i === index ? { ...row, ...update } : row)),
);
const exhausted = source.lots.filter(
(lot) =>
lot.quantity_milli === 0 &&
!rows.some((row) => row.lot_id === lot.lot_id),
);
return (
<fieldset className="space-y-3 rounded-lg border p-4 sm:col-span-2">
<legend className="px-2 text-sm font-semibold">
Conferência física por lote
</legend>
<p className="text-sm text-muted-foreground">
Registe apenas o que contou. O total é calculado; identificação,
validade e saldo de referência vêm do registo existente. A
contagem não altera existências nem liberta qualidade.
</p>
{rows.map((row, index) => {
const known = source.lots.find(
(lot) => lot.lot_id === row.lot_id,
);
return (
<div
key={row.lot_id ?? row.new_key}
className="grid gap-3 rounded border p-3 md:grid-cols-2"
>
<p className="text-sm">
{known
? `${known.code ?? `Lote #${known.lot_id}`} · Saldo: ${quantity(known.quantity_milli)} · Validade: ${known.expires_on ?? (known.no_expiry ? 'Sem prazo confirmado' : 'Por confirmar')}`
: 'Lote encontrado sem identidade anterior neste local'}
</p>
<label className="grid gap-1 text-sm">
Quantidade contada
<Input
required
inputMode="decimal"
pattern="\d{1,9}(\.\d{1,3})?"
value={row.quantity}
onChange={(e) =>
change(index, { quantity: e.target.value })
}
/>
</label>
{!known && (
<>
<label className="grid gap-1 text-sm">
Código, se conhecido
<Input
maxLength={100}
value={row.code}
onChange={(e) =>
change(index, {
code: e.target.value,
})
}
/>
</label>
<label className="grid gap-1 text-sm">
Validade, se conhecida
<Input
type="date"
value={row.expires_on}
onChange={(e) =>
change(index, {
expires_on: e.target.value,
})
}
/>
</label>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={row.no_expiry}
onChange={(e) =>
change(index, {
no_expiry: e.target.checked,
})
}
/>
Confirmo artigo sem prazo de validade
</label>
<Button
type="button"
variant="outline"
onClick={() =>
setRows(
rows.filter((_, i) => i !== index),
)
}
>
Remover linha encontrada
</Button>
</>
)}
</div>
);
})}
<Button
type="button"
variant="outline"
onClick={() =>
setRows([
...rows,
{
lot_id: null,
new_key: crypto.randomUUID(),
quantity: '',
code: '',
expires_on: '',
no_expiry: false,
},
])
}
>
Acrescentar lote encontrado
</Button>
{exhausted.length > 0 && (
<label className="grid gap-1 text-sm">
Reutilizar identidade de lote sem saldo que voltou a
encontrar
<select
className={controlClass}
value=""
onChange={(e) => {
const lot = exhausted.find(
(l) => String(l.lot_id) === e.target.value,
);
if (lot)
setRows([
...rows,
{
lot_id: lot.lot_id,
new_key: null,
quantity: '',
code: lot.code ?? '',
expires_on: lot.expires_on ?? '',
no_expiry: lot.no_expiry,
},
]);
}}
>
<option value="">
Seleccione apenas se encontrou quantidade deste lote
</option>
{exhausted.map((lot) => (
<option key={lot.lot_id} value={lot.lot_id}>
{lot.code ?? `Lote #${lot.lot_id}`}
</option>
))}
</select>
</label>
)}
<p className="text-sm font-medium">
Total contado: {countTotal(rows) ?? 'Complete as quantidades'}
</p>
<p className="text-sm text-muted-foreground">
Quantidades encontradas a mais ficam em quarentena após o
ajuste, mesmo num lote anteriormente libertado. A aplicação
exige conferência independente da contagem.
</p>
</fieldset>
);
}