Files
compras/resources/js/pages/stock/availability.tsx
T
2026-11-02 08:10:00 +02:00

231 lines
12 KiB
TypeScript

import { Head, Link, useForm } from '@inertiajs/react';
import {
PageTitle,
Panel,
FormErrors,
controlClass,
} from '@/components/procurement';
import { Button } from '@/components/ui/button';
type Row = {
id: number;
code: string;
name: string;
unit: string;
physical_milli: number;
usable_milli: number | null;
blocked_milli: number | null;
lot_control: boolean;
reconciled: boolean;
planned_milli: number | null;
minimum_milli: number | null;
at_minimum: boolean;
physical_commitment_milli: number | null;
free_milli: number | null;
};
type Props = {
filters: { warehouse_id?: number; search?: string };
warehouses: { id: number; code: string; name: string }[];
warehouse: { name: string } | null;
checkedAt: string;
planningVisible: boolean;
rows: null | {
data: Row[];
total: number;
prev_page_url: string | null;
next_page_url: string | null;
};
};
const quantity = (n: number | null) =>
n === null
? 'Por determinar'
: (n / 1000).toLocaleString('pt-PT', { maximumFractionDigits: 3 });
export default function Availability(props: Props) {
const form = useForm({
warehouse_id: props.filters.warehouse_id?.toString() ?? '',
search: props.filters.search ?? '',
});
return (
<>
<Head title="Disponibilidade para requisições" />
<div className="space-y-6 p-6">
<PageTitle
eyebrow="Armazém e activos"
title="Disponibilidade para requisições"
description="Consulta dos registos existentes. Consultar não reserva nem autoriza uma saída."
/>
<Panel title="Escolher armazém">
<form
className="grid gap-4 md:grid-cols-3"
onSubmit={(e) => {
e.preventDefault();
form.get('/stock/disponibilidade');
}}
>
<label>
Armazém
<select
required
className={controlClass}
value={form.data.warehouse_id}
onChange={(e) =>
form.setData('warehouse_id', e.target.value)
}
>
<option value="">Seleccione</option>
{props.warehouses.map((warehouse) => (
<option
key={warehouse.id}
value={warehouse.id}
>
{warehouse.code} · {warehouse.name}
</option>
))}
</select>
</label>
<label>
Artigo ou código
<input
className={controlClass}
value={form.data.search}
onChange={(e) =>
form.setData('search', e.target.value)
}
/>
</label>
<Button disabled={form.processing}>Consultar</Button>
<FormErrors errors={form.errors} />
</form>
</Panel>
<Panel
title={props.warehouse?.name ?? 'Disponibilidade'}
description={`Consulta em ${props.checkedAt}. Cada quantidade mantém a unidade do artigo.`}
>
<p className="mb-4 text-sm">
As necessidades planeadas são apresentadas
separadamente. O compromisso físico e a reserva
autorizada de requisições ao armazém ainda não estão
implementados; não são considerados zero nem deduzidos
como se já existissem.
</p>
{!props.rows ? (
<p>Seleccione o armazém para consultar.</p>
) : (
<>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr>
{[
'Artigo',
'Físico',
'Utilizável pelos controlos activos',
'Retido/indisponível',
'Necessidades planeadas',
'Reservado / livre',
'Mínimo aprovado',
'Situação',
].map((label) => (
<th
className="border-b p-3"
key={label}
>
{label}
</th>
))}
</tr>
</thead>
<tbody>
{props.rows.data.map((row) => (
<tr key={row.id}>
<td className="border-b p-3">
{row.code} · {row.name}
<p>{row.unit}</p>
</td>
<td className="border-b p-3">
{quantity(
row.physical_milli,
)}
</td>
<td className="border-b p-3">
{quantity(row.usable_milli)}
{!row.lot_control && (
<p className="text-xs">
Sem controlo por
lote neste âmbito
</p>
)}
</td>
<td className="border-b p-3">
{row.lot_control
? quantity(
row.blocked_milli,
)
: 'Sem controlo por lote'}
</td>
<td className="border-b p-3">
{props.planningVisible
? quantity(
row.planned_milli,
)
: 'Sem acesso ao planeamento'}
</td>
<td className="border-b p-3">
{quantity(
row.physical_commitment_milli,
)}{' '}
/ {quantity(row.free_milli)}
</td>
<td className="border-b p-3">
{quantity(
row.minimum_milli,
)}
</td>
<td className="border-b p-3">
{!row.reconciled
? 'Reconciliar repartição dos lotes'
: row.usable_milli === 0
? 'Sem material utilizável — necessidade de compra'
: row.at_minimum
? 'No mínimo ou abaixo — necessidade de compra'
: row.minimum_milli ===
null
? 'Mínimo por determinar'
: 'Acima do mínimo'}
</td>
</tr>
))}
</tbody>
</table>
</div>
{props.rows.data.length === 0 && (
<p>Sem artigos nos filtros seleccionados.</p>
)}
<p className="mt-4 text-sm">
A compra automática usa a parametrização do
artigo e armazém. Esta consulta não cria
documentos.{' '}
<Link href="/stock/reposicao">
Consultar reposição e necessidades pendentes
</Link>
.
</p>
<div className="mt-4 flex gap-4">
{props.rows.prev_page_url && (
<Link href={props.rows.prev_page_url}>
Anterior
</Link>
)}
{props.rows.next_page_url && (
<Link href={props.rows.next_page_url}>
Seguinte
</Link>
)}
</div>
</>
)}
</Panel>
</div>
</>
);
}