Files
SGF/resources/js/MultiLineInvoiceWorkspace.jsx
2026-11-11 15:04:01 +02:00

50 lines
24 KiB
React

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 <aside className="multi-invoice-totals panel" aria-label="Totais do documento"><h2>Totais do cálculo seleccionado</h2><dl><dt>Base documental</dt><dd>{money(result?.base_document)}</dd><dt>Imposto documental</dt><dd>{money(result?.tax_document)}</dd><dt>Total</dt><dd className="multi-total">{money(result?.total)}</dd></dl>{!result&&<p>{arithmetic!==null?`${mode==='included'?'Bruto':'Base aritmética'} proposta: ${arithmetic} MZN. `:''}Escolha e confira a regra aplicável; imposto desconhecido não é zero.</p>}{result&&<p>Valores documentais das linhas somados. {result.price_mode==='gross_included'?'Preço com imposto incluído.':'Preço sem imposto; imposto acrescido.'}</p>}</aside>;}
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 <fieldset disabled={disabled} className="multi-reference"><legend>{label}</legend><div className="actions" data-sgf-read-filters="true"><input aria-label={'Pesquisar '+label} value={search} onChange={e=>setSearch(e.target.value)}/><button type="button" onClick={()=>setQ({page:1,search})}>Pesquisar</button></div><select aria-label={label} value={value??''} onChange={e=>onChange(e.target.value,rows.find(r=>String(r.id)===e.target.value))}><option value="">Escolher explicitamente…</option>{value&&!rows.some(r=>String(r.id)===String(value))&&<option value={value}>Escolha conservada #{value}</option>}{rows.map(r=><option key={r.id} value={r.id}>{r.name??r.identifier??r.rule_key} {r.valid_from?`· ${r.valid_from} a ${r.valid_until??'por definir'}`:''}{r.rule_version?' · v'+r.rule_version:r.version?' · v'+r.version:''}</option>)}</select>{error&&<p role="alert">{error}</p>}{data&&<div className="actions"><button type="button" disabled={q.page<=1} onClick={()=>setQ({...q,page:q.page-1})}>Anteriores</button><span>Página {q.page}/{data.last_page}</span><button type="button" disabled={q.page>=data.last_page} onClick={()=>setQ({...q,page:q.page+1})}>Seguintes</button></div>}</fieldset>;
}
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 <section className="multi-invoice-workspace" ref={rootRef} data-sgf-operation-uncertain={protect?'true':undefined}><p className="eyebrow">COMERCIAL · FACTURAÇÃO</p><h1>Documento com várias linhas</h1><p className="fiscal-notice"><strong>TESTE — SEM VALIDADE FISCAL.</strong> Guardar, calcular, rever e emitir são decisões distintas. Sem recebimento ou lançamento automático.</p>{error&&<p role="alert" className="alert error">{error}</p>}{notice&&<p role="status">{notice}</p>}
<section className="panel"><h2>Rascunhos deste percurso</h2><form data-sgf-read-filters="true" onSubmit={e=>{e.preventDefault();setQuery({...query,page:1,search});}}><div className="actions"><label>Pesquisar documento<input disabled={locked} type="search" value={search} onChange={e=>setSearch(e.target.value)}/></label><label>Por página<select disabled={locked} value={query.per_page} onChange={e=>setQuery({...query,page:1,per_page:Number(e.target.value)})}><option>25</option><option>100</option></select></label><button disabled={locked}>Pesquisar</button><button type="button" disabled={locked||dirty||intentDirty} onClick={reset}>Novo documento</button></div></form>{list?.data.map(d=><button key={d.id} disabled={locked||dirty||intentDirty} onClick={()=>work(()=>open(d.id))}>{d.name} · v{d.version}</button>)}{list&&<nav aria-label="Páginas dos documentos"><button disabled={locked||query.page<=1} onClick={()=>setQuery({...query,page:query.page-1})}>Anterior</button><span>Página {list.current_page}/{list.last_page}</span><button disabled={locked||query.page>=list.last_page} onClick={()=>setQuery({...query,page:query.page+1})}>Seguinte</button></nav>}</section>
<div className="multi-invoice-layout"><div><form ref={formRef} onSubmit={previewDraft}><fieldset disabled={locked||!edit}><legend>{frame?'Rascunho #'+frame.billing.id+' · versão '+frame.billing.version:'Novo rascunho'}</legend><section className="panel"><h2>Cabeçalho</h2><div className="form-grid"><label>Designação<input required value={form.name} onChange={e=>change('name',e.target.value)} maxLength={255}/></label><label>Moeda<select required value={form.currency} onChange={e=>change('currency',e.target.value)}><option value="MZN">MZN</option></select></label><label>Modalidade de preço<select value={form.price_mode} onChange={e=>change('price_mode',e.target.value)}><option value="">Escolher…</option><option value="added">Sem imposto, acrescido</option><option value="included">Com imposto incluído</option></select></label></div><InvoiceReferencePicker api={api} kind="clients" value={form.client_id} label="Cliente" disabled={locked||!edit} onChange={(v,r)=>{change('client_id',v);setForm(f=>({...f,client_name:r?.name??''}));}}/>{frame?.parties?.client&&String(form.client_id)===String(frame.billing.client_id)&&<p>NUIT: {frame.parties.client.data.nuit??'Por preencher'} · Morada: {frame.parties.client.data.address??'Por preencher'}</p>}<div className="form-grid">{[['service_date','Data do serviço'],['period_start','Período desde'],['period_end','Período até'],['proposed_due_date','Vencimento proposto']].map(([f,l])=><label key={f}>{l}<input type="date" value={form[f]} onChange={e=>change(f,e.target.value)}/></label>)}</div><label>Condições de pagamento<textarea value={form.payment_terms} onChange={e=>change('payment_terms',e.target.value)} maxLength={1000}/></label></section>
<section className="panel"><h2>Linhas do documento</h2>{form.lines.map((l,i)=><fieldset key={i}><legend>Linha {i+1}</legend><InvoiceReferencePicker api={api} kind="services" value={l.service_id} label={'Serviço da linha '+(i+1)} disabled={locked||!edit} onChange={(v,r)=>{line(i,'service_id',v);setForm(f=>({...f,lines:f.lines.map((x,n)=>n===i?{...x,service_name:r?.name??''}:x)}));}}/><div className="multi-line-fields"><label>Descrição<input value={l.description??''} onChange={e=>line(i,'description',e.target.value)} maxLength={2000}/></label><label>Unidade<input value={l.unit??''} onChange={e=>line(i,'unit',e.target.value)} maxLength={80}/></label><label>Quantidade<input inputMode="decimal" value={l.quantity??''} onChange={e=>line(i,'quantity',e.target.value)}/></label><label>Preço {form.price_mode==='included'?'com imposto':'sem imposto'}<input inputMode="decimal" value={l.unit_price??''} onChange={e=>line(i,'unit_price',e.target.value)}/></label><button type="button" disabled={form.lines.length<=1} onClick={()=>change('lines',form.lines.filter((_,n)=>n!==i))}>Retirar linha</button></div></fieldset>)}<button type="button" disabled={form.lines.length>=25} onClick={()=>change('lines',[...form.lines,{service_id:'',service_name:'',description:'',quantity:'',unit_price:'',unit:''}])}>Adicionar linha</button><p>{form.lines.length} de25 linhas. Preços e quantidades em decimais exactos, com ponto.</p><label>Fundamento da preparação<textarea required minLength={10} maxLength={2000} value={form.reason} onChange={e=>change('reason',e.target.value)}/></label><div className="actions"><button className="primary">Conferir rascunho e preparação</button><button type="button" onClick={()=>{if(frame)work(()=>open(frame.billing.id));else reset();}}>Descartar alterações locais</button></div></section></fieldset></form>{!edit&&<p className="help">Consulta da origem. A alteração exige os direitos próprios de preparação; documentos emitidos conservam a fotografia.</p>}
{frame?.invoice&&<section className="panel"><h2>{frame.invoice.series_code} / {frame.invoice.number}</h2><a href={'/api/service-invoices/'+frame.invoice.id+'/pdf'}>Descarregar o PDF preservado</a><button disabled={locked} onClick={()=>work(async()=>{const d=await api('/api/service-invoices/'+frame.invoice.id);setReceipt({...d,type:'invoice'});})}>Consultar fotografia e histórico</button>{can('service_receivables.view',unit)&&<button disabled={locked} onClick={()=>navigate('service_receivables')}>Continuar no Financeiro</button>}{can('general_ledger.view',unit)&&<button disabled={locked} onClick={()=>navigate('accounting_panel')}>Consultar o diário</button>}</section>}
{intentDirty&&!cut&&!op.uncertain&&<button disabled={busy} onClick={()=>{setDecisionReason('');setSelectedRule('');setSelectedCalc(null);setSelectedTemporal(null);setSeries('');setIntentDirty(false);if(!dirty)markClean(rootRef.current);}}>Descartar conferência local</button>}
{frame&&!dirty&&!frame.invoice&&<section className="panel"><h2>Cálculo e conferência temporal</h2><p>O preparador fiscal conserva um cálculo vinculado a estas linhas. A regra temporal exige outro revisor antes da emissão.</p>{can('fiscal_rules.prepare',unit)&&<><InvoiceReferencePicker api={api} kind="rules" value={selectedRule} label="Regra QA revista e vigente" disabled={locked} onChange={v=>{setSelectedRule(v);setIntentDirty(true);}}/><label>Fundamento fiscal/documental<textarea disabled={locked} minLength={10} value={decisionReason} onChange={e=>{setDecisionReason(e.target.value);setIntentDirty(true);}}/></label><button disabled={locked||!selectedRule||decisionReason.trim().length<10} onClick={previewCalculation}>Conferir cálculo destas linhas</button></>}
<InvoiceReferencePicker api={api} kind="calculations" value={selectedCalc?.id??''} draftId={frame.billing.id} label="Cálculo preservado destas linhas" disabled={locked} onChange={(v,r)=>{setSelectedCalc(r??null);setIntentDirty(true);}}/>{selectedCalc&&<CalculationLines result={selectedCalc.result}/>}
{can('service_fiscal.propose')&&selectedCalc&&<><div className="form-grid"><label>Janela fundamentada desde<input disabled={locked} type="date" value={windowDates.from} onChange={e=>{setWindowDates({...windowDates,from:e.target.value});setIntentDirty(true);}}/></label><label>Janela fundamentada até<input disabled={locked} type="date" value={windowDates.to} onChange={e=>{setWindowDates({...windowDates,to:e.target.value});setIntentDirty(true);}}/></label></div><label>Fundamento da prontidão<textarea disabled={locked} value={decisionReason} minLength={10} onChange={e=>{setDecisionReason(e.target.value);setIntentDirty(true);}}/></label><button disabled={locked||!windowDates.from||!windowDates.to||decisionReason.trim().length<10} onClick={prepareTemporal}>{selectedTemporal?'Preparar nova versão temporal':'Preparar regra temporal desta origem'}</button></>}
<InvoiceReferencePicker api={api} kind="temporals" value={selectedTemporal?.id??''} draftId={frame.billing.id} label="Versão temporal" disabled={locked} onChange={(v,r)=>{setSelectedTemporal(r??null);setIntentDirty(true);}}/>{selectedTemporal&&!selectedTemporal.decision&&can('service_fiscal.approve')&&selectedTemporal.actor_id!==actor&&<><label>Fundamento independente<textarea disabled={locked} value={decisionReason} minLength={10} onChange={e=>{setDecisionReason(e.target.value);setIntentDirty(true);}}/></label>{['approved','refused'].map(d=><button key={d} disabled={locked||decisionReason.trim().length<10} onClick={()=>reviewTemporal(d)}>{d==='approved'?'Aprovar versão temporal':'Recusar versão temporal'}</button>)}</>}
{can('service_invoices.issue',unit)&&<><InvoiceReferencePicker api={api} kind="series" value={series} label="Série de teste revista" disabled={locked} filter={r=>r.decision?.decision==='approved'&&r.data.state==='open'} onChange={v=>{setSeries(v);setIntentDirty(true);}}/><button disabled={locked||!series||selectedTemporal?.decision?.decision!=='approved'} onClick={readiness}>Conferir prontidão e impacto de emissão</button></>}{can('service_invoices.view',unit)&&<button disabled={locked} onClick={()=>navigate('service_invoice_setup')}>Configuração de séries e emissão de teste</button>}</section>}
</div><InvoiceTotals result={scopedResult} mode={form.price_mode}/></div>
{cut&&<section className="panel fiscal-review"><h2>Impacto antes da confirmação</h2>{cut.domain==='drafts'?<><p>{cut.impact.after.name} · {cut.impact.after.client} · {cut.impact.after.lines.length} linhas.</p><p>Montante aritmético proposto: {cut.impact.after.arithmetic_amount??'Indeterminado'} MZN. Sem documento emitido, recebível ou lançamento.</p></>:cut.domain==='calculations'?<CalculationLines result={cut.impact.result}/>:<><p>Cliente: {cut.impact.document.client.name} · Série: {cut.impact.series_code} · Data prevista: {cut.impact.expected_date}</p><InvoiceTotals result={cut.impact.calculation_entry.snapshot.result.common_document_result}/><p>A confirmação atribui um número e fixa o PDF de teste. Recebível e diário exigem decisões posteriores.</p></>}<button className="primary" disabled={busy||op.uncertain} onClick={confirm}>{cut.domain==='issue'?'Confirmar emissão só de teste':'Confirmar este impacto'}</button><button disabled={busy||op.uncertain} onClick={()=>{setCut(null);if(dirty)formRef.current?.dispatchEvent?.(new Event('input',{bubbles:true}));setKey(crypto.randomUUID());}}>Voltar à conferência</button></section>}
{op.uncertain&&<section className="panel" role="alert"><h2>Resultado por recuperar</h2><p>Conserve a mesma operação. A recuperação usa a mesma chave e o mesmo pedido.</p><button disabled={busy} onClick={recover}>Recuperar a mesma operação</button></section>}
{receipt&&<section className="panel"><h2>Comprovativo e evidências</h2><p>Registo #{receipt.id} · {receipt.number?'número '+receipt.number:''} · {receipt.version?'versão '+receipt.version:''}</p>{receipt.document_lines?.map(([k,v],i)=>k==='title'?<h3 key={i}>{v}</h3>:k==='section'?<h4 key={i}>{v}</h4>:<p key={i}>{v}</p>)}{receipt.command_key&&<details><summary>Identificador para recuperar o comprovativo</summary><p>{receipt.command_key}</p></details>}<p>Autor, versões, decisão e fotografia permanecem no histórico da origem.</p></section>}
</section>;
}
export function CalculationLines({result:r}){if(!r)return null;return <><InvoiceTotals result={r}/><details><summary>Conferir valores por linha e diferenças</summary><div className="table-scroll"><table><thead><tr><th>Linha</th><th>Base documental</th><th>Imposto</th><th>Total</th><th>Exactos e diferenças</th></tr></thead><tbody>{r.lines.map(l=><tr key={l.line}><td>{l.line}</td><td>{money(l.base_document)}</td><td>{money(l.tax_document)}</td><td>{money(l.total_document)}</td><td>Base {l.base_exact_fraction} · imposto {l.tax_exact_fraction}<br/>Diferenças {l.base_difference_fraction} / {l.tax_difference_fraction}</td></tr>)}</tbody></table></div></details></>;}