Files
SGF/resources/js/ServiceFiscalWorkspace.jsx
T

53 lines
11 KiB
React

import React, { useEffect, useState } from 'react';
const fieldLabels = {currency:'Moeda',treatment:'Tratamento fiscal',price_mode:'Modo de preço',service_id:'Serviço',service_version:'Versão do serviço',valid_from:'Vigência desde',valid_until:'Vigência até',rate_percent:'Taxa de IVA',profile_proposal_id:'Proposta do perfil',draft_id:'Rascunho',draft_version:'Versão do rascunho',preparation_version:'Versão da preparação',reference_date:'Data de referência',reference_basis:'Fundamento da referência',period_start:'Início do período',period_end:'Fim do período',next_billing:'Próxima facturação prevista',event:'Acontecimento',event_date:'Data do acontecimento'};
const valueLabels = {domestic_normal:'Operação interna · regime normal',added:'Preço sem IVA · imposto acrescido',included:'Imposto incluído',pending:'Por determinar',invoice_in_time:'Emissão dentro do prazo fundamentado',invoice_deadline:'Termo do prazo de emissão fundamentado'};
function displayValue(key,value) {
if(value===null || value===undefined || value==='') return 'Não indicado';
if(['valid_from','valid_until','reference_date','period_start','period_end','next_billing','event_date'].includes(key) && /^\d{4}-\d{2}-\d{2}$/.test(value)) return value.split('-').reverse().join('/');
if(key==='rate_percent') return `${value}%`;
if(['service_id','draft_id','profile_proposal_id'].includes(key)) return `#${value}`;
return valueLabels[value] || String(value);
}
function FiscalProposal({p,can,busy,reasons,setReasons,perform,api,load,listing,context}) {
const d=p.definition, status=p.decision?.decision;
const stateLabel=status==='approved'?'Validada':status==='refused'?'Recusada':'Aguarda validação';
const summary=p.kind==='profile'?['rate_percent','currency','price_mode','valid_from']:['period_start','period_end','event','event_date'];
return <article className="fiscal-proposal">
<div className="fiscal-card-heading"><div><p className="eyebrow">{p.kind==='profile'?'PERFIL FISCAL':'DECISÃO TEMPORAL'} · PROPOSTA #{p.id}</p><h3>{p.kind==='profile'?`Enquadramento de IVA · serviço #${d.service_id}`:`Período e exigibilidade · rascunho #${d.draft_id}`}</h3><p className="fiscal-reference">{p.identifier} · Versão {p.version}</p></div><div className="fiscal-statuses"><span className={`fiscal-status ${status || 'pending'}`}>{stateLabel}</span>{p.kind==='profile' && <span className={`fiscal-status ${p.active?'approved':'inactive'}`}>{p.active?'Activo para cálculo proposto':'Sem activação'}</span>}</div></div>
<dl className="fiscal-summary">{summary.map(key=><div key={key}><dt>{fieldLabels[key]}</dt><dd>{displayValue(key,d[key])}</dd></div>)}</dl>
<dl className="fiscal-facts">{Object.entries(d).filter(([key])=>!summary.includes(key) && !['evidence','conditions','reference_basis'].includes(key)).map(([key,value])=><div key={key}><dt>{fieldLabels[key] || key}</dt><dd>{displayValue(key,value)}</dd></div>)}</dl>
{d.conditions && <section className="fiscal-conditions"><h4>Condições de aplicação</h4><p>{d.conditions}</p></section>}
{d.reference_basis && <section className="fiscal-conditions"><h4>Fundamento da data de referência</h4><p>{d.reference_basis}</p></section>}
{d.evidence && <details className="fiscal-evidence"><summary>Fontes e elementos de suporte</summary><p>{d.evidence}</p></details>}
{p.decision && <div className="fiscal-recorded"><strong>Fundamento da decisão</strong><p>{p.decision.reason}</p></div>}
{!p.decision && can('service_fiscal.approve') && <div className="fiscal-review"><h4>Validar esta versão</h4><p>A decisão refere-se à versão {p.version}, na unidade seleccionada. Validar não activa o perfil nem emite uma factura.</p><label htmlFor={`fiscal-reason-${p.id}`}>Fundamento da decisão<textarea id={`fiscal-reason-${p.id}`} rows={4} placeholder="Indique o fundamento da validação ou recusa." value={reasons[p.id] || ''} onChange={e=>setReasons({...reasons,[p.id]:e.target.value})}/></label><div className="actions">{['approved','refused'].map(decision=><button className={decision==='approved'?'primary':'fiscal-refuse'} key={decision} disabled={busy || !reasons[p.id]} onClick={()=>perform(async()=>{await api(`/api/service-fiscal/proposals/${p.id}/decision`,'POST',{version:p.version,decision,reason:reasons[p.id]});await load();},'Decisão registada para a versão exacta; sem activação.')}>{decision==='approved'?'Validar versão':'Recusar versão'}</button>)}</div></div>}
{p.kind==='temporal' && status==='approved' && can('service_fiscal.calculate',context.unit.id) && <div className="fiscal-review"><button disabled={busy} onClick={()=>perform(async()=>{const profile=listing.proposals.find(x=>x.id===d.profile_proposal_id); if(!profile) throw new Error('Perfil exacto não disponível; actualize a consulta.'); await api(`/api/service-fiscal/drafts/${d.draft_id}/calculate`,'POST',{profile_proposal_id:profile.id,profile_version:profile.version,temporal_proposal_id:p.id,temporal_version:p.version,draft_version:d.draft_version,preparation_version:d.preparation_version});},'Resultado proposto guardado; consulte a pré-visualização do rascunho.')}>Calcular com perfil activo e decisão exacta</button></div>}
</article>;
}
export default function ServiceFiscalWorkspace({ api, context, can, perform, busy }) {
const [listing,setListing]=useState(null), [error,setError]=useState(''), [kind,setKind]=useState('profile');
const [form,setForm]=useState({identifier:'',previous_id:'',service_id:'',service_version:'',valid_from:'',valid_until:'',conditions:'',evidence:'',profile_proposal_id:'',draft_id:'',draft_version:'',preparation_version:'',reference_date:'',reference_basis:'',period_start:'',period_end:'',next_billing:'',event:'pending',event_date:''});
const [reasons,setReasons]=useState({});
async function load() { setListing(await api('/api/service-fiscal')); }
useEffect(()=>{setListing(null);setError(''); if(context.unit) load().catch(e=>setError(e.message));},[context.token]);
if (!context.unit) return <section className="panel"><h1>Enquadramento fiscal do serviço</h1><p>Seleccione uma unidade autorizada.</p></section>;
const fields=kind==='profile' ? {service_id:'ID do serviço existente',service_version:'Versão do serviço',valid_from:'Vigência desde',valid_until:'Vigência até (opcional)',conditions:'Condições de aplicabilidade',evidence:'Referências de validação e fontes'} : {profile_proposal_id:'ID da proposta exacta do perfil',draft_id:'ID do rascunho',draft_version:'Versão do rascunho',preparation_version:'Versão da preparação',reference_date:'Data explícita para verificar vigência',reference_basis:'Fundamento dessa referência (não presumido)',period_start:'Início do período na preparação',period_end:'Fim do período (opcional)',next_billing:'Próxima facturação prevista (opcional)',event_date:'Data do acontecimento (apenas se determinado)',evidence:'Evidência e fundamento do acontecimento/pêndencia'};
const dates=['valid_from','valid_until','reference_date','period_start','period_end','next_billing','event_date'];
async function propose(e) { e.preventDefault(); await perform(async()=>{
const data={}; Object.keys(fields).forEach(key=>{ if(form[key]!=='' && !(key==='event_date' && form.event==='pending')) data[key]=key.endsWith('_id') || key.endsWith('_version') ? Number(form[key]) : form[key]; });
if(kind==='profile') Object.assign(data,{currency:'MZN',price_mode:'added',rate_percent:'16',treatment:'domestic_normal'}); else data.event=form.event;
await api('/api/service-fiscal/proposals','POST',{kind,identifier:form.identifier,...(form.previous_id ? {previous_id:Number(form.previous_id)} : {}),data}); await load();
},'Proposta guardada; não activa o perfil.'); }
return <section className="fiscal-workspace"><div className="fiscal-page-heading"><p className="eyebrow">ENQUADRAMENTO DO SERVIÇO</p><h1>Perfis e validação fiscal</h1><p>Consulte as condições propostas e acompanhe a validação de cada versão.</p></div><div className="fiscal-notice"><strong>RASCUNHO — SEM VALIDADE FISCAL</strong><span>Aprovar não activa o perfil. Este ecrã não emite facturas; calcular não altera uma decisão temporal validada.</span></div>{error && <p className="alert error" role="alert">{error}</p>}
{listing?.migration_pending ? <p>Preparação técnica pendente de migração autorizada; não há perfil activo.</p> : <>
{can('service_fiscal.propose') && <details className="fiscal-new panel"><summary>Preparar nova proposta</summary><form onSubmit={propose}><h2>Preparar proposta para validação</h2><label>Tipo<select value={kind} onChange={e=>setKind(e.target.value)}><option value="profile">Perfil deste serviço · MZN · IVA geral 16% · preço sem IVA</option><option value="temporal">Decisão temporal de um rascunho</option></select></label>
<label>Identificador da proposta<input value={form.identifier} required onChange={e=>setForm({...form,identifier:e.target.value})}/></label><label>ID da versão anterior (apenas correcção)<input type="number" min="1" value={form.previous_id} onChange={e=>setForm({...form,previous_id:e.target.value})}/></label>
{kind==='temporal' && <label>Acontecimento<select value={form.event} onChange={e=>setForm({...form,event:e.target.value,event_date:''})}><option value="pending">Pendente — não determinar exigibilidade</option><option value="invoice_in_time">Emissão dentro do prazo fundamentado</option><option value="invoice_deadline">Termo do prazo de emissão fundamentado</option></select><small>Adiantamentos e afectações parciais ficam fora deste primeiro caso.</small></label>}
{Object.entries(fields).filter(([key])=>key!=='event_date' || form.event!=='pending').map(([key,label])=><label key={key}>{label}{['conditions','evidence','reference_basis'].includes(key)?<textarea rows={4} value={form[key]} required onChange={e=>setForm({...form,[key]:e.target.value})}/>:<input type={dates.includes(key)?'date':'text'} value={form[key]} required={!['valid_until','period_end','next_billing'].includes(key)} onChange={e=>setForm({...form,[key]:e.target.value})}/>}</label>)}
<p className="help">Fontes e condições são referências de validação; não escreva segredos. Correcção material gera nova versão. Facturação prevista não é emissão efectiva.</p><button className="primary" disabled={busy}>Submeter proposta</button></form></details>}
<div className="fiscal-list-heading"><h2>Propostas na unidade</h2><span>{context.unit.name} · {listing?.proposals.length ?? '…'} proposta(s)</span></div>{listing?.proposals.map(p=><FiscalProposal key={p.id} {...{p,can,busy,reasons,setReasons,perform,api,load,listing,context}}/>)}{!listing && <p role="status">A carregar propostas…</p>}{listing && !listing.proposals.length && <div className="panel"><h3>Ainda não há propostas nesta unidade</h3><p>As propostas e respectivas decisões serão apresentadas aqui.</p></div>}</>}
</section>;
}