438 lines
22 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|