35 lines
9.0 KiB
React
35 lines
9.0 KiB
React
import React,{useEffect,useRef,useState} from 'react';
|
|
import {milliQuantity} from './StockReadWorkspace.jsx';
|
|
|
|
export function WorkshopStockImpact({impact:p}) {
|
|
return <p>Físico: {milliQuantity(p.physical_before)} → {milliQuantity(p.physical_after)}. Nova saída: {milliQuantity(p.quantity_milli)}. {['link_existing_delivery','allocate_existing_delivery'].includes(p.mode)&&<>Ligar entrega #{p.existing_movement_id}: {milliQuantity(p.linked_quantity_milli)}, sem novo abatimento. </>}{p.mode==='return_existing_delivery'&&<>Entrada por devolução: {milliQuantity(p.return_quantity_milli)}, da entrega #{p.existing_movement_id}. </>}{['allocate_existing_delivery','return_existing_delivery'].includes(p.mode)&&<>Quantidade não associada: {milliQuantity(p.unallocated_before)} → {milliQuantity(p.unallocated_after)}. </>}Artigo #{p.stock_item_id}, armazém #{p.warehouse_id}, origem versão {p.source_version}.</p>;
|
|
}
|
|
export function WorkshopStockClosureImpact({action}) {
|
|
return <p>Encerrar esta proposta como {action==='withdraw'?'retirada':'recusada'}. Efeito físico: 0; nenhuma origem será associada ou libertada. O original e esta decisão ficam no histórico; voltar a tratar a origem exige nova conferência.</p>;
|
|
}
|
|
|
|
export default function WorkshopStockWorkspace({api,context,can}) {
|
|
const [data,setData]=useState(null),[error,setError]=useState(''),[reload,setReload]=useState(0),[page,setPage]=useState(1),[busy,setBusy]=useState(false),[preview,setPreview]=useState(null),[proposalPage,setProposalPage]=useState(1),[status,setStatus]=useState("all");
|
|
const request=useRef(null),generation=useRef(0),unit=context.unit.id;
|
|
useEffect(()=>{setPreview(null);setBusy(false);request.current=null;},[context.token]);
|
|
useEffect(()=>{const n=++generation.current;setData(null);setError('');api(`/api/workshop-stock?page=${page}&proposal_page=${proposalPage}&status=${status}`).then(d=>{if(n===generation.current)setData(d);}).catch(e=>{if(n===generation.current)setError(e.message);});return()=>{generation.current++;};},[context.token,reload,page,proposalPage,status]);
|
|
async function prepare(e,source){e.preventDefault();const n=generation.current;setBusy(true);setError('');const body={...Object.fromEntries(new FormData(e.currentTarget)),entry_id:source.id,version:source.version};body.warehouse_id=Number(body.warehouse_id);
|
|
const text=JSON.stringify(body);if(request.current?.text!==text)request.current={text,key:crypto.randomUUID()};
|
|
const allocation=body.mode==='allocate_existing_delivery',returning=body.mode==='return_existing_delivery',delivery=returning||allocation||body.mode==='link_existing_delivery';delete body.mode;if(delivery)body.movement_id=Number(body.movement_id);else delete body.movement_id;
|
|
try{const result=await api(returning?'/api/workshop-stock/prepare-return':allocation?'/api/workshop-stock/prepare-allocation':delivery?'/api/workshop-stock/prepare-delivery':'/api/workshop-stock/prepare','POST',body,undefined,request.current.key);if(n===generation.current){setPreview(result);setReload(v=>v+1);}}catch(e){if(n===generation.current)setError(e.message);}finally{if(n===generation.current)setBusy(false);}}
|
|
async function confirm(e,p){e.preventDefault();const n=generation.current;setBusy(true);setError('');const body=Object.fromEntries(new FormData(e.currentTarget));if('received_in_warehouse' in body)body.received_in_warehouse=body.received_in_warehouse==='on';try{await api(`/api/workshop-stock/${p.id}/confirm`,'POST',body);if(n===generation.current){setPreview(null);setReload(v=>v+1);}}catch(e){if(n===generation.current)setError(e.message);}finally{if(n===generation.current)setBusy(false);}}
|
|
async function close(e,p,action){e.preventDefault();const n=generation.current;setBusy(true);setError('');const body={...Object.fromEntries(new FormData(e.currentTarget)),state_version:p.state_version,source_hash:p.source_hash};try{await api(`/api/workshop-stock/${p.id}/${action}`,'POST',body);if(n===generation.current){setPreview(null);setReload(v=>v+1);}}catch(e){if(n===generation.current)setError(e.message);}finally{if(n===generation.current)setBusy(false);}}
|
|
const closureForm=(p,action)=><details><summary>{action==='withdraw'?'Retirar proposta própria':'Recusar proposta'}</summary><WorkshopStockClosureImpact action={action}/><form onSubmit={e=>close(e,p,action)}><label>Fundamento do encerramento<textarea name="reason" minLength={10} maxLength={1000} required/></label><button disabled={busy}>{action==='withdraw'?'Confirmar retirada sem movimento':'Confirmar recusa sem movimento'}</button></form></details>;
|
|
const impact=p=><WorkshopStockImpact impact={p.impact}/>;
|
|
return <section className="panel"><h2>Conferência de consumo de material</h2><p>Uma origem verificada não provoca saída automática. Entregas já abatidas e correcções exigem reconciliação própria.</p>
|
|
{error&&<p role="alert">{error}</p>}{!data&&!error&&<p role="status">A consultar origens autorizadas…</p>}
|
|
<button disabled={busy} onClick={()=>setReload(n=>n+1)}>Actualizar</button>
|
|
{preview&&<div role="status"><h3>Proposta #{preview.id}</h3>{impact(preview)}<p>{preview.reason} · Evidência: {preview.evidence}</p><p>{preview.state&&preview.state!=='pending'?'Proposta já encerrada; a prévia é histórica.':'A confirmação é individual, por outra identidade autorizada.'}</p></div>}
|
|
{data&&<><p>{data.notice}</p><h3>Origens verificadas</h3>{data.sources.map(source=>{const d=typeof source.data==='string'?JSON.parse(source.data):source.data;return <article className="panel" key={source.id}><h4>Ordem #{source.order_id} · Origem #{source.id} / v{source.version}</h4><p>{d.resource} · {source.quantity} {d.unit} · {d.event} · {source.origin}</p>
|
|
{source.movement_id?<p>Esta origem já tem o movimento #{source.movement_id}. Uma nova versão exige conferência/correcção física separada, sem novo abatimento automático.</p>:can('workshop_stock.prepare',unit)&&<form onSubmit={e=>prepare(e,source)}><label>Tratamento<select name="mode" required defaultValue=""><option value="" disabled>Seleccione individualmente</option>{d.event!=="returned"&&<><option value="new_issue">Registar saída nova</option><option value="link_existing_delivery">Ligar saída já registada, sem novo abatimento</option><option value="allocate_existing_delivery">Associar este consumo à parte restante de uma entrega</option></>}{d.event==="returned"&&<option value="return_existing_delivery">Confirmar entrada da devolução não consumida</option>}</select></label><label>Movimento da entrega existente (apenas para ligação)<input name="movement_id" type="number" min="1"/></label><label>Armazém autorizado<input name="warehouse_id" type="number" min="1" required/></label><label>Fundamento<textarea name="reason" minLength={10} maxLength={1000} required/></label><label>Evidência da entrega e do tratamento físico<input name="evidence" minLength={3} maxLength={500} required/></label><button disabled={busy}>Conferir impacto</button></form>}</article>;})}
|
|
<button disabled={busy||page<=1} onClick={()=>setPage(p=>p-1)}>Anterior</button><button disabled={busy||page>=data.pagination.last_page} onClick={()=>setPage(p=>p+1)}>Seguinte</button>
|
|
<h3>Histórico de propostas da unidade</h3><label>Estado<select value={status} onChange={e=>{setStatus(e.target.value);setProposalPage(1);}}><option value="all">Todas</option><option value="pending">Pendentes</option><option value="confirmed">Confirmadas</option>{data.closure_supported&&<><option value="withdrawn">Retiradas</option><option value="rejected">Recusadas</option></>}</select></label>{data.proposals.map(p=><article className="panel" key={p.id}><h4>Proposta #{p.id} · Origem #{p.entry_id} / v{p.entry_version}</h4>{impact(p)}<p>Preparador #{p.preparer_id} · {p.created_at}</p><p>{p.reason} · Evidência: {p.evidence}</p>
|
|
{p.closure?<p>Prévia arquivada, não executada. {p.state==='withdrawn'?'Retirada':'Recusada'} por #{p.closure.actor_id} em {p.closure.created_at}: {p.closure.reason}.</p>:p.movement_id?<p>Tratamento já registado: movimento #{p.movement_id}.</p>:<>{p.can_review&&can('workshop_stock.confirm',unit)?<><form onSubmit={e=>confirm(e,p)}>{p.impact.mode==="return_existing_delivery"&&<label><input type="checkbox" name="received_in_warehouse" required/>Confirmo a recepção física desta quantidade no armazém indicado</label>}<label>Fundamento da confirmação<textarea name="reason" minLength={10} maxLength={1000} required/></label><p>O servidor exige outra identidade e revalida origem, mapa, direitos e livro antes de gravar.</p><button disabled={busy}>Confirmar tratamento conferido</button></form>{data.closure_supported&&closureForm(p,'reject')}</>:<p>Aguarda confirmação independente.</p>}{p.can_withdraw&&can('workshop_stock.prepare',unit)&&closureForm(p,'withdraw')}</>}</article>)}<button disabled={busy||proposalPage<=1} onClick={()=>setProposalPage(p=>p-1)}>Propostas anteriores</button><button disabled={busy||proposalPage>=data.proposal_pagination.last_page} onClick={()=>setProposalPage(p=>p+1)}>Propostas seguintes</button></>}
|
|
</section>;
|
|
}
|