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
; } 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}});});} returnCOMERCIAL · FACTURAÇÃO
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}
}Consulta da origem. A alteração exige os direitos próprios de preparação; documentos emitidos conservam a fotografia.
} {frame?.invoice&&O preparador fiscal conserva um cálculo vinculado a estas linhas. A regra temporal exige outro revisor antes da emissão.
{can('fiscal_rules.prepare',unit)&&<>{cut.impact.after.name} · {cut.impact.after.client} · {cut.impact.after.lines.length} linhas.
Montante aritmético proposto: {cut.impact.after.arithmetic_amount??'Indeterminado'} MZN. Sem documento emitido, recebível ou lançamento.
>:cut.domain==='calculations'?Cliente: {cut.impact.document.client.name} · Série: {cut.impact.series_code} · Data prevista: {cut.impact.expected_date}
A confirmação atribui um número e fixa o PDF de teste. Recebível e diário exigem decisões posteriores.
>}Conserve a mesma operação. A recuperação usa a mesma chave e o mesmo pedido.
Registo #{receipt.id} · {receipt.number?'número '+receipt.number:''} · {receipt.version?'versão '+receipt.version:''}
{receipt.document_lines?.map(([k,v],i)=>k==='title'?{v}
)}{receipt.command_key&&{receipt.command_key}
Autor, versões, decisão e fotografia permanecem no histórico da origem.
| Linha | Base documental | Imposto | Total | Exactos e diferenças |
|---|---|---|---|---|
| {l.line} | {money(l.base_document)} | {money(l.tax_document)} | {money(l.total_document)} | Base {l.base_exact_fraction} · imposto {l.tax_exact_fraction} Diferenças {l.base_difference_fraction} / {l.tax_difference_fraction} |