15 lines
6.4 KiB
React
15 lines
6.4 KiB
React
import React,{useEffect,useRef,useState} from 'react';
|
|
import {workshopBillingProposal} from './workshopBillingProposal.js';
|
|
export default function WorkshopBillingWorkspace({context,can,api,busy}){
|
|
const [data,setData]=useState(null),[orderId,setOrderId]=useState(''),[error,setError]=useState(''),[working,setWorking]=useState(false),[notice,setNotice]=useState('');const retry=useRef(null);const feedback=useRef(null);
|
|
const allowed=r=>can(`workshop_billing.${r}`,context.unit?.id??null);const load=async()=>setData(await api('/api/workshop-billing'));
|
|
useEffect(()=>{setData(null);setOrderId('');retry.current=null;load().catch(e=>setError(e.message));},[context.token]);
|
|
const order=data?.orders.find(o=>String(o.id)===orderId)??data?.orders[0],disabled=busy||working;
|
|
useEffect(()=>{if(error||notice)feedback.current?.scrollIntoView({behavior:'auto',block:'center'});},[error,notice]);
|
|
async function send(action,input){setWorking(true);setError('');setNotice('');const path=`/api/workshop-billing/orders/${order.id}/${action}`,signature=JSON.stringify([path,input]);if(retry.current?.signature!==signature)retry.current={signature,key:crypto.randomUUID()};try{const result=await api(path,'POST',input,context.token,retry.current.key);retry.current=null;setNotice(action==='proposals'?`Proposta #${result.id} · v${result.version} registada. Aguarda revisão distinta.`:action==='reviews'?'Revisão comercial registada.':'Rascunho gerado.');try{await load();}catch{setError('O registo foi guardado, mas a lista não actualizou. Use Actualizar versões explicitamente; não volte a submeter.');}}catch(e){setError(e.message);}finally{setWorking(false);}}
|
|
return <section><p className="eyebrow">WORKSHOP-BILLING-DRAFT-SIM-v1</p><h1>Oficina → rascunho de facturação</h1><div className="alert home-notice">ENSAIO — SEM UTILIZAÇÃO OPERACIONAL. Preços comerciais explícitos; custo interno não determina preço de venda. Sem emissão ou confirmação automática.</div><div ref={feedback} aria-live="polite">{notice&&<p className="alert success">{notice}</p>}{error&&<p role="alert" className="alert error">{error}</p>}</div>{!data?<p>A carregar…</p>:data.blocked?<p>Conversão real bloqueada nesta instância.</p>:<><button onClick={()=>load().catch(e=>setError(e.message))} disabled={disabled}>Actualizar versões explicitamente</button><label>Ordem<select value={order?.id??''} onChange={e=>setOrderId(e.target.value)}>{data.orders.map(o=><option key={o.id} value={o.id}>{o.reference} · #{o.id}</option>)}</select></label>{order?<><p>Cliente #{order.client_id} · MZN explícito · Seleccionar origens integrais verificadas.</p>
|
|
{allowed('prepare')&&!order.generation&&<form className="panel" noValidate onSubmit={e=>{e.preventDefault();try{const input=workshopBillingProposal(order,data.services,new FormData(e.currentTarget));send("proposals",input);}catch(error){setError(error.message);}}}><h2>Seleccionar e preparar proposta comercial</h2>{order.entries.map(r=><fieldset key={r.id}><legend>Origem #{r.id} v{r.version} · {r.kind==='time'?`${r.minutes} MIN verificados`:`${r.quantity} ${r.data.unit} consumidos — ${r.data.resource}`}</legend><label><input type="checkbox" name={`select-${r.id}`} style={{display:'inline',width:'auto'}}/>Incluir origem integral</label><label>Serviço comercial<select name={`service-${r.id}`}><option value="">Seleccionar serviço e versão</option>{data.services.map(s=><option key={s.id} value={s.id}>{s.name} · #{s.id} v{s.version}</option>)}</select></label><label>Preço comercial MZN/{r.kind==='time'?'H':r.data.unit}<input name={`price-${r.id}`} inputMode="decimal"/></label><label>Fonte comercial e versão (preenchimento obrigatório)<input name={`source-${r.id}`} placeholder="Escreva a referência da tabela"/></label></fieldset>)}<label>Fundamento da proposta / exclusões<input name="evidence" required minLength={3}/></label><button disabled={disabled}>Submeter proposta comercial</button></form>}
|
|
{order.proposals.map(p=><article className="panel" key={p.id}><h2>Proposta #{p.id} · v{p.version}</h2><p>{p.current?'Versão actual':'HISTÓRICO / DESACTUALIZADA'} · {p.decision?`${p.decision.decision==='approved'?'Revista e aprovada':'Recusada'} — ${p.decision.reason}`:'Aguarda revisão distinta'}</p><p>{p.snapshot.evidence}</p><table><thead><tr><th>Origem exacta</th><th>Serviço / versão</th><th>Quantidade comercial</th><th>Preço MZN</th><th>Fonte</th></tr></thead><tbody>{p.snapshot.lines.map(l=><tr key={l.entry_id}><td>#{l.entry_id} v{l.entry_version}</td><td>#{l.service_id} v{l.service_version}</td><td>{l.quantity} {l.unit}</td><td>{l.unit_price}</td><td>{l.price_source}</td></tr>)}</tbody></table><p>Soma sem imposto: <strong>{p.snapshot.base_exact} MZN</strong> · Fiscalidade pendente.</p>{allowed('review')&&p.current&&!p.decision&&<form onSubmit={e=>{e.preventDefault();const f=new FormData(e.currentTarget);send('reviews',{id:p.id,version:p.version,decision:f.get('decision'),reason:f.get('reason')});}}><label>Revisão comercial<select name="decision"><option value="approved">Aprovar 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 revisão comercial</button></form>}{allowed('generate')&&p.current&&p.decision?.decision==='approved'&&!order.generation&&<button disabled={disabled} onClick={()=>send('generations',{id:p.id,version:p.version})}>Gerar prestação e rascunho rastreáveis</button>}</article>)}
|
|
{order.generation&&<section className="panel"><h2>Rascunho original gerado — sem validade fiscal</h2>{order.generation.commercial_state === "superseded" && <p className="alert home-notice">Original substituído; consultar a cadeia em Correcção e cancelamento.</p>}{order.generation.latest_commercial_change?.kind === "cancel" && <p className="alert home-notice">Preparação comercial cancelada. Origens não libertadas.</p>}{order.generation.latest_commercial_change?.new_billing_id && <p>Destino sucessor: prestação #{order.generation.latest_commercial_change.new_service_id} · rascunho #{order.generation.latest_commercial_change.new_billing_id}</p>}<p>Prestação #{order.generation.service_draft_id} · Rascunho de facturação #{order.generation.billing_draft_id} · Proposta #{order.generation.proposal_id}</p><p>Origens utilizadas uma única vez. Edição directa bloqueada; correcção exige circuito próprio, fora deste recorte.</p></section>}</>:<p>Sem ordens autorizadas neste contexto.</p>}</>}</section>;
|
|
}
|