231 lines
12 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|