import React,{useEffect,useMemo,useRef,useState}from'react'; import{WorkshopStockOperation}from'./workshop-stock-operation.js'; const base='/api/multiline-invoices',policy='SGF-MULTI-LINE-INVOICE-INTEGRATED-QA-v1'; const fresh=()=>({draft_id:null,version:0,source_version:0,preparation_version:0,name:'',client_id:'',client_name:'',currency:'MZN',price_mode:'',service_date:'',period_start:'',period_end:'',proposed_due_date:'',payment_terms:'',lines:[{service_id:'',service_name:'',description:'',quantity:'',unit_price:'',unit:''}],reason:''}); export function checkedInvoiceOptions(v,kind,page,size){if(v.kind!==kind||v.records?.current_page!==page||v.records.per_page!==size||!Array.isArray(v.records.data)||v.records.data.length>size)throw Error('Resposta de outra consulta/página. Actualize.');return v.records;} const money=v=>v===null||v===undefined?'A conferir':`${String(v).replace('.',',')} MZN`; export function InvoiceTotals({result,arithmetic=null,mode}){return ;} export function InvoiceReferencePicker({api,kind,value,label,onChange,disabled=false,draftId=null,filter}){ const[q,setQ]=useState({page:1,search:''}),[search,setSearch]=useState(''),[data,setData]=useState(null),[error,setError]=useState('');useEffect(()=>{let live=true;if(disabled)return;setData(null);api(base+'/options?'+new URLSearchParams({kind,page:q.page,per_page:25,...(q.search?{search:q.search}:{}),...(draftId?{draft_id:draftId}:{})})).then(v=>{if(live){setData(checkedInvoiceOptions(v,kind,q.page,25));setError('');}}).catch(e=>{if(live)setError(e.message);});return()=>{live=false;};},[q,kind,draftId,disabled]);const rows=(data?.data??[]).filter(filter??(()=>true));return
{label}
setSearch(e.target.value)}/>
{error&&

{error}

}{data&&
Página {q.page}/{data.last_page}
}
; } export default function MultiLineInvoiceWorkspace({api,context,can,navigate,markClean=()=>{},actor}){ const[query,setQuery]=useState({page:1,per_page:25,search:''}),[search,setSearch]=useState(''),[list,setList]=useState(null),[frame,setFrame]=useState(null),[form,setForm]=useState(fresh),[dirty,setDirty]=useState(false),[intentDirty,setIntentDirty]=useState(false),[busy,setBusy]=useState(false),[error,setError]=useState(''),[notice,setNotice]=useState(''),[cut,setCut]=useState(null),[key,setKey]=useState(crypto.randomUUID()),[selectedRule,setSelectedRule]=useState(''),[calculations,setCalculations]=useState([]),[selectedCalc,setSelectedCalc]=useState(null),[temporals,setTemporals]=useState([]),[selectedTemporal,setSelectedTemporal]=useState(null),[series,setSeries]=useState(''),[decisionReason,setDecisionReason]=useState(''),[windowDates,setWindowDates]=useState({from:'',to:''}),[receipt,setReceipt]=useState(null); const op=useMemo(()=>new WorkshopStockOperation(api),[context.token]),formRef=useRef(null),rootRef=useRef(null),reads=useRef({list:0,editor:0,extras:0});const unit=context.unit?.id,locked=busy||op.uncertain||Boolean(cut),edit=can('service_drafts.'+(frame?'edit':'create'),unit)&&can('billing_drafts.'+(frame?'edit':'create'),unit)&&can('draft_documents.edit',unit)&&!frame?.invoice; const protect=locked||dirty||intentDirty; const scopedResult=frame?.invoice?.totals??(!dirty&&frame&&selectedCalc&&selectedCalc.draft_version===frame?.billing.version&&selectedCalc?.preparation_version===frame?.preparation?.version?selectedCalc.result:null); async function load(){const n=++reads.current.list;const v=await api(base+'/options?'+new URLSearchParams({kind:'drafts',...query}));if(n===reads.current.list)setList(checkedInvoiceOptions(v,'drafts',query.page,query.per_page));} async function extras(id){const n=++reads.current.extras;const paths=[api(base+'/options?kind=calculations&draft_id='+id+'&page=1&per_page=100'),can('service_invoices.view',unit)?api(base+'/options?kind=temporals&draft_id='+id+'&page=1&per_page=100'):Promise.resolve(null)];const results=await Promise.all(paths);if(n!==reads.current.extras)return;setCalculations(checkedInvoiceOptions(results[0],'calculations',1,100).data);if(results[1])setTemporals(checkedInvoiceOptions(results[1],'temporals',1,100).data);return{calculations:results[0].records.data,temporals:results[1]?.records.data??[]};} async function open(id){const n=++reads.current.editor;setError('');const v=await api(base+'/drafts/'+id);if(n!==reads.current.editor)return;if(v.policy!==policy||v.billing.id!==id)throw Error('Resposta de outra origem.');const p=v.preparation?.data??{};setFrame(v);setForm({draft_id:id,version:v.billing.version,source_version:v.source.version,preparation_version:v.preparation?.version??0,name:v.billing.name,client_id:v.billing.client_id,client_name:v.billing.data.client_name,currency:v.billing.data.currency??'',price_mode:p.price_mode==='undefined'?'':p.price_mode??'',service_date:p.service_date??'',period_start:p.period_start??'',period_end:p.period_end??'',proposed_due_date:p.proposed_due_date??'',payment_terms:p.payment_terms??'',lines:v.billing.data.lines.map((l,i)=>({...l,unit:p.units?.[i]??''})),reason:''});setDirty(false);setIntentDirty(false);setDecisionReason('');setSelectedRule('');setSeries('');setCut(null);setSelectedCalc(null);setSelectedTemporal(null);setReceipt(null);setKey(crypto.randomUUID());setWindowDates({from:v.server_date??'',to:v.server_date??''});markClean(rootRef.current);await extras(id);} useEffect(()=>{load().catch(e=>setError(e.message));},[query,context.token]); async function work(fn){if(busy)return;setBusy(true);setError('');try{await fn();}catch(e){setError(e.message);}finally{setBusy(false);}} function change(k,v){setForm(f=>({...f,[k]:v}));setDirty(true);setCut(null);setSelectedCalc(null);setSelectedTemporal(null);setReceipt(null);} function line(i,k,v){setForm(f=>({...f,lines:f.lines.map((l,n)=>n===i?{...l,[k]:v}:l)}));setDirty(true);setSelectedCalc(null);setSelectedTemporal(null);setReceipt(null);} function reset(){reads.current.editor++;reads.current.extras++;setFrame(null);setForm(fresh());setDirty(false);setIntentDirty(false);setDecisionReason('');setSelectedRule('');setCut(null);setSelectedCalc(null);setSelectedTemporal(null);setCalculations([]);setTemporals([]);setReceipt(null);setSeries('');setKey(crypto.randomUUID());markClean(rootRef.current);} async function previewDraft(e){e.preventDefault();await work(async()=>{const data={...form};delete data.client_name;data.price_mode=data.price_mode||'undefined';for(const f of['period_start','period_end'])data[f]=data[f]||null;data.client_id=Number(data.client_id);data.lines=data.lines.map(({service_id,description,quantity,unit_price,unit})=>({service_id:Number(service_id),description:description||null,quantity:quantity===''?null:quantity,unit_price:unit_price===''?null:unit_price,unit:unit||null}));for(const k of['service_date','proposed_due_date','payment_terms'])data[k]=data[k]||null;const v=await api(base+'/drafts/preview','POST',data,context.token,key);setCut({domain:'drafts',...v});});} async function previewCalculation(){await work(async()=>{const body={draft_id:frame.billing.id,draft_version:frame.billing.version,preparation_version:frame.preparation.version,rule_event_id:Number(selectedRule),reason:decisionReason};const v=await api(base+'/calculations/preview','POST',body,context.token,key);setCut({domain:'calculations',...v});});} async function complete(result,domain){result={...result,command_key:op.operation?.key};setIntentDirty(false);setReceipt(result);setCut(null);setKey(crypto.randomUUID());if(domain==='drafts'){await open(result.id);setReceipt(result);await load();setNotice('Rascunho e preparação guardados atomicamente; sem emissão.');}else if(domain==='calculations'){await extras(frame.billing.id);if(result.draft_id!==frame.billing.id)throw Error('Comprovativo de outro rascunho.');setSelectedCalc(result);setNotice('Cálculo preservado. A emissão ainda exige revisão temporal e confirmação.');}else if(domain==='issue'){const id=frame.billing.id;await open(id);setReceipt(result);setNotice('Documento de teste emitido uma vez. Recebível e Contabilidade mantêm as suas decisões.');}else{const next=await extras(frame.billing.id);setSelectedTemporal(next?.temporals.find(t=>t.id===result.id)??null);setNotice('Decisão temporal registada. Seleccione explicitamente a versão actual.');}markClean(rootRef.current);} async function confirm(){await work(async()=>{const c=cut;if(c.domain==='issue'){const result=await op.send(base+'/drafts/'+frame.billing.id+'/issue',c.body,key);await complete(result,'issue');}else{const result=await op.send(base+'/'+c.domain+'/confirm',{preview:c.preview},key);await complete(result,c.domain);}});} async function recover(){await work(async()=>{const result=await op.retry();const domain=op.operation.path.endsWith('/issue')?'issue':op.operation.path.includes('/drafts/confirm')?'drafts':op.operation.path.includes('/calculations/confirm')?'calculations':'temporal';await complete(result,domain);});} async function prepareTemporal(){await work(async()=>{const data={calculation_policy:policy,common_calculation_id:selectedCalc.id,common_rule_event_id:selectedCalc.rule_event_id??Number(selectedRule),draft_id:frame.billing.id,draft_version:frame.billing.version,preparation_version:frame.preparation.version,valid_from:windowDates.from,valid_until:windowDates.to,period_start:form.period_start,period_end:form.period_end,issue_window_start:windowDates.from,issue_window_end:windowDates.to,event:'on_issue_within_deadline',readiness_basis:decisionReason,evidence:decisionReason};const body={kind:'temporal',identifier:selectedTemporal?.identifier??'MULTI-DRAFT-'+frame.billing.id,...(selectedTemporal?{previous_id:selectedTemporal.id}:{}),data};const result=await op.send(base+'/temporals',body,crypto.randomUUID());await complete(result,'temporal');});} async function reviewTemporal(decision){await work(async()=>{const result=await op.send(base+'/temporals/'+selectedTemporal.id+'/decision',{version:selectedTemporal.version,decision,reason:decisionReason},crypto.randomUUID());await complete(result,'temporal');});} async function readiness(){await work(async()=>{const r=await api(base+'/drafts/'+frame.billing.id+'/readiness','POST',{series_id:Number(series),rule_id:selectedTemporal.id});if(!r.ready){setError(r.payload.blocks.join(' '));return;}setKey(crypto.randomUUID());setCut({domain:'issue',impact:r.payload,body:{series_id:Number(series),rule_id:selectedTemporal.id,review_hash:r.review_hash}});});} return

COMERCIAL · FACTURAÇÃO

Documento com várias linhas

TESTE — SEM VALIDADE FISCAL. Guardar, calcular, rever e emitir são decisões distintas. Sem recebimento ou lançamento automático.

{error&&

{error}

}{notice&&

{notice}

}

Rascunhos deste percurso

{e.preventDefault();setQuery({...query,page:1,search});}}>
{list?.data.map(d=>)}{list&&}
{frame?'Rascunho #'+frame.billing.id+' · versão '+frame.billing.version:'Novo rascunho'}

Cabeçalho

{change('client_id',v);setForm(f=>({...f,client_name:r?.name??''}));}}/>{frame?.parties?.client&&String(form.client_id)===String(frame.billing.client_id)&&

NUIT: {frame.parties.client.data.nuit??'Por preencher'} · Morada: {frame.parties.client.data.address??'Por preencher'}

}
{[['service_date','Data do serviço'],['period_start','Período desde'],['period_end','Período até'],['proposed_due_date','Vencimento proposto']].map(([f,l])=>)}