17 lines
6.8 KiB
React
17 lines
6.8 KiB
React
import React,{useEffect,useRef,useState} from 'react';
|
||
const labels={material:'Material',labour:'Trabalho',allocated:'Indirectos imputados'};
|
||
const value=v=>v===null?'Indisponível':v;
|
||
export default function IndustrialBudgetsWorkspace({context,can,api,busy}){
|
||
const [data,setData]=useState(null),[lotId,setLotId]=useState(''),[error,setError]=useState(''),[working,setWorking]=useState(false);const retry=useRef(null);
|
||
const allowed=r=>can(r,context.unit?.id??null);const disabled=busy||working;
|
||
const load=async()=>setData(await api('/api/industrial-budgets'));
|
||
useEffect(()=>{setData(null);setLotId('');retry.current=null;load().catch(e=>setError(e.message));},[context.token]);
|
||
const lot=data?.lots.find(l=>String(l.id)===lotId)??data?.lots[0];
|
||
async function send(action,input){setWorking(true);setError('');const path=`/api/industrial-budgets/lots/${lot.id}/${action}`,signature=JSON.stringify([path,input]);if(retry.current?.signature!==signature)retry.current={signature,key:crypto.randomUUID()};try{await api(path,'POST',input,context.token,retry.current.key);retry.current=null;await load();}catch(e){setError(e.message);}finally{setWorking(false);}}
|
||
return <section><p className="eyebrow">INDUSTRIAL-BUDGET-ACTUAL-SIM-v1</p><h2>Orçamento versus realizado</h2><div className="alert">ENSAIO — SEM UTILIZAÇÃO OPERACIONAL. Hipótese retrospectiva sintética; diferenças monetárias não demonstram causas ou eficiência.</div>{error&&<p role="alert" className="alert error">{error}</p>}{!data?<p>A carregar…</p>:data.blocked?<p>Operações reais bloqueadas.</p>:<><button disabled={disabled} onClick={()=>load().catch(e=>setError(e.message))}>Actualizar versões explicitamente</button><label>Lote<select value={lot?.id??''} onChange={e=>setLotId(e.target.value)}>{data.lots.map(l=><option value={l.id} key={l.id}>{l.reference} · #{l.id}</option>)}</select></label>{lot?<><p>{lot.product.name} · {lot.period_start} a {lot.period_end} · 10 UN · MZN · Material, trabalho e indirectos imputados</p>
|
||
{allowed('industrial_budgets.prepare')&&<form className="panel" onSubmit={e=>{e.preventDefault();const f=new FormData(e.currentTarget),last=lot.budgets.at(-1);send('proposals',{version:(last?.version??0)+1,previous_id:last?.id??null,dimensions:lot.dimensions,amounts:Object.fromEntries(Object.keys(labels).map(k=>[k,f.get(k)===''?null:f.get(k)])),evidence:f.get('evidence')});}}><h3>Preparar orçamento versionado</h3><p>{lot.budgets.length?`Sucessora explícita de #${lot.budgets.at(-1).id} v${lot.budgets.at(-1).version}. Referência histórica preservada.`:'Primeira proposta. Não é planeamento anterior comprovado.'}</p>{Object.entries(labels).map(([k,l])=><label key={k}>{l} previsto (MZN; vazio = desconhecido)<input name={k} inputMode="decimal" placeholder="Montante exacto ou vazio"/></label>)}<label>Fundamento / motivo da versão<input name="evidence" required minLength={3}/></label><button disabled={disabled}>Submeter orçamento</button></form>}
|
||
<h3>Orçamentos e decisões</h3>{lot.budgets.map(b=><article className="panel" key={b.id}><h3>Orçamento #{b.id} · v{b.version}</h3><p>{b.current?'Última versão':'HISTÓRICO — substituído explicitamente'} · {b.decision?`${b.decision.decision==='approved'?'Validado':'Recusado'} — ${b.decision.reason}`:'Aguarda validação distinta'}</p><p>{b.evidence}</p><ul>{Object.entries(labels).map(([k,l])=><li key={k}>{l}: {value(b.amounts[k])} MZN</li>)}</ul>{allowed('industrial_budgets.validate')&&b.current&&!b.decision&&<form onSubmit={e=>{e.preventDefault();const f=new FormData(e.currentTarget);send('decisions',{id:b.id,version:b.version,decision:f.get('decision'),reason:f.get('reason')});}}><label>Decisão<select name="decision"><option value="approved">Validar versão exacta</option><option value="refused">Recusar versão exacta</option></select></label><label>Fundamento<input name="reason" required minLength={3}/></label><button disabled={disabled}>Registar decisão do orçamento</button></form>}</article>)}
|
||
{allowed('industrial_budget_comparisons.prepare')&&<form className="panel" onSubmit={e=>{e.preventDefault();const f=new FormData(e.currentTarget),b=lot.budgets.find(x=>String(x.id)===f.get('budget')),r=lot.results.find(x=>String(x.id)===f.get('result')),last=lot.comparisons.at(-1);send('comparisons',{version:(last?.version??0)+1,previous_id:last?.id??null,budget_id:b.id,budget_version:b.version,result_id:r.id,result_version:r.version});}}><h3>Comparar versões explícitas</h3><label>Orçamento aprovado<select name="budget" required defaultValue=""><option value="">Seleccionar ID e versão</option>{lot.budgets.filter(b=>b.current&&b.decision?.decision==='approved').map(b=><option key={b.id} value={b.id}>#{b.id} v{b.version}</option>)}</select></label><label>Realizado validado<select name="result" required defaultValue=""><option value="">Seleccionar ID e versão</option>{lot.results.filter(r=>r.current&&r.decision?.decision==='approved').map(r=><option key={r.id} value={r.id}>#{r.id} v{r.version} · {r.snapshot.incomplete?'800 conhecidos — incompleto':`${r.snapshot.known_cost_exact} MZN`}</option>)}</select></label><button disabled={disabled}>Comparar orçamento e realizado</button></form>}
|
||
<h3>Comparações preservadas</h3>{lot.comparisons.map(c=><article key={c.id} className="panel"><h3>Comparação #{c.id} · v{c.version}</h3><p>{c.current?'Versões actuais':'DESACTUALIZADA / HISTÓRICA — não substituída silenciosamente'}</p><p>Orçamento #{c.snapshot.budget.id} v{c.snapshot.budget.version} · Realizado #{c.snapshot.result.id} v{c.snapshot.result.version}</p><table><thead><tr><th>Componente</th><th>Previsto MZN</th><th>Realizado MZN</th><th>Desvio R − P</th><th>Percentagem</th></tr></thead><tbody>{Object.entries(labels).map(([k,l])=>{const r=c.snapshot.rows[k];return <tr key={k}><td>{l}</td><td>{value(r.planned)}</td><td>{value(r.actual)}</td><td>{value(r.delta)}</td><td>{r.percent===null?`Indisponível — ${r.percent_state==='zero_base'?'base zero':'valor desconhecido'}`:`${r.percent} %`}</td></tr>;})}</tbody></table><p><strong>{c.snapshot.incomplete?'COMPARAÇÃO INCOMPLETA — sem desvio total conclusivo':`Desvio total: ${c.snapshot.total_delta} MZN · ${c.snapshot.total_percent===null?'Percentagem indisponível — base zero':c.snapshot.total_percent+' %'}`}</strong></p><p>Realizado conhecido: {c.snapshot.actual_known} MZN</p><p>Reconciliação {c.snapshot.incomplete?'dos pares conhecidos':'total'}: {c.snapshot.paired_planned} + ({c.snapshot.paired_delta}) = {c.snapshot.paired_actual} MZN</p><p><strong>Residual separado: {c.snapshot.residual_separate} MZN por imputar — fora do desvio do lote</strong></p><p>Origens realizadas: {c.snapshot.actual_reconciliation.known_origins_exact} = {c.snapshot.actual_reconciliation.assigned_exact} atribuídos + {c.snapshot.actual_reconciliation.unassigned_exact} por imputar{c.snapshot.incomplete?' (cobertura parcial)':''}.</p></article>)}</>:<p>Sem lotes autorizados.</p>}</>}</section>;
|
||
}
|