Files
SGF/resources/js/IndustrialBudgetsWorkspace.jsx
T

17 lines
6.8 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>;
}