23 lines
10 KiB
React
23 lines
10 KiB
React
import React, {useEffect,useState} from 'react';
|
|
|
|
const notice='TESTE — SEM VALIDADE FISCAL';
|
|
export default function ServiceInvoiceWorkspace({api,context,can,perform,busy}) {
|
|
const [list,setList]=useState(null),[error,setError]=useState(''),[review,setReview]=useState(null),[document,setDocument]=useState(null),[key,setKey]=useState(crypto.randomUUID());
|
|
const [choice,setChoice]=useState({draft_id:'',series_id:'',rule_id:''}),[reason,setReason]=useState(''),[reasons,setReasons]=useState({});
|
|
const [kind,setKind]=useState('series'),[f,setF]=useState({identifier:'',previous_id:'',code:'',first_number:'',state:'open',valid_from:'',valid_until:'',evidence:'',draft_id:'',draft_version:'',preparation_version:'',profile_proposal_id:'',period_start:'',period_end:'',issue_window_start:'',issue_window_end:'',readiness_basis:''});
|
|
async function load(){setList(await api('/api/service-invoices'));}
|
|
useEffect(()=>{setList(null);setReview(null);setDocument(null);setError('');if(context.unit)load().catch(e=>setError(e.message));},[context.token]);
|
|
const unit=context.unit?.id;
|
|
const fields=kind==='series'?{code:'Código da série de teste (TEST-...)',first_number:'Primeiro número de teste',valid_from:'Vigência desde',valid_until:'Vigência até (opcional)',evidence:'Fundamento da configuração sintética'}:{draft_id:'Rascunho existente',draft_version:'Versão do rascunho',preparation_version:'Versão da preparação',profile_proposal_id:'Proposta do perfil fiscal',valid_from:'Vigência desde',valid_until:'Vigência até (opcional)',period_start:'Início da cobertura',period_end:'Fim da cobertura',issue_window_start:'Janela fundamentada de emissão desde',issue_window_end:'Janela fundamentada até',readiness_basis:'Fundamento da prontidão documental e representação',evidence:'Evidências e pressupostos sintéticos'};
|
|
async function propose(e){e.preventDefault();await perform(async()=>{const data={};for(const field of Object.keys(fields))if(f[field]!=='')data[field]=field.endsWith('_id')||field.endsWith('_version')||field==='first_number'?Number(f[field]):f[field];if(kind==='series')data.state=f.state;else data.event='on_issue_within_deadline';await api('/api/service-invoices/proposals','POST',{kind,identifier:kind==='series'?f.code:f.identifier,...(f.previous_id?{previous_id:Number(f.previous_id)}:{}),data});await load();},'Proposta de teste submetida para validação.');}
|
|
return <section className="fiscal-workspace invoice-test-workspace"><p className="eyebrow">DEMONSTRAÇÃO DOCUMENTAL ISOLADA</p><h1>Facturas de teste</h1><div className="fiscal-notice"><strong>{notice}</strong><span>Emissão real bloqueada. Sem comunicação à AT, envio automático, recebimentos ou lançamentos.</span></div>{error&&<p role="alert">{error}</p>}
|
|
{!context.unit?<p>Seleccione uma unidade autorizada.</p>:!list?<p>A carregar…</p>:!list.test_enabled?<div className="panel"><h2>Emissão real indisponível</h2><p>{list.notice}</p></div>:<>
|
|
{(can('invoice_series.propose')||can('service_fiscal.propose'))&&<details className="panel fiscal-new"><summary>Preparar série ou regra temporal de teste</summary><form onSubmit={propose}><label>Tipo<select value={kind} onChange={e=>setKind(e.target.value)}><option value="series">Configuração de série sintética</option><option value="temporal">Regra temporal prospectiva</option></select></label>{kind==='temporal'&&<label>Identificador da regra<input required value={f.identifier} onChange={e=>setF({...f,identifier:e.target.value})}/></label>}<label>Proposta anterior (apenas nova versão)<input type="number" min="1" value={f.previous_id} onChange={e=>setF({...f,previous_id:e.target.value})}/></label>{kind==='series'&&<label>Estado da série<select value={f.state} onChange={e=>setF({...f,state:e.target.value})}><option value="open">Aberta</option><option value="closed">Encerrada</option></select></label>}{Object.entries(fields).map(([field,label])=><label key={field}>{label}{['evidence','readiness_basis'].includes(field)?<textarea required rows={4} value={f[field]} onChange={e=>setF({...f,[field]:e.target.value})}/>:<input type={field.includes('window')||field.startsWith('valid_')||field.startsWith('period_')?'date':'text'} required={field!=='valid_until'} value={f[field]} onChange={e=>setF({...f,[field]:e.target.value})}/>}</label>)}<button className="primary" disabled={busy||!can(kind==='series'?'invoice_series.propose':'service_fiscal.propose')}>Submeter proposta</button></form></details>}
|
|
<h2>Séries e regras temporais</h2>{list.proposals.map(p=><article className="fiscal-proposal" key={p.id}><div className="fiscal-card-heading"><div><p className="eyebrow">{p.kind==='series'?'SÉRIE SINTÉTICA':'REGRA TEMPORAL'}</p><h3>{p.identifier} · v{p.version}</h3><p>Proposta #{p.id} · {p.decision?.decision==='approved'?'Validada':p.decision?.decision==='refused'?'Recusada':'Aguarda validação'}</p></div></div>{p.kind==='series'?<p>Número inicial: {p.definition.first_number} · {p.definition.state==='open'?'Aberta':'Encerrada'} · Desde {p.definition.valid_from}</p>:<p>Rascunho #{p.definition.draft_id} · Emissão dentro da janela fundamentada {p.definition.issue_window_start} a {p.definition.issue_window_end}. A data efectiva só é fixada na confirmação.</p>}<p>{p.definition.evidence}</p>{p.definition.readiness_basis&&<p>{p.definition.readiness_basis}</p>}{!p.decision&&can(p.kind==='series'?'invoice_series.approve':'service_fiscal.approve')&&<div className="fiscal-review"><label>Fundamento da decisão<textarea value={reasons[p.id]||''} onChange={e=>setReasons({...reasons,[p.id]:e.target.value})}/></label><div className="actions">{['approved','refused'].map(decision=><button key={decision} className={decision==='approved'?'primary':''} disabled={busy||!reasons[p.id]} onClick={()=>perform(async()=>{await api(`/api/service-invoices/proposals/${p.id}/decision`,'POST',{version:p.version,decision,reason:reasons[p.id]});await load();},'Decisão registada apenas para a versão de teste.')}>{decision==='approved'?'Validar versão':'Recusar versão'}</button>)}</div></div>}</article>)}
|
|
{can('service_invoices.issue',unit)&&<section className="panel"><h2>Rever emissão de teste</h2><form onSubmit={e=>{e.preventDefault();perform(async()=>{setReview(null);setDocument(null);const r=await api(`/api/service-invoices/drafts/${choice.draft_id}/review`,'POST',{series_id:Number(choice.series_id),rule_id:Number(choice.rule_id)});setReview({...r,draft_id:Number(choice.draft_id)});setKey(crypto.randomUUID());},'Revisão preparada sem atribuir número.');}}><div className="form-grid"><label>Rascunho<input required type="number" min="1" value={choice.draft_id} onChange={e=>{setChoice({...choice,draft_id:e.target.value});setReview(null);}}/></label><label>Série aprovada<select required value={choice.series_id} onChange={e=>{setChoice({...choice,series_id:e.target.value});setReview(null);}}><option value="">Seleccionar série</option>{list.proposals.filter(p=>p.kind==='series'&&p.decision?.decision==='approved').map(p=><option value={p.id} key={p.id}>{p.identifier} v{p.version}</option>)}</select></label><label>Regra temporal aprovada<select required value={choice.rule_id} onChange={e=>{setChoice({...choice,rule_id:e.target.value});setReview(null);}}><option value="">Seleccionar regra</option>{list.proposals.filter(p=>p.kind==='temporal'&&p.decision?.decision==='approved').map(p=><option value={p.id} key={p.id}>{p.identifier} v{p.version} · rascunho #{p.definition.draft_id}</option>)}</select></label></div><button disabled={busy}>Rever prontidão</button></form>{review&&<div className="fiscal-review"><h3>{review.ready?'Pronto para demonstração isolada':'Emissão de teste bloqueada'}</h3><p>{review.notice}</p><p>Cliente: {review.payload.document.client.name} · Data prevista da operação: {review.payload.expected_date}. O instante efectivo será fixado na confirmação.</p>{review.payload.calculation_entry&&<p>Base: {review.payload.calculation_entry.snapshot.result.base_exact} · IVA proposto: {review.payload.calculation_entry.snapshot.result.vat_document_proposed} · Total: {review.payload.calculation_entry.snapshot.result.total_document_proposed} MZN</p>}<ul>{review.payload.blocks.map((b,i)=><li key={i}>{b}</li>)}</ul><button className="primary" disabled={busy||!review.ready} onClick={()=>perform(async()=>{const issued=await api(`/api/service-invoices/drafts/${review.draft_id}/issue`,'POST',{series_id:review.payload.series_id,rule_id:review.payload.rule_id,review_hash:review.review_hash},context.token,key);setDocument(await api(`/api/service-invoices/${issued.id}`));setReview(null);await load();},'Documento de teste fixado, sem emissão real.')}>Confirmar emissão apenas de teste</button></div>}</section>}
|
|
<section className="panel"><h2>Documentos de teste preservados</h2>{!list.invoices.length?<p>Nenhum documento emitido neste cenário.</p>:list.invoices.map(i=><button key={i.id} disabled={busy} onClick={()=>perform(async()=>{setDocument(await api(`/api/service-invoices/${i.id}`));setReview(null);},'Consulta da fotografia preservada.')}>{i.series_code} / {i.number} · rascunho #{i.draft_id}</button>)}</section>
|
|
{document&&<article className="preview-sheet invoice-test-document"><div className="fiscal-notice"><strong>{notice}</strong></div>{document.document_lines.map(([kind,text],i)=>kind==='title'?<h2 key={i}>{text}</h2>:kind==='section'?<h3 key={i}>{text}</h3>:<p key={i}>{text}</p>)}<div className="fiscal-notice"><strong>{notice}</strong><span>Fotografia imutável · modelo {document.snapshot.renderer}</span></div><a className="invoice-pdf-link" href={`/api/service-invoices/${document.id}/pdf`}>Descarregar PDF de teste</a><p className="help">O PDF guardado reproduz a mesma fotografia, sem nova numeração.</p>{can('service_invoices.correction_request',unit)&&<form onSubmit={e=>{e.preventDefault();perform(async()=>{await api(`/api/service-invoices/${document.id}/corrections`,'POST',{reason});setDocument(await api(`/api/service-invoices/${document.id}`));setReason('');},'Pedido de correcção registado, sem efeito fiscal ou monetário.');}}><label>Motivo do pedido de correcção<textarea required value={reason} maxLength={2000} onChange={e=>setReason(e.target.value)}/></label><button disabled={busy}>Pedir correcção · sem alterar o original</button></form>}{document.corrections.map(r=><p key={r.id}>Pedido #{r.id}: {r.reason} · Pendente, sem efeito fiscal.</p>)}</article>}
|
|
</>}
|
|
</section>;
|
|
}
|