Accept conditional service calculation with versioned fiscal profile
This commit is contained in:
@@ -9,3 +9,4 @@
|
||||
.accounting-proposal dl { display: grid; gap: .5rem; }
|
||||
.accounting-proposal dl > div { display: grid; grid-template-columns: minmax(8rem, 1fr) 2fr; gap: 1rem; }
|
||||
.accounting-proposal dd { margin: 0; overflow-wrap: anywhere; white-space: pre-wrap; }
|
||||
.fiscal-page-heading h1{margin:8px 0 12px}.fiscal-page-heading>p:last-child{color:#5d716d;margin:0 0 24px}.fiscal-notice{display:flex;flex-direction:column;gap:7px;background:#fff7e7;border:1px solid #ead7ad;border-left:4px solid #c5983e;border-radius:8px;padding:16px 20px;margin-bottom:24px;color:#745723;font-size:.85rem;line-height:1.6}.fiscal-notice strong{font-size:.75rem;letter-spacing:.04em}.fiscal-list-heading{display:flex;justify-content:space-between;align-items:center;gap:16px;margin:26px 0 16px}.fiscal-list-heading h2{margin:0}.fiscal-list-heading>span{font-size:.8rem;color:#64796f}.fiscal-proposal{background:#fff;border:1px solid #dce6e1;border-radius:12px;padding:28px;margin-bottom:24px;box-shadow:0 3px 12px #123f3505}.fiscal-card-heading{display:flex;justify-content:space-between;align-items:flex-start;gap:20px}.fiscal-card-heading h3{font-size:1.3rem;margin:8px 0;letter-spacing:-.02em}.fiscal-card-heading .eyebrow{margin-top:0}.fiscal-reference{color:#65786f;font-size:.78rem;margin:0;overflow-wrap:anywhere}.fiscal-statuses{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}.fiscal-status{border-radius:20px;padding:6px 10px;white-space:nowrap;font-size:.72rem;font-weight:650;background:#edf2ef;color:#53665d}.fiscal-status.pending{background:#fff2d3;color:#80601c}.fiscal-status.approved{background:#e5f3e9;color:#286444}.fiscal-status.refused{background:#fbece8;color:#963f2d}.fiscal-summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));margin:24px 0;background:#f3f7f4;border:1px solid #e0e9e3;border-radius:8px;padding:18px;gap:20px}.fiscal-summary dt,.fiscal-facts dt{color:#667b70;font-size:.75rem;margin-bottom:8px}.fiscal-summary dd{margin:0;font-weight:650;font-size:.95rem;line-height:1.5}.fiscal-facts{display:grid;grid-template-columns:1fr 1fr;gap:18px 30px;margin:0 0 24px}.fiscal-facts dd{margin:0;line-height:1.5;font-size:.9rem}.fiscal-conditions{border-top:1px solid #e5ece7;padding-top:18px}.fiscal-conditions h4,.fiscal-review h4{font-size:.95rem;margin:0 0 10px}.fiscal-conditions p,.fiscal-evidence p,.fiscal-recorded p{font-size:.88rem;line-height:1.85;white-space:pre-wrap;overflow-wrap:anywhere;margin:0 0 18px;max-width:75ch}.fiscal-evidence{border-top:1px solid #e5ece7;padding:16px 0}.fiscal-evidence summary,.fiscal-new>summary{cursor:pointer;color:#28634f;font-weight:650;font-size:.9rem}.fiscal-evidence p{margin:15px 0 0;color:#5d716d}.fiscal-review{background:#f5f8f6;border-top:1px solid #dce6e1;margin:8px -28px -28px;padding:24px 28px;border-radius:0 0 12px 12px}.fiscal-review>p{font-size:.85rem;color:#5d716d;margin:0 0 18px}.fiscal-review label{max-width:700px}.fiscal-review textarea{resize:vertical;min-height:110px}.fiscal-refuse{color:#923f2c;border-color:#dbb7ac}.fiscal-recorded{padding-top:16px;border-top:1px solid #e5ece7}.fiscal-recorded>strong{font-size:.85rem}.fiscal-new form{margin-top:24px}.fiscal-new textarea{resize:vertical}.fiscal-proposal dd{overflow-wrap:anywhere}@media(max-width:1000px){.fiscal-summary{grid-template-columns:1fr 1fr}.fiscal-card-heading{flex-direction:column}.fiscal-statuses{justify-content:flex-start}}@media(max-width:560px){.fiscal-proposal{padding:20px}.fiscal-review{margin:8px -20px -20px;padding:20px}.fiscal-list-heading{display:block}.fiscal-list-heading>span{display:block;margin-top:8px}.fiscal-facts{grid-template-columns:1fr}.fiscal-summary{gap:16px;padding:14px}.fiscal-card-heading h3{font-size:1.12rem}.fiscal-review .actions button{flex:1}}
|
||||
|
||||
@@ -52,6 +52,7 @@ export default function DocumentPreview({ id, historical, context, api, onClose
|
||||
<p>Base aritmética exacta: <strong>{entry.snapshot.result.base_exact}</strong></p><p>IVA exacto: <strong>{entry.snapshot.result.vat_exact}</strong></p><p>IVA truncado proposto: <strong>{entry.snapshot.result.vat_document_proposed}</strong></p><p>Diferença descartada: <strong>{entry.snapshot.result.discarded_difference}</strong></p><p>Total documental proposto: <strong>{entry.snapshot.result.total_document_proposed ?? 'Pendente: representação da base/total'}</strong> {entry.snapshot.result.currency}</p>{entry.snapshot.result.pending.map((text,i)=><p key={i}>{text}</p>)}<p>Sem validação fiscal. {notice}</p></div>)}
|
||||
</>}
|
||||
</section>}
|
||||
{!historical && view.fiscal_history?.length>0 && <section><h2>Cálculo proposto do serviço — condicionado</h2>{view.fiscal_history.map(entry=><div className="preview-values" key={entry.id}><h3>Resultado #{entry.id} · {entry.current?'Actual para as versões validadas':'Desactualizado — histórico'}</h3><p>Perfil {entry.snapshot.input.profile_identifier} v{entry.snapshot.input.profile_version} · decisão temporal v{entry.snapshot.input.temporal_version}</p><p>Base exacta: {entry.snapshot.result.base_exact} {entry.snapshot.result.currency}</p><p>IVA exacto: {entry.snapshot.result.vat_exact}</p><p>IVA truncado proposto: {entry.snapshot.result.vat_document_proposed}</p><p>Diferença preservada: {entry.snapshot.result.discarded_difference}</p><p>Total proposto: {entry.snapshot.result.total_document_proposed ?? 'Pendente'}</p><p>Exigibilidade: {entry.snapshot.result.exigibility_date ?? 'Não determinada'}</p><p>Período de apuramento: {entry.snapshot.result.assessment_period ?? 'Não determinado'}</p>{entry.snapshot.result.pending.map((p,i)=><p key={i}>{p}</p>)}<p>{notice} · Não representa emissão ou apuramento líquido do imposto.</p></div>)}</section>}
|
||||
{!!view.payments.length && <section><h2>Instruções do emitente para facilitar o pagamento</h2><p className="help">Informação introduzida pelo utilizador, sem verificação bancária. Não regista recebimento nem inicia operação bancária.</p>{view.payments.map(payment => <div key={payment.id} className="preview-payment"><h3>{payment.data.label} · versão {payment.version}</h3>{Object.entries({ bank:'Banco', holder:'Titular', account_number:'Conta', nib:'NIB', iban:'IBAN', swift:'SWIFT', bank_code:'Código bancário', currency:'Moeda' }).filter(([key]) => payment.data[key]).map(([key,label]) => <p key={key}>{label}: <strong>{payment.data[key]}</strong></p>)}{payment.data.instructions && <p className="preview-text">{payment.data.instructions}</p>}</div>)}</section>}
|
||||
{view.observations && <section><h2>Observações</h2><p className="preview-text">{view.observations}</p></section>}
|
||||
<section className="preview-pending" aria-label="Pendências obrigatórias"><h2>Dados em falta e validações pendentes</h2><p>Preenchido não significa validado. Imposto e total definitivo continuam por apurar.</p><ul>{Object.entries(view.pending).map(([code,text]) => <li key={code}>{text}</li>)}</ul></section><p className="preview-end-notice">{notice}</p>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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>;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import ServicesWorkspace, { serviceAreas } from './ServicesWorkspace.jsx';
|
||||
import DocumentPreparationWorkspace from './DocumentPreparationWorkspace.jsx';
|
||||
import { DocumentPreviewConfiguration } from './DocumentPreview.jsx';
|
||||
import CatalogueAccountingWorkspace from './CatalogueAccountingWorkspace.jsx';
|
||||
import ServiceFiscalWorkspace from './ServiceFiscalWorkspace.jsx';
|
||||
|
||||
const labels = { 'catalogue_accounting.view':'Consultar classificação contabilística', 'catalogue_accounting.propose':'Preparar classificação contabilística', 'catalogue_accounting.approve':'Validar ou recusar classificação contabilística', 'company.view': 'Consultar empresa', 'company.edit': 'Editar nome da empresa', 'unit.create': 'Criar unidades', 'access.manage': 'Gerir acessos', 'unit.view': 'Consultar unidade', 'unit.edit': 'Editar nome da unidade' };
|
||||
|
||||
@@ -49,7 +50,7 @@ function App() {
|
||||
if (!state) return <main className="loading"><h1>SGF TEMPO</h1><p role="status">{error || 'A carregar…'}</p></main>;
|
||||
if (state.setup_pending) return <main className="setup-pending"><p className="eyebrow">SGF TEMPO · PREPARAÇÃO LOCAL</p><h1>Falta concluir a primeira conta</h1><p>A aplicação está pronta, mas ainda não existe um utilizador. O login ficará disponível depois de definires a palavra-passe no procedimento local já autorizado.</p><section className="panel"><h2>Concluir no terminal do VS Code</h2><p>Executa este comando, confere o âmbito apresentado e define a palavra-passe nos dois pedidos de entrada oculta:</p><pre>powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\laragon\data\sgf-tempo\run\complete-first-admin.ps1</pre><p className="help">Não envies a palavra-passe pelo chat. Nenhuma conta é criada por este ecrã.</p><button className="primary" disabled={busy} onClick={() => perform(async () => {}, '')}>Já concluí — actualizar estado</button>{error && <p role="alert">{error}</p>}</section></main>;
|
||||
if (!state.user) return <div className="login-layout"><aside><span className="brand">TEMPO<span>SGF</span></span><h1>O seu trabalho,<br />no contexto certo.</h1><p>Uma entrada para as empresas e unidades a que tem acesso.</p><small>Ambiente local · Primeiro percurso funcional</small></aside><main className="login-card"><p className="eyebrow">BEM-VINDO AO SGF TEMPO</p><h2>Entrar na aplicação</h2><p className="muted">Use a conta preparada pelo procedimento local autorizado.</p>{error && <div className="alert error" role="alert">{error}</div>}<Login busy={busy} onSubmit={(email, password) => perform(() => api('/api/login', 'POST', { email, password }), 'Sessão iniciada. Seleccione o contexto de trabalho.')} /><p className="help">Precisa de recuperar o acesso? Solicite recuperação local assistida. Não existe recuperação por email nesta etapa.</p></main></div>;
|
||||
return <div className="shell"><aside className="sidebar"><a className="brand" href="/">TEMPO<span>SGF</span></a><p className="sidebar-label">ESPAÇO DE TRABALHO</p><nav>{[['home', 'Área inicial'], ['company', 'Empresa'], ['units', 'Unidades'], ...(can('access.manage') ? [['access', 'Acessos']] : []), ...((can('issuer_documents.view') || can('client_documents.view') || can('draft_documents.view', context?.unit?.id ?? null)) ? [['documents', 'Preparação documental'], ...(can('issuer_documents.view') ? [['preview_model', 'Apresentação do rascunho']] : [])] : []), ...(can('catalogue_accounting.view') ? [['catalogue_accounting', 'Classificação contabilística']] : []), ...Object.entries(serviceAreas).filter(([area]) => can(`${area}.view`, area.endsWith('_drafts') ? context?.unit?.id ?? null : null))].map(([id, text]) => <button key={id} className={page === id ? 'active' : ''} onClick={() => { setPage(id); setError(''); }}>{text}</button>)}</nav><div className="sidebar-bottom"><strong>{state.user.name}</strong><small>{state.user.email}</small><button disabled={busy} onClick={() => perform(() => api('/api/logout', 'POST'), 'Sessão terminada.')}>Sair da aplicação</button></div></aside><div className="workspace"><header><div><span className="eyebrow">CONTEXTO DE TRABALHO</span><strong>{context?.company.name || 'Seleccione uma empresa'}</strong><span className="muted">{context?.unit?.name || 'Sem unidade seleccionada'}</span></div><span className="local-badge">Ambiente local</span></header><main>
|
||||
return <div className="shell"><aside className="sidebar"><a className="brand" href="/">TEMPO<span>SGF</span></a><p className="sidebar-label">ESPAÇO DE TRABALHO</p><nav>{[['home', 'Área inicial'], ['company', 'Empresa'], ['units', 'Unidades'], ...(can('access.manage') ? [['access', 'Acessos']] : []), ...((can('issuer_documents.view') || can('client_documents.view') || can('draft_documents.view', context?.unit?.id ?? null)) ? [['documents', 'Preparação documental'], ...(can('issuer_documents.view') ? [['preview_model', 'Apresentação do rascunho']] : [])] : []), ...(can('service_fiscal.view') ? [['service_fiscal', 'Perfil fiscal do servi\u00e7o']] : []), ...(can('catalogue_accounting.view') ? [['catalogue_accounting', 'Classificação contabilística']] : []), ...Object.entries(serviceAreas).filter(([area]) => can(`${area}.view`, area.endsWith('_drafts') ? context?.unit?.id ?? null : null))].map(([id, text]) => <button key={id} className={page === id ? 'active' : ''} onClick={() => { setPage(id); setError(''); }}>{text}</button>)}</nav><div className="sidebar-bottom"><strong>{state.user.name}</strong><small>{state.user.email}</small><button disabled={busy} onClick={() => perform(() => api('/api/logout', 'POST'), 'Sessão terminada.')}>Sair da aplicação</button></div></aside><div className="workspace"><header><div><span className="eyebrow">CONTEXTO DE TRABALHO</span><strong>{context?.company.name || 'Seleccione uma empresa'}</strong><span className="muted">{context?.unit?.name || 'Sem unidade seleccionada'}</span></div><span className="local-badge">Ambiente local</span></header><main>
|
||||
{error && <div className="alert error" role="alert">{error}</div>}{notice && <div className="alert success" role="status">{notice}</div>}
|
||||
<section className="context-card"><label>Empresa<select aria-label="Empresa activa" disabled={busy} value={context?.company.id || ''} onChange={e => select(e.target.value)}><option value="">Escolher empresa autorizada</option>{state.companies.map(c => <option value={c.id} key={c.id}>{c.name}</option>)}</select></label><label>Unidade<select aria-label="Unidade activa" disabled={busy || !context} value={context?.unit?.id || ''} onChange={e => select(context.company.id, e.target.value ? Number(e.target.value) : null)}><option value="">Sem unidade seleccionada</option>{context?.units.map(u => <option value={u.id} key={u.id}>{u.name}</option>)}</select></label><p>Apenas os contextos com acesso explícito são apresentados.</p></section>
|
||||
{!state.companies.length ? <section className="panel"><h1>Sem empresas autorizadas</h1><p>A conta existe, mas ainda não tem acesso a uma empresa. Peça ao responsável uma concessão explícita.</p></section> : !context ? <section className="panel"><h1>Escolha onde vai trabalhar</h1><p>Seleccione uma empresa. A pertença a um cliente contratante não dá acesso automático às suas empresas ou unidades.</p></section> : <div key={context.token}>
|
||||
@@ -59,6 +60,7 @@ function App() {
|
||||
{serviceAreas[page] && <ServicesWorkspace key={page} area={page} context={context} can={can} api={api} perform={perform} busy={busy} initialId={aSelection?.area === page ? aSelection.id : null} navigate={(area,id) => { setASelection({area,id}); setPage(area); }} />}
|
||||
{page === 'preview_model' && <DocumentPreviewConfiguration context={context} can={can} api={api} perform={perform} busy={busy} />}
|
||||
{page === 'documents' && <DocumentPreparationWorkspace context={context} can={can} api={api} perform={perform} busy={busy} />}
|
||||
{page === 'service_fiscal' && <ServiceFiscalWorkspace context={context} can={can} api={api} perform={perform} busy={busy} />}
|
||||
{page === 'catalogue_accounting' && <CatalogueAccountingWorkspace context={context} can={can} api={api} perform={perform} busy={busy} />}
|
||||
{page === 'access' && (can('access.manage') ? <AccessPanel context={context} can={can} api={api} perform={perform} busy={busy} /> : <section className="panel"><h1>Acesso não autorizado</h1><p>A gestão de acessos exige uma concessão própria.</p></section>)}
|
||||
</div>}
|
||||
|
||||
Reference in New Issue
Block a user