commit13
This commit is contained in:
@@ -298,7 +298,12 @@ export function procurementNavigation(
|
||||
id: 'warehouse_replenishment',
|
||||
label: 'Reposição automática',
|
||||
path: '/stock/reposicao',
|
||||
roles: ['admin', 'supply_read', 'supply_prepare', 'supply_review'],
|
||||
roles: [
|
||||
'admin',
|
||||
'supply_read',
|
||||
'supply_prepare',
|
||||
'supply_review',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'warehouse_requests',
|
||||
|
||||
@@ -169,8 +169,17 @@ export default function Availability(props: Props) {
|
||||
)
|
||||
: '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">
|
||||
{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'
|
||||
@@ -192,7 +201,13 @@ export default function Availability(props: Props) {
|
||||
<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>.
|
||||
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 && (
|
||||
|
||||
@@ -1,32 +1,435 @@
|
||||
import { Head, Link, useForm, router } from '@inertiajs/react';
|
||||
import { PageTitle, Panel, FormErrors, controlClass } from '@/components/procurement';
|
||||
import {
|
||||
PageTitle,
|
||||
Panel,
|
||||
FormErrors,
|
||||
controlClass,
|
||||
} from '@/components/procurement';
|
||||
import { Button } from '@/components/ui/button';
|
||||
type RecordRow = { id:number; record_key:string; version:number; state:string; reason:string; created_at:string; data:{ policy_key?:string; requester_id?:number; cost_center_id?:number; unit_price?:string; tax_rate?:string; needed_days?:number; valid_until?:string; message?:string; purchases?:{id:number;number:string;quantity_milli:number}[] }; snapshot:{item:{id:number;code?:string;name?:string};warehouse:{id:number;name?:string};requester?:{name:string};cost_center?:{code:string;name:string};origins?:{warehouse_request_id:number;destination:string;equipment:{code:string;description:string}|null;remaining_milli:number}[]} };
|
||||
type Props={ready:boolean;canPrepare:boolean;canReview:boolean;rules:RecordRow[];needs:RecordRow[];history:RecordRow[];policies:RecordRow[];requesters:{id:number;name:string;department_id:number}[];centers:{id:number;code:string;name:string;department_id:number}[]};
|
||||
const states:Record<string,string>={draft:'Por activar',approved:'Activa',suspended:'Suspensa',pending:'Necessidade pendente',created:'Rascunho criado',covered:'Reposição em curso',satisfied:'Disponibilidade suficiente'};
|
||||
function RuleForm({props,rule}:{props:Props;rule?:RecordRow}) {
|
||||
const form=useForm({operation_key:crypto.randomUUID(),record_key:rule?.record_key??null,version:rule?.version??0,action:'save',reason:'',data:{policy_key:rule?.data.policy_key??'',requester_id:rule?.data.requester_id?.toString()??'',cost_center_id:rule?.data.cost_center_id?.toString()??'',unit_price:rule?.data.unit_price??'',tax_rate:rule?.data.tax_rate??'',needed_days:rule?.data.needed_days?.toString()??'',valid_until:rule?.data.valid_until??''}});
|
||||
const requester=props.requesters.find(u=>u.id.toString()===form.data.data.requester_id);
|
||||
const set=(key:keyof typeof form.data.data,value:string)=>form.setData('data',{...form.data.data,[key]:value,...(key==='requester_id'?{cost_center_id:''}:{})});
|
||||
return <form className="grid gap-3 md:grid-cols-2" onSubmit={e=>{e.preventDefault();form.post('/stock/reposicao',{preserveState:'errors'});}}><FormErrors errors={form.errors}/>
|
||||
<label>Política aprovada · artigo/armazém<select required className={controlClass} value={form.data.data.policy_key} onChange={e=>set('policy_key',e.target.value)}><option value="">Seleccione</option>{props.policies.map(p=><option key={p.record_key} value={p.record_key}>{p.snapshot.item.code} · {p.snapshot.item.name} · {p.snapshot.warehouse.name}</option>)}</select></label>
|
||||
<label>Requisitante responsável<select required className={controlClass} value={form.data.data.requester_id} onChange={e=>set('requester_id',e.target.value)}><option value="">Seleccione</option>{props.requesters.map(u=><option key={u.id} value={u.id}>{u.name}</option>)}</select></label>
|
||||
<label>Centro de custo<select required className={controlClass} value={form.data.data.cost_center_id} onChange={e=>set('cost_center_id',e.target.value)}><option value="">Seleccione</option>{props.centers.filter(c=>c.department_id===requester?.department_id).map(c=><option key={c.id} value={c.id}>{c.code} · {c.name}</option>)}</select></label>
|
||||
<label>Preço estimado (MZN), se não existir no acordo<input inputMode="decimal" placeholder="Vazio: reutilizar acordo em MZN" className={controlClass} value={form.data.data.unit_price} onChange={e=>set('unit_price',e.target.value)}/></label>
|
||||
<label>Imposto estimado (%), se diferente do parâmetro da empresa<input inputMode="decimal" placeholder="Vazio: reutilizar parâmetro da empresa" className={controlClass} value={form.data.data.tax_rate} onChange={e=>set('tax_rate',e.target.value)}/></label>
|
||||
<label>Prazo pretendido após geração (dias)<input required type="number" min="0" max="366" className={controlClass} value={form.data.data.needed_days} onChange={e=>set('needed_days',e.target.value)}/></label>
|
||||
<label>Validade desta parametrização e estimativa<input required type="date" className={controlClass} value={form.data.data.valid_until} onChange={e=>set('valid_until',e.target.value)}/></label>
|
||||
<label>Fundamento<textarea required minLength={10} className={controlClass} value={form.data.reason} onChange={e=>form.setData('reason',e.target.value)}/></label>
|
||||
<p className="text-sm md:col-span-2">Mínimo, alvo, máximo e embalagem são reutilizados da política aprovada. A activação exige outro responsável. Alterar uma regra exige nova activação e afecta apenas novas gerações; documentos existentes conservam a sua origem.</p><Button disabled={form.processing}>Guardar para activação</Button></form>;
|
||||
type RecordRow = {
|
||||
id: number;
|
||||
record_key: string;
|
||||
version: number;
|
||||
state: string;
|
||||
reason: string;
|
||||
created_at: string;
|
||||
data: {
|
||||
policy_key?: string;
|
||||
requester_id?: number;
|
||||
cost_center_id?: number;
|
||||
unit_price?: string;
|
||||
tax_rate?: string;
|
||||
needed_days?: number;
|
||||
valid_until?: string;
|
||||
message?: string;
|
||||
purchases?: { id: number; number: string; quantity_milli: number }[];
|
||||
};
|
||||
snapshot: {
|
||||
item: { id: number; code?: string; name?: string };
|
||||
warehouse: { id: number; name?: string };
|
||||
requester?: { name: string };
|
||||
cost_center?: { code: string; name: string };
|
||||
origins?: {
|
||||
warehouse_request_id: number;
|
||||
destination: string;
|
||||
equipment: { code: string; description: string } | null;
|
||||
remaining_milli: number;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
type Props = {
|
||||
ready: boolean;
|
||||
canPrepare: boolean;
|
||||
canReview: boolean;
|
||||
rules: RecordRow[];
|
||||
needs: RecordRow[];
|
||||
history: RecordRow[];
|
||||
policies: RecordRow[];
|
||||
requesters: { id: number; name: string; department_id: number }[];
|
||||
centers: {
|
||||
id: number;
|
||||
code: string;
|
||||
name: string;
|
||||
department_id: number;
|
||||
}[];
|
||||
};
|
||||
const states: Record<string, string> = {
|
||||
draft: 'Por activar',
|
||||
approved: 'Activa',
|
||||
suspended: 'Suspensa',
|
||||
pending: 'Necessidade pendente',
|
||||
created: 'Rascunho criado',
|
||||
covered: 'Reposição em curso',
|
||||
satisfied: 'Disponibilidade suficiente',
|
||||
};
|
||||
function RuleForm({ props, rule }: { props: Props; rule?: RecordRow }) {
|
||||
const form = useForm({
|
||||
operation_key: crypto.randomUUID(),
|
||||
record_key: rule?.record_key ?? null,
|
||||
version: rule?.version ?? 0,
|
||||
action: 'save',
|
||||
reason: '',
|
||||
data: {
|
||||
policy_key: rule?.data.policy_key ?? '',
|
||||
requester_id: rule?.data.requester_id?.toString() ?? '',
|
||||
cost_center_id: rule?.data.cost_center_id?.toString() ?? '',
|
||||
unit_price: rule?.data.unit_price ?? '',
|
||||
tax_rate: rule?.data.tax_rate ?? '',
|
||||
needed_days: rule?.data.needed_days?.toString() ?? '',
|
||||
valid_until: rule?.data.valid_until ?? '',
|
||||
},
|
||||
});
|
||||
const requester = props.requesters.find(
|
||||
(u) => u.id.toString() === form.data.data.requester_id,
|
||||
);
|
||||
const set = (key: keyof typeof form.data.data, value: string) =>
|
||||
form.setData('data', {
|
||||
...form.data.data,
|
||||
[key]: value,
|
||||
...(key === 'requester_id' ? { cost_center_id: '' } : {}),
|
||||
});
|
||||
return (
|
||||
<form
|
||||
className="grid gap-3 md:grid-cols-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.post('/stock/reposicao', { preserveState: 'errors' });
|
||||
}}
|
||||
>
|
||||
<FormErrors errors={form.errors} />
|
||||
<label>
|
||||
Política aprovada · artigo/armazém
|
||||
<select
|
||||
required
|
||||
className={controlClass}
|
||||
value={form.data.data.policy_key}
|
||||
onChange={(e) => set('policy_key', e.target.value)}
|
||||
>
|
||||
<option value="">Seleccione</option>
|
||||
{props.policies.map((p) => (
|
||||
<option key={p.record_key} value={p.record_key}>
|
||||
{p.snapshot.item.code} · {p.snapshot.item.name} ·{' '}
|
||||
{p.snapshot.warehouse.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Requisitante responsável
|
||||
<select
|
||||
required
|
||||
className={controlClass}
|
||||
value={form.data.data.requester_id}
|
||||
onChange={(e) => set('requester_id', e.target.value)}
|
||||
>
|
||||
<option value="">Seleccione</option>
|
||||
{props.requesters.map((u) => (
|
||||
<option key={u.id} value={u.id}>
|
||||
{u.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Centro de custo
|
||||
<select
|
||||
required
|
||||
className={controlClass}
|
||||
value={form.data.data.cost_center_id}
|
||||
onChange={(e) => set('cost_center_id', e.target.value)}
|
||||
>
|
||||
<option value="">Seleccione</option>
|
||||
{props.centers
|
||||
.filter(
|
||||
(c) => c.department_id === requester?.department_id,
|
||||
)
|
||||
.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.code} · {c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Preço estimado (MZN), se não existir no acordo
|
||||
<input
|
||||
inputMode="decimal"
|
||||
placeholder="Vazio: reutilizar acordo em MZN"
|
||||
className={controlClass}
|
||||
value={form.data.data.unit_price}
|
||||
onChange={(e) => set('unit_price', e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Imposto estimado (%), se diferente do parâmetro da empresa
|
||||
<input
|
||||
inputMode="decimal"
|
||||
placeholder="Vazio: reutilizar parâmetro da empresa"
|
||||
className={controlClass}
|
||||
value={form.data.data.tax_rate}
|
||||
onChange={(e) => set('tax_rate', e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Prazo pretendido após geração (dias)
|
||||
<input
|
||||
required
|
||||
type="number"
|
||||
min="0"
|
||||
max="366"
|
||||
className={controlClass}
|
||||
value={form.data.data.needed_days}
|
||||
onChange={(e) => set('needed_days', e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Validade desta parametrização e estimativa
|
||||
<input
|
||||
required
|
||||
type="date"
|
||||
className={controlClass}
|
||||
value={form.data.data.valid_until}
|
||||
onChange={(e) => set('valid_until', e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Fundamento
|
||||
<textarea
|
||||
required
|
||||
minLength={10}
|
||||
className={controlClass}
|
||||
value={form.data.reason}
|
||||
onChange={(e) => form.setData('reason', e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<p className="text-sm md:col-span-2">
|
||||
Mínimo, alvo, máximo e embalagem são reutilizados da política
|
||||
aprovada. A activação exige outro responsável. Alterar uma regra
|
||||
exige nova activação e afecta apenas novas gerações; documentos
|
||||
existentes conservam a sua origem.
|
||||
</p>
|
||||
<Button disabled={form.processing}>Guardar para activação</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
function Decision({rule,action}:{rule:RecordRow;action:'approve'|'suspend'}) {
|
||||
const form=useForm({operation_key:crypto.randomUUID(),record_key:rule.record_key,version:rule.version,action,reason:'',data:{}});
|
||||
return <form className="mt-3 flex flex-wrap gap-3" onSubmit={e=>{e.preventDefault();form.post('/stock/reposicao',{preserveState:'errors'});}}><FormErrors errors={form.errors}/><input aria-label="Fundamento da decisão" required minLength={10} placeholder="Fundamento da decisão" className={controlClass} value={form.data.reason} onChange={e=>form.setData('reason',e.target.value)}/><Button disabled={form.processing}>{action==='approve'?'Activar parametrização':'Suspender parametrização'}</Button></form>;
|
||||
function Decision({
|
||||
rule,
|
||||
action,
|
||||
}: {
|
||||
rule: RecordRow;
|
||||
action: 'approve' | 'suspend';
|
||||
}) {
|
||||
const form = useForm({
|
||||
operation_key: crypto.randomUUID(),
|
||||
record_key: rule.record_key,
|
||||
version: rule.version,
|
||||
action,
|
||||
reason: '',
|
||||
data: {},
|
||||
});
|
||||
return (
|
||||
<form
|
||||
className="mt-3 flex flex-wrap gap-3"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.post('/stock/reposicao', { preserveState: 'errors' });
|
||||
}}
|
||||
>
|
||||
<FormErrors errors={form.errors} />
|
||||
<input
|
||||
aria-label="Fundamento da decisão"
|
||||
required
|
||||
minLength={10}
|
||||
placeholder="Fundamento da decisão"
|
||||
className={controlClass}
|
||||
value={form.data.reason}
|
||||
onChange={(e) => form.setData('reason', e.target.value)}
|
||||
/>
|
||||
<Button disabled={form.processing}>
|
||||
{action === 'approve'
|
||||
? 'Activar parametrização'
|
||||
: 'Suspender parametrização'}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
export default function Replenishment(props: Props) {
|
||||
return (
|
||||
<>
|
||||
<Head title="Reposição automática" />
|
||||
<div className="space-y-6 p-6">
|
||||
<PageTitle
|
||||
eyebrow="Armazém e compras"
|
||||
title="Reposição automática"
|
||||
description="Reutilizar a política aprovada e preparar a compra quando houver falta ou atingir o mínimo. As aprovações de compra mantêm-se."
|
||||
/>
|
||||
<div className="flex gap-4">
|
||||
<Link href="/stock/disponibilidade">Disponibilidade</Link>
|
||||
<Link href="/stock/requisicoes">
|
||||
Requisições ao armazém
|
||||
</Link>
|
||||
<Link href="/compras/fornecimento?view=calendar">
|
||||
Políticas de reposição
|
||||
</Link>
|
||||
</div>
|
||||
{!props.ready ? (
|
||||
<Panel title="Instalação pendente">
|
||||
<p>
|
||||
Instale a reserva e reposição para activar estas
|
||||
regras.
|
||||
</p>
|
||||
</Panel>
|
||||
) : (
|
||||
<>
|
||||
{props.canPrepare && (
|
||||
<Panel title="Parametrizar artigo e armazém">
|
||||
<RuleForm props={props} />
|
||||
</Panel>
|
||||
)}
|
||||
<Panel title="Parametrizações">
|
||||
{props.rules.length === 0 ? (
|
||||
<p>
|
||||
Sem parametrizações. Não são assumidos
|
||||
requisitantes ou centros de custo.
|
||||
</p>
|
||||
) : (
|
||||
props.rules.map((r) => (
|
||||
<div
|
||||
className="mb-4 rounded border p-4"
|
||||
key={r.id}
|
||||
>
|
||||
<p className="font-medium">
|
||||
{r.snapshot.item.code} ·{' '}
|
||||
{r.snapshot.item.name} ·{' '}
|
||||
{r.snapshot.warehouse.name}
|
||||
</p>
|
||||
<p>
|
||||
{states[r.state]} · Versão{' '}
|
||||
{r.version} ·{' '}
|
||||
{r.snapshot.requester?.name} ·{' '}
|
||||
{r.snapshot.cost_center?.code}
|
||||
</p>
|
||||
<p>
|
||||
Preço estimado {r.data.unit_price}{' '}
|
||||
MZN · Imposto {r.data.tax_rate}% ·
|
||||
Validade {r.data.valid_until}
|
||||
</p>
|
||||
{props.canReview &&
|
||||
r.state === 'draft' && (
|
||||
<Decision
|
||||
key={`approve:${r.id}`}
|
||||
rule={r}
|
||||
action="approve"
|
||||
/>
|
||||
)}
|
||||
{props.canReview &&
|
||||
r.state === 'approved' && (
|
||||
<Decision
|
||||
key={`suspend:${r.id}`}
|
||||
rule={r}
|
||||
action="suspend"
|
||||
/>
|
||||
)}
|
||||
{props.canPrepare && (
|
||||
<details className="mt-3">
|
||||
<summary>
|
||||
Rever parametrização
|
||||
</summary>
|
||||
<RuleForm
|
||||
key={r.id}
|
||||
props={props}
|
||||
rule={r}
|
||||
/>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</Panel>
|
||||
<Panel title="Necessidades e compras associadas">
|
||||
{props.needs.length === 0 ? (
|
||||
<p>
|
||||
Sem necessidades registadas após alterações
|
||||
de stock ou activação de regras.
|
||||
</p>
|
||||
) : (
|
||||
props.needs.map((n) => (
|
||||
<div
|
||||
key={n.id}
|
||||
className="mb-4 rounded border p-4"
|
||||
>
|
||||
<p className="font-medium">
|
||||
Artigo #{n.snapshot.item.id} ·
|
||||
Armazém #{n.snapshot.warehouse.id} ·{' '}
|
||||
{states[n.state]}
|
||||
</p>
|
||||
<p>{n.data.message}</p>
|
||||
{n.snapshot.origins?.map((o, i) => (
|
||||
<p key={i}>
|
||||
<Link
|
||||
href={`/stock/requisicoes/${o.warehouse_request_id}`}
|
||||
>
|
||||
RA-{o.warehouse_request_id}
|
||||
</Link>{' '}
|
||||
· {o.destination}
|
||||
{o.equipment
|
||||
? ` · ${o.equipment.code} · ${o.equipment.description}`
|
||||
: ''}{' '}
|
||||
· Pendente{' '}
|
||||
{o.remaining_milli / 1000}
|
||||
</p>
|
||||
))}
|
||||
{n.data.purchases?.map((p) => (
|
||||
<p key={p.id}>
|
||||
<Link
|
||||
href={`/requisicoes/${p.id}`}
|
||||
>
|
||||
{p.number}
|
||||
</Link>{' '}
|
||||
· Quantidade original{' '}
|
||||
{p.quantity_milli / 1000}
|
||||
</p>
|
||||
))}
|
||||
{(props.canPrepare ||
|
||||
props.canReview) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mt-3"
|
||||
onClick={() =>
|
||||
router.post(
|
||||
`/stock/reposicao/${n.id}/avaliar`,
|
||||
{},
|
||||
{
|
||||
preserveState:
|
||||
'errors',
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
Reavaliar necessidade
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</Panel>
|
||||
<Panel title="Histórico">
|
||||
<details>
|
||||
<summary>
|
||||
Consultar versões e fundamentos
|
||||
</summary>
|
||||
{props.history.map((h) => (
|
||||
<p key={h.id} className="mt-2">
|
||||
#{h.id} · {h.created_at} · Versão{' '}
|
||||
{h.version} · {states[h.state]} ·{' '}
|
||||
{h.reason}
|
||||
</p>
|
||||
))}
|
||||
</details>
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default function Replenishment(props:Props){return <><Head title="Reposição automática"/><div className="space-y-6 p-6"><PageTitle eyebrow="Armazém e compras" title="Reposição automática" description="Reutilizar a política aprovada e preparar a compra quando houver falta ou atingir o mínimo. As aprovações de compra mantêm-se."/><div className="flex gap-4"><Link href="/stock/disponibilidade">Disponibilidade</Link><Link href="/stock/requisicoes">Requisições ao armazém</Link><Link href="/compras/fornecimento?view=calendar">Políticas de reposição</Link></div>
|
||||
{!props.ready?<Panel title="Instalação pendente"><p>Instale a reserva e reposição para activar estas regras.</p></Panel>:<>
|
||||
{props.canPrepare&&<Panel title="Parametrizar artigo e armazém"><RuleForm props={props}/></Panel>}
|
||||
<Panel title="Parametrizações">{props.rules.length===0?<p>Sem parametrizações. Não são assumidos requisitantes ou centros de custo.</p>:props.rules.map(r=><div className="mb-4 rounded border p-4" key={r.id}><p className="font-medium">{r.snapshot.item.code} · {r.snapshot.item.name} · {r.snapshot.warehouse.name}</p><p>{states[r.state]} · Versão {r.version} · {r.snapshot.requester?.name} · {r.snapshot.cost_center?.code}</p><p>Preço estimado {r.data.unit_price} MZN · Imposto {r.data.tax_rate}% · Validade {r.data.valid_until}</p>{props.canReview&&r.state==='draft'&&<Decision key={`approve:${r.id}`} rule={r} action="approve"/>}{props.canReview&&r.state==='approved'&&<Decision key={`suspend:${r.id}`} rule={r} action="suspend"/>}{props.canPrepare&&<details className="mt-3"><summary>Rever parametrização</summary><RuleForm key={r.id} props={props} rule={r}/></details>}</div>)}</Panel>
|
||||
<Panel title="Necessidades e compras associadas">{props.needs.length===0?<p>Sem necessidades registadas após alterações de stock ou activação de regras.</p>:props.needs.map(n=><div key={n.id} className="mb-4 rounded border p-4"><p className="font-medium">Artigo #{n.snapshot.item.id} · Armazém #{n.snapshot.warehouse.id} · {states[n.state]}</p><p>{n.data.message}</p>{n.snapshot.origins?.map((o,i)=><p key={i}><Link href={`/stock/requisicoes/${o.warehouse_request_id}`}>RA-{o.warehouse_request_id}</Link> · {o.destination}{o.equipment?` · ${o.equipment.code} · ${o.equipment.description}`:''} · Pendente {o.remaining_milli/1000}</p>)}{n.data.purchases?.map(p=><p key={p.id}><Link href={`/requisicoes/${p.id}`}>{p.number}</Link> · Quantidade original {p.quantity_milli/1000}</p>)}{(props.canPrepare||props.canReview)&&<Button variant="outline" className="mt-3" onClick={()=>router.post(`/stock/reposicao/${n.id}/avaliar`,{}, {preserveState:'errors'})}>Reavaliar necessidade</Button>}</div>)}</Panel>
|
||||
<Panel title="Histórico"><details><summary>Consultar versões e fundamentos</summary>{props.history.map(h=><p key={h.id} className="mt-2">#{h.id} · {h.created_at} · Versão {h.version} · {states[h.state]} · {h.reason}</p>)}</details></Panel></>}
|
||||
</div></>}
|
||||
|
||||
@@ -278,8 +278,14 @@ export default function WarehouseRequest(props: Props) {
|
||||
<td className="border-b p-3">
|
||||
{p.delivered[index] / 1000}
|
||||
</td>
|
||||
<td className="border-b p-3">{p.reservation_ready ? p.reserved[index] / 1000 : 'Por instalar'}</td>
|
||||
<td className="border-b p-3">{p.cancelled[index] / 1000}</td>
|
||||
<td className="border-b p-3">
|
||||
{p.reservation_ready
|
||||
? p.reserved[index] / 1000
|
||||
: 'Por instalar'}
|
||||
</td>
|
||||
<td className="border-b p-3">
|
||||
{p.cancelled[index] / 1000}
|
||||
</td>
|
||||
<td className="border-b p-3">
|
||||
{p.remaining[index] / 1000}
|
||||
</td>
|
||||
@@ -289,7 +295,9 @@ export default function WarehouseRequest(props: Props) {
|
||||
</table>
|
||||
</div>
|
||||
<p className="mt-4 text-sm">
|
||||
{p.reservation_ready ? 'A autorização reserva o material utilizável disponível. Só a entrega confirmada reduz o stock. Retenções de qualidade e validade são revalidadas na entrega.' : 'Reserva de stock por instalar. A disponibilidade é revalidada na entrega.'}
|
||||
{p.reservation_ready
|
||||
? 'A autorização reserva o material utilizável disponível. Só a entrega confirmada reduz o stock. Retenções de qualidade e validade são revalidadas na entrega.'
|
||||
: 'Reserva de stock por instalar. A disponibilidade é revalidada na entrega.'}
|
||||
</p>
|
||||
</Panel>
|
||||
{!props.ready ? (
|
||||
@@ -326,10 +334,20 @@ export default function WarehouseRequest(props: Props) {
|
||||
action="deliver"
|
||||
/>
|
||||
)}
|
||||
{props.canManageReservation && p.reservation_ready && <>
|
||||
<Command key={`reserve:${p.version}`} props={props} action="reserve" />
|
||||
<Command key={`cancel:${p.version}`} props={props} action="cancel" />
|
||||
</>}
|
||||
{props.canManageReservation && p.reservation_ready && (
|
||||
<>
|
||||
<Command
|
||||
key={`reserve:${p.version}`}
|
||||
props={props}
|
||||
action="reserve"
|
||||
/>
|
||||
<Command
|
||||
key={`cancel:${p.version}`}
|
||||
props={props}
|
||||
action="cancel"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Panel title="Decisões, entregas e recepção">
|
||||
|
||||
Reference in New Issue
Block a user