import React,{useEffect,useRef,useState} from 'react'; const names={material:'Consumo material',labour:'Trabalho efectivo',pool:'Origem do conjunto indirecto',machine:'Utilização de máquina',production:'Produção boa concluída',material_rate:'Taxa de material',labour_rate:'Taxa de trabalho',criterion:'Critério de imputação'}; const units={material:'KG',labour:'MIN',pool:'MZN',machine:'HM',production:'UN',material_rate:'KG',labour_rate:'H'}; const money=v=>v===null?'Indisponível':`${v} MZN`; export default function IndustrialCostsWorkspace({context,can,api,busy}){ const [data,setData]=useState(null),[error,setError]=useState(''),[lotId,setLotId]=useState(''),[kind,setKind]=useState('criterion'),[selection,setSelection]=useState({}),[working,setWorking]=useState(false);const retry=useRef(null); const allowed=r=>can(`industrial_costs.${r}`,context.unit?.id??null);const disabled=busy||working; const load=async()=>{const r=await api('/api/industrial-costs');setData(r);};useEffect(()=>{setSelection({});load().catch(e=>setError(e.message));},[context.token]); const lot=data?.lots.find(l=>String(l.id)===lotId)??data?.lots[0];const latest=lot?.results.at(-1);const lastInput=lot?.inputs.filter(i=>i.kind===kind).at(-1); const options=k=>lot?.inputs.filter(i=>i.kind===k&&i.current&&i.decision?.decision==='approved')??[]; const reference=v=>{const r=lot.inputs.find(i=>String(i.id)===String(v));return r?{id:r.id,version:r.version}:null;}; async function send(path,input){setWorking(true);setError('');const 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);}} const decide=(type,row,decision,reason)=>send(`/api/industrial-costs/lots/${lot.id}/decisions`,{type,id:row.id,version:row.version,decision,reason}); const inputForm=e=>{e.preventDefault();const f=new FormData(e.currentTarget);let d; if(kind==='criterion'){const p=reference(f.get('pool')),m=reference(f.get('machine'));if(!p||!m){setError('Escolher versões explícitas do conjunto e utilização.');return;}d={pool_id:p.id,pool_version:p.version,machine_id:m.id,machine_version:m.version,capacity:f.get('capacity'),driver_unit:'HM',hypothesis:'synthetic_only'};} else if(kind.endsWith('_rate')){const source=reference(f.get('source'));if(!source){setError('Escolher origem exacta.');return;}d={source_id:source.id,source_version:source.version,amount:f.get('amount'),currency:'MZN',unit:units[kind]};} else {d={quantity:f.get('quantity'),unit:units[kind],event_date:f.get('event_date')};if(kind==='production')d.completed=f.get('completed')==='on';} send(`/api/industrial-costs/lots/${lot.id}/inputs`,{kind,source_ref:lastInput?.source_ref??f.get('source_ref'),version:(lastInput?.version??0)+1,previous_id:lastInput?.id??null,data:d,evidence:f.get('evidence')});}; return

INDUSTRIAL-COST-SIM-v1

Ensaio de custos industriais

ENSAIO INDUSTRIAL — SEM UTILIZAÇÃO OPERACIONAL. Critério sintético; não configura método contabilístico real. Sem stock, facturação, margens ou lançamentos.
{error&&

{error}

} {!data?

A carregar…

:data.blocked?

Custeio industrial real bloqueado nesta instância.

:<> {allowed('prepare')&&
Abrir lote sintético e produto próprios
{e.preventDefault();const f=new FormData(e.currentTarget);send('/api/industrial-costs/lots',Object.fromEntries([...f.entries(),['currency','MZN']]));}}>{[['reference','Referência do lote'],['product_reference','Referência do produto'],['product_name','Nome do produto'],['centre','Centro sintético']].map(([n,l])=>)}

MZN explícito; produto em UN. Não é ordem de oficina.

} {!lot?

Sem lotes no âmbito autorizado.

:<>

{lot.product.name} · {lot.centre} · {lot.period_start} a {lot.period_end} · {lot.currency}

{allowed('prepare')&&

Preparar origem, taxa ou critério versionado

{lastInput?`Nova versão ${(lastInput.version)+1}, anterior #${lastInput.id}; origem ${lastInput.source_ref}.`:'Nova origem sintética identificada, sem duplicar outra origem.'}

{!lastInput&&} {kind==='criterion'?<>

Hipótese apenas do ensaio; denominador explícito. Não redistribuir residual.

:kind.endsWith('_rate')?<>:<>{kind==='production'&&}} }

Origens, taxas e critério

{lot.inputs.map(i=>
#{i.id} · {names[i.kind]} v{i.version}

{i.source_ref} · {i.current?'Última versão':'Histórico'} · {i.decision?`${i.decision.decision==='approved'?'Validado':'Recusado'} — ${i.decision.reason}`:'Aguarda validação'}

{Object.entries(i.data).map(([k,v])=>
{({quantity:'Quantidade',unit:'Unidade',event_date:'Data',amount:'Taxa',currency:'Moeda',capacity:'Capacidade',driver_unit:'Unidade da base',hypothesis:'Hipótese',source_id:'Origem #',source_version:'Versão da origem',pool_id:'Conjunto #',pool_version:'Versão do conjunto',machine_id:'Utilização #',machine_version:'Versão da utilização',completed:'Concluído'})[k]??k}
{v===true?'Sim':v==='synthetic_only'?'Apenas ensaio':String(v)}
)}

{i.evidence}

{allowed('validate')&&i.current&&!i.decision&&decide('input',i,d,r)}/>}
)}
{allowed('prepare')&&
{e.preventDefault();const refs=Object.fromEntries(Object.keys(names).map(k=>[k,reference(selection[k]??'')]));send(`/api/industrial-costs/lots/${lot.id}/calculations`,{currency:'MZN',version:(latest?.version??0)+1,previous_id:latest?.id??null,refs});}}>

Seleccionar versões para cálculo

Selecção explícita; taxa material ausente mantém pendência. As outras referências são necessárias.

{Object.entries(names).map(([k,n])=>)}
}

Resultados e residual

{lot.results.map(r=>

Resultado #{r.id} · v{r.version}

{r.current?'Dependências actuais':'DESACTUALIZADO / HISTÓRICO — não usar como actual'} · {r.decision?`${r.decision.decision==='approved'?'Validado':'Recusado'} — ${r.decision.reason}`:'Aguarda validação'}

{r.snapshot.hypothesis}

ComponenteComposição exactaCusto no lote
Material{r.snapshot.origins.material.data.quantity} KG × {r.snapshot.origins.material_rate?.data.amount??'taxa em falta'}{money(r.snapshot.material_exact)}
Trabalho{r.snapshot.origins.labour.data.quantity} MIN ÷ 60 = {r.snapshot.labour_hours_exact} H × {r.snapshot.origins.labour_rate.data.amount}{money(r.snapshot.labour_exact)}
Indirectos imputados{r.snapshot.pool_exact} ÷ {r.snapshot.capacity_hm} HM = {r.snapshot.indirect_rate_exact} MZN/HM; × {r.snapshot.used_hm} HM{money(r.snapshot.allocated_exact)}

Custo atribuído conhecido: {money(r.snapshot.known_cost_exact)}

{r.snapshot.incomplete?'TOTAL INCOMPLETO — material pendente':'Total do perímetro sintético: '+money(r.snapshot.selected_total_exact)}

{r.snapshot.good_units} UN boas · Custo unitário do cenário: {r.snapshot.unit_cost_exact===null?'Indisponível':money(r.snapshot.unit_cost_exact)+'/UN'}

Residual separado: {money(r.snapshot.residual_exact)} por imputar

Conjunto: {r.snapshot.pool_exact} = {r.snapshot.allocated_exact} imputados + {r.snapshot.residual_exact} por imputar.

Reconciliação {r.snapshot.reconciliation.coverage==='partial_known_only'?'PARCIAL — só valores conhecidos':'do perímetro'}: {r.snapshot.reconciliation.known_origins_exact} = {r.snapshot.reconciliation.assigned_exact} atribuídos + {r.snapshot.reconciliation.unassigned_exact} por imputar.

    {r.snapshot.pending.map(p=>
  • {p}
  • )}
Referências exactas utilizadas
    {Object.entries(r.snapshot.refs).map(([k,v])=>
  • {names[k]}: {v?`#${v.id} v${v.version}`:'Ausente'}
  • )}
{allowed('validate')&&r.current&&!r.decision&&decide('result',r,d,reason)}/>}
)}
}}
; } function SourceSelect({label,name,rows}){return ;} function DecisionForm({disabled,onDecision}){return
{e.preventDefault();const f=new FormData(e.currentTarget);onDecision(f.get('decision'),f.get('reason'));}}>
;}