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

438 lines
22 KiB
TypeScript

import { Head, Link, useForm } from '@inertiajs/react';
import {
PageTitle,
Panel,
FormErrors,
controlClass,
} from '@/components/procurement';
import { Button } from '@/components/ui/button';
type Item = { id: number; code: string; name: string; unit: string };
type Line = {
stock_item_id: string;
quantity: string;
use: string;
equipment_id: string;
};
type Snapshot = {
requester: { name: string };
warehouse: { name: string };
cost_center: { code: string; name: string };
destination: string;
reason: string;
priority: string;
lines: {
item: Item;
quantity_milli: number;
use: string;
destination: string;
equipment: { code: string; description: string } | null;
}[];
};
type Props = {
ready: boolean;
canPrepare: boolean;
items: Item[];
warehouses: { id: number; code: string; name: string }[];
costCenters: { id: number; code: string; name: string }[];
equipment: { id: number; code: string; description: string }[];
records: null | {
data: {
id: number;
status: string;
snapshot: Snapshot;
progress?: { state: string };
}[];
prev_page_url: string | null;
next_page_url: string | null;
};
};
const blank = (): Line => ({
stock_item_id: '',
quantity: '',
use: 'internal',
equipment_id: '',
});
export default function Requests(props: Props) {
const form = useForm({
operation_key: crypto.randomUUID(),
warehouse_id: '',
cost_center_id: '',
destination: '',
reason: '',
priority: 'normal',
lines: [blank()],
});
const line = (index: number, values: Partial<Line>) =>
form.setData(
'lines',
form.data.lines.map((row, i) =>
i === index ? { ...row, ...values } : row,
),
);
return (
<>
<Head title="Requisições ao armazém" />
<div className="space-y-6 p-6">
<PageTitle
eyebrow="Armazém e activos"
title="Requisições ao armazém"
description="Registo inicial com destino de utilização. Um rascunho não autoriza, reserva ou retira material."
/>
<p className="text-sm">
Uso interno sem factura: a requisição autorizada acompanha a
saída e as entregas. Reserva física exclusiva e geração
automática de compra por falta ainda estão em implementação.
A venda a terceiros pertence à composição SGF.
</p>
<Link href="/stock/disponibilidade">
Consultar disponibilidade (conforme o seu acesso)
</Link>
{!props.ready ? (
<Panel title="Instalação pendente">
<p>
O registo de requisições ainda não está instalado.
</p>
</Panel>
) : (
props.canPrepare && (
<Panel title="Preparar rascunho">
<form
className="space-y-4"
onSubmit={(e) => {
e.preventDefault();
form.post('/stock/requisicoes', {
preserveState: 'errors',
});
}}
>
<FormErrors errors={form.errors} />
<div className="grid gap-4 md:grid-cols-2">
<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((w) => (
<option key={w.id} value={w.id}>
{w.code} · {w.name}
</option>
))}
</select>
</label>
<label>
Centro de custo
<select
required
className={controlClass}
value={form.data.cost_center_id}
onChange={(e) =>
form.setData(
'cost_center_id',
e.target.value,
)
}
>
<option value="">
Seleccione do seu departamento
</option>
{props.costCenters.map((c) => (
<option key={c.id} value={c.id}>
{c.code} · {c.name}
</option>
))}
</select>
</label>
<label>
Onde será utilizado
<input
required
className={controlClass}
value={form.data.destination}
onChange={(e) =>
form.setData(
'destination',
e.target.value,
)
}
/>
</label>
<label>
Prioridade
<select
className={controlClass}
value={form.data.priority}
onChange={(e) =>
form.setData(
'priority',
e.target.value,
)
}
>
<option value="normal">
Normal
</option>
<option value="urgent">
Urgente
</option>
</select>
</label>
</div>
<label className="block">
Finalidade/justificação
<textarea
required
minLength={10}
className={controlClass}
value={form.data.reason}
onChange={(e) =>
form.setData(
'reason',
e.target.value,
)
}
/>
</label>
{form.data.lines.map((row, index) => (
<fieldset
key={index}
className="grid gap-3 rounded border p-3 md:grid-cols-2"
>
<legend>Artigo {index + 1}</legend>
<label>
Artigo do cadastro
<select
required
className={controlClass}
value={row.stock_item_id}
onChange={(e) =>
line(index, {
stock_item_id:
e.target.value,
})
}
>
<option value="">
Seleccione
</option>
{props.items.map((item) => (
<option
key={item.id}
value={item.id}
>
{item.code} ·{' '}
{item.name} ({item.unit}
)
</option>
))}
</select>
</label>
<label>
Quantidade
<input
required
className={controlClass}
inputMode="decimal"
placeholder="Ex.: 2.500"
value={row.quantity}
onChange={(e) =>
line(index, {
quantity:
e.target.value,
})
}
/>
</label>
<label>
Utilização
<select
className={controlClass}
value={row.use}
onChange={(e) =>
line(index, {
use: e.target.value,
equipment_id: '',
})
}
>
<option value="internal">
Uso interno no local
indicado
</option>
<option value="equipment">
Em equipamento
</option>
</select>
</label>
{row.use === 'equipment' && (
<label>
Equipamento de destino
<select
required
className={controlClass}
value={row.equipment_id}
onChange={(e) =>
line(index, {
equipment_id:
e.target.value,
})
}
>
<option value="">
Seleccione do seu
departamento
</option>
{props.equipment.map(
(eq) => (
<option
key={eq.id}
value={eq.id}
>
{eq.code} ·{' '}
{eq.description}
</option>
),
)}
</select>
</label>
)}
{form.data.lines.length > 1 && (
<Button
type="button"
variant="outline"
onClick={() =>
form.setData(
'lines',
form.data.lines.filter(
(_, i) =>
i !== index,
),
)
}
>
Retirar linha
</Button>
)}
</fieldset>
))}
<div className="flex gap-3">
<Button
type="button"
variant="outline"
disabled={form.data.lines.length >= 50}
onClick={() =>
form.setData('lines', [
...form.data.lines,
blank(),
])
}
>
Acrescentar artigo
</Button>
<Button
disabled={
form.processing ||
props.costCenters.length === 0
}
>
Guardar rascunho
</Button>
</div>
{props.costCenters.length === 0 && (
<p>
É necessário um centro de custo
associado ao seu departamento.
</p>
)}
</form>
</Panel>
)
)}
<Panel title="Requisições registadas">
{!props.records?.data.length ? (
<p>Sem registos no seu âmbito.</p>
) : (
props.records.data.map((record) => (
<details
key={record.id}
className="mb-3 rounded border p-4"
>
<summary className="cursor-pointer font-medium">
RA-{record.id} ·{' '}
{
(
{
draft: 'Rascunho',
submitted: 'Submetida',
authorized: 'Autorizada',
partial:
'Parcialmente entregue',
delivered: 'Entregue',
closed: 'Encerrada',
rejected: 'Rejeitada',
} as Record<string, string>
)[record.progress?.state ?? 'draft']
}{' '}
· {record.snapshot.requester.name} ·{' '}
{record.snapshot.warehouse.name}
</summary>
<div className="mt-3 space-y-2 text-sm">
<Link
className="underline"
href={`/stock/requisicoes/${record.id}`}
>
Abrir requisição, autorizar ou registar
entrega
</Link>
<p>
Destino: {record.snapshot.destination}
</p>
<p>
Centro:{' '}
{record.snapshot.cost_center.code} ·{' '}
{record.snapshot.cost_center.name}
</p>
<p>Finalidade: {record.snapshot.reason}</p>
{record.snapshot.lines.map(
(item, index) => (
<p key={index}>
{item.item.code} ·{' '}
{item.item.name} ·{' '}
{item.quantity_milli / 1000}{' '}
{item.item.unit} ·{' '}
{item.equipment
? `Equipamento ${item.equipment.code} · ${item.equipment.description}`
: item.destination}
</p>
),
)}
</div>
</details>
))
)}
<div className="mt-4 flex gap-4">
{props.records?.prev_page_url && (
<Link href={props.records.prev_page_url}>
Anterior
</Link>
)}
{props.records?.next_page_url && (
<Link href={props.records.next_page_url}>
Seguinte
</Link>
)}
</div>
</Panel>
</div>
</>
);
}