Preserva classificacao contabilistica aceite e bases locais

Regista aceitacao funcional do primeiro incremento PGC-NIRF e preserva as dependencias documentais e tecnicas existentes. A validacao do calculo fiscal continua separada e pendente. Sem credenciais, bases operacionais ou publicacao.
This commit is contained in:
2026-10-26 13:47:11 +02:00
parent 8fcd52c31d
commit f8d57b52aa
86 changed files with 3621 additions and 8 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1,47 @@
import React, { useEffect, useState } from 'react';
const kinds={plan:'Plano da empresa',account:'Conta empresarial',category:'Categoria e regra-base',service_link:'Ligação do serviço à categoria',service_override:'Excepção explícita por serviço'};
const states={draft:'Proposta em preparação',submitted:'Submetida — aguarda validação',approved:'Aprovada — verificar vigência',refused:'Recusada'};
const labels={plan:'Plano',source_version:'Versão documental',plan_id:'Versão do plano aprovada',code:'Código da conta',name:'Designação',origin:'Origem da conta',movable:'Admite movimento após validação',evidence:'Fundamento e prova da conta empresarial',account_id:'Conta aprovada',conditions:'Condições e exclusões',service_id:'Serviço existente',category_id:'Categoria aprovada'};
function Decision({row,can,busy,act}) {
const [reason,setReason]=useState('');
if(row.state!=='submitted'||!can('catalogue_accounting.approve'))return null;
return <form onSubmit={e=>e.preventDefault()}><label>Motivo da decisão sobre a versão {row.version}<textarea required maxLength={2000} value={reason} onChange={e=>setReason(e.target.value)} /></label><div className="actions"><button disabled={busy||!reason.trim()} onClick={()=>act(`/proposals/${row.id}/decision`,{decision:'approved',reason})}>Validar esta versão</button><button disabled={busy||!reason.trim()} onClick={()=>act(`/proposals/${row.id}/decision`,{decision:'refused',reason})}>Recusar esta versão</button></div><p className="help">Decisão fixa apenas esta versão. Não edita a proposta, reconhece rendimento ou valida IVA.</p></form>;
}
export default function CatalogueAccountingWorkspace({context,can,api,perform,busy}) {
const [list,setList]=useState(null),[error,setError]=useState(''),[kind,setKind]=useState('plan'),[data,setData]=useState({}),[previous,setPrevious]=useState(null),[replaces,setReplaces]=useState(''),[from,setFrom]=useState(''),[until,setUntil]=useState(''),[reason,setReason]=useState('');
const [service,setService]=useState(''),[date,setDate]=useState(''),[resolution,setResolution]=useState(null),[area,setArea]=useState('service_drafts'),[operations,setOperations]=useState([]),[operation,setOperation]=useState(''),[history,setHistory]=useState([]);
const prefix='/api/catalogue-accounting';
async function load(){setList(await api(prefix));}
useEffect(()=>{load().catch(e=>setError(e.message));},[context.token]);
async function act(path,body) {await perform(async()=>{await api(prefix+path,'POST',body,context.token,crypto.randomUUID());await load();},'Operação de classificação guardada; sem lançamentos.');}
function fresh(type){setKind(type);setData(type==='plan'?{plan:'PGC-NIRF',source_version:list.reference.source_version}:type==='account'?{origin:'enterprise',movable:false}:{});setPrevious(null);setReplaces('');setFrom('');setUntil('');setReason('');}
function revise(row){setKind(row.kind);setData({...row.data});setPrevious(row);setReplaces('');setFrom('');setUntil('');setReason('');}
if(!can('catalogue_accounting.view'))return <section className="panel"><h1>Acesso não autorizado</h1></section>;
if(!list)return <section className="panel"><p>{error||'A carregar classificação…'}</p></section>;
const rows=list.proposals, approved=type=>rows.filter(r=>r.kind===type&&r.state==='approved');
const defaults=kind==='plan'?{plan:'PGC-NIRF',source_version:list.reference.source_version}:kind==='account'?{plan_id:'',code:'',name:'',origin:'enterprise',movable:false,evidence:''}:kind==='category'?{name:'',account_id:'',conditions:''}:kind==='service_link'?{service_id:'',category_id:''}:{service_id:'',account_id:'',conditions:''};
const fields={...defaults,...data};
const title=row=>`${kinds[row.kind]} #${row.id} · v${row.version} · ${(row.kind==='account'?row.data.code+' — '+row.data.name:row.data.name)||row.data.code||row.data.plan||`Serviço #${row.data.service_id}`}`;
const displayField=(field,value)=>{if(typeof value==='boolean')return value?'Sim':'Não';if(field==='origin')return value==='enterprise'?'Desdobramento empresarial':'Referência oficial agregadora';if(field==='service_id')return list.services.find(r=>r.id===value)?.name||'Serviço #'+value;if(field.endsWith('_id')){const ref=rows.find(r=>r.id===value);return ref?title(ref):'Referência #'+value;}return String(value);};
const refOptions=field=>field==='service_id'?list.services.map(s=>({value:s.id,label:`${s.name} · v${s.version}`})):approved({plan_id:'plan',account_id:'account',category_id:'category'}[field]).map(r=>({value:r.id,label:title(r)}));
async function consult(){await perform(async()=>setResolution(await api(`${prefix}/services/${service}/resolve${date?`?reference_date=${date}`:''}`)),'');}
async function loadOperations(){await perform(async()=>{const result=await api(`/api/a/${area}`);setOperations(result.records);setOperation('');setHistory([]);},'');}
async function readHistory(){await perform(async()=>setHistory((await api(`${prefix}/history/${area}/${operation}`)).records),'');}
async function captureResolution(){const record=operations.find(r=>String(r.id)===operation);await perform(async()=>{await api(prefix+'/resolutions','POST',{area,record_id:record.id,record_version:record.version,service_id:Number(service),reference_date:date,resolution_revision:resolution.revision},context.token,crypto.randomUUID());await load();setHistory((await api(`${prefix}/history/${area}/${operation}`)).records);},'Resolução consultada preservada; sem lançamentos.');}
return <>
<section className="panel"><p className="eyebrow">CATÁLOGO DA EMPRESA · PGC-NIRF</p><h1>Classificação contabilística</h1><p>{list.notice}</p><p>Categoria como regra-base; só uma excepção aprovada, vigente e compatível a substitui. Correcções e mudanças de categoria exigem nova versão e validação.</p>{error&&<p role="alert">{error}</p>}<h2>Referências documentais disponíveis</h2><p>{list.reference.source_version}</p><p className="help">{list.reference.status}</p><ul>{list.reference.accounts.map(a=><li key={a.code}>{a.code} — {a.name} · referência agregadora, sem movimento</li>)}</ul><p className="help">Não há conta de movimento empresarial criada por omissão. Quem prepara propõe o desdobramento documentado; quem valida decide.</p></section>
{can('catalogue_accounting.propose')&&<section className="panel"><h2>{previous?`Nova versão da proposta #${previous.id}`:'Preparar proposta'}</h2><form onSubmit={async e=>{e.preventDefault();await act('/proposals',{kind,data:Object.fromEntries(Object.entries(fields).map(([k,v])=>[k,k.endsWith('_id')?Number(v):v])),valid_from:from,valid_until:until||null,reason,...(previous?{previous_id:previous.id}:{}),...(replaces?{replaces_id:Number(replaces)}:{})});}}>
<label>Tipo de proposta<select disabled={!!previous} value={kind} onChange={e=>fresh(e.target.value)}>{Object.entries(kinds).map(([k,v])=><option key={k} value={k}>{v}</option>)}</select></label>
{Object.entries(fields).map(([field,value])=><label key={field}>{labels[field]}{field==='movable'?<input type="checkbox" checked={!!value} onChange={e=>setData({...fields,[field]:e.target.checked})}/>:field.endsWith('_id')?<select required value={value} onChange={e=>setData({...fields,[field]:e.target.value})}><option value="">Seleccionar referência</option>{refOptions(field).map(o=><option key={o.value} value={o.value}>{o.label}</option>)}</select>:field==='origin'?<select value={value} onChange={e=>setData({...fields,[field]:e.target.value})}><option value="enterprise">Desdobramento empresarial proposto</option><option value="reference">Referência oficial agregadora 7.2</option></select>:['conditions','evidence'].includes(field)?<textarea required maxLength={2000} value={value} onChange={e=>setData({...fields,[field]:e.target.value})}/>:<input required readOnly={kind==='plan'||(!!previous&&field==='code')} maxLength={255} value={value} onChange={e=>setData({...fields,[field]:e.target.value})}/>}</label>)}
<div className="form-grid"><label>Vigente desde<input required type="date" value={from} onChange={e=>setFrom(e.target.value)}/></label><label>Vigente até (inclusive; opcional)<input type="date" value={until} onChange={e=>setUntil(e.target.value)}/></label></div><p className="help">Datas de classificação, não de reconhecimento ou exigibilidade de IVA. Nenhuma data é presumida.</p>
{previous&&<label>Versão aprovada explicitamente substituída<select value={replaces} onChange={e=>setReplaces(e.target.value)}><option value="">Nenhuma — sem sobreposição</option>{rows.filter(r=>r.kind===previous.kind&&r.entity_key===previous.entity_key&&r.state==='approved').map(r=><option key={r.id} value={r.id}>{title(r)}</option>)}</select></label>}
<label>Motivo e fundamento<textarea required maxLength={2000} value={reason} onChange={e=>setReason(e.target.value)}/></label><div className="actions"><button className="primary" disabled={busy}>Guardar proposta em preparação</button>{previous&&<button type="button" onClick={()=>fresh(kind)}>Cancelar correcção</button>}</div><p className="help">Guardar não submete nem aprova. Uma versão guardada é imutável; correcções criam outra versão.</p></form></section>}
<section className="panel"><h2>Propostas, submissões e decisões</h2><button disabled={busy} onClick={()=>perform(load,'')}>Actualizar lista</button>{!rows.length&&<p>Nenhuma proposta nesta empresa.</p>}{[...rows].reverse().map(row=><article className="accounting-proposal" key={row.id}><h3>{title(row)}</h3><p><strong>{states[row.state]}</strong> · {row.valid_from} até {row.valid_until||'sem termo definido'} · Autor #{row.actor_id}</p><dl>{Object.entries(row.data).map(([k,v])=><div key={k}><dt>{labels[k]||k}</dt><dd>{displayField(k,v)}</dd></div>)}</dl><p>Fundamento: {row.reason}</p>{row.replaces_id&&<p>Substitui explicitamente a versão aprovada #{row.replaces_id}, a partir do início da nova vigência.</p>}{row.decision&&<p>Decisão por #{row.decision.actor_id}: {row.decision.reason}</p>}
{can('catalogue_accounting.propose')&&<div className="actions">{row.state==='draft'&&<button disabled={busy} onClick={()=>act(`/proposals/${row.id}/submit`,{})}>Submeter versão #{row.id}</button>}{!rows.some(other=>other.kind===row.kind&&other.entity_key===row.entity_key&&other.version>row.version)&&<button disabled={busy} onClick={()=>revise(row)}>Preparar nova versão de #{row.id}</button>}</div>}<Decision row={row} can={can} busy={busy} act={act}/></article>)}</section>
<section className="panel"><h2>Consultar classificação do serviço</h2><label>Serviço<select value={service} onChange={e=>{setService(e.target.value);setResolution(null);}}><option value="">Seleccionar serviço existente</option>{list.services.map(s=><option value={s.id} key={s.id}>{s.name}</option>)}</select></label><label>Data de referência<input type="date" value={date} onChange={e=>{setDate(e.target.value);setResolution(null);}}/></label><button disabled={busy||!service} onClick={consult}>Consultar classificação</button>{resolution&&<div role="status"><h3>{resolution.state==='resolved_classification_only'?'Classificação resolvida — sem reconhecimento':'Classificação pendente'}</h3>{resolution.origin&&<p>Origem: {resolution.origin==='category_base'?'Regra-base da categoria':'Excepção explícita do serviço'}</p>}{resolution.references&&<ul>{Object.values(resolution.references).map(r=><li key={r.id}>{title(r)} · {r.valid_from} até {r.valid_until||'sem termo declarado'}</li>)}</ul>}<ul>{resolution.pending.map((p,i)=><li key={i}>{p}</li>)}</ul></div>}</section>
{context.unit&&<section className="panel"><h2>Classificação preservada por operação autorizada</h2><p>Operações apenas da unidade activa. Não altera a fotografia B1 ou os dados do rascunho.</p><label>Área<select value={area} onChange={e=>{setArea(e.target.value);setOperations([]);setOperation('');setHistory([]);}}><option value="service_drafts">Prestações em rascunho</option><option value="billing_drafts">Rascunhos de facturação</option></select></label><button disabled={busy} onClick={loadOperations}>Carregar operações autorizadas</button><label>Operação<select value={operation} onChange={e=>{setOperation(e.target.value);setHistory([]);}}><option value="">Seleccionar operação</option>{operations.map(r=><option key={r.id} value={r.id}>{r.name} · #{r.id} · v{r.version}</option>)}</select></label><div className="actions"><button disabled={busy||!operation} onClick={readHistory}>Consultar histórico preservado</button>{can('catalogue_accounting.propose')&&<button disabled={busy||!operation||!service||!date||resolution?.state!=='resolved_classification_only'} onClick={captureResolution}>Guardar resolução na operação</button>}</div>{history.map(r=><article key={r.id}><h3>Resolução histórica #{r.id} · operação v{r.record_version}</h3><p>Data de referência {r.reference_date} · autor #{r.actor_id} · serviço {r.snapshot.service.name} v{r.snapshot.service.version}</p><ul>{Object.values(r.snapshot.references).map(ref=><li key={ref.id}>{title(ref)}</li>)}</ul><p>{r.snapshot.notice}</p></article>)}</section>}
</>;
}
@@ -0,0 +1,54 @@
import React, { useEffect, useState } from 'react';
const names = { issuer: 'Dados do emitente', client: 'Dados dos clientes', payment: 'Instruções de pagamento', service_drafts: 'Preparação de prestações', billing_drafts: 'Preparação de facturação' };
const profileFields = { nuit: 'NUIT', address: 'Morada', locality: 'Localidade', country: 'País' };
const paymentFields = { label: 'Designação das instruções', bank: 'Banco (se aplicável)', holder: 'Titular (se aplicável)', account_number: 'Identificador de conta (se aplicável)', nib: 'NIB (se aplicável)', iban: 'IBAN (se aplicável)', swift: 'SWIFT (se aplicável)', bank_code: 'Código bancário (se aplicável)', currency: 'Moeda das instruções (opcional)' };
const states = { missing: 'Por completar / confirmar aplicabilidade', filled_not_validated: 'Preenchido — não validado', validation_pending: 'Validação pendente', stale: 'Origem alterada — rever' };
const isDraft = kind => kind.endsWith('_drafts');
export default function DocumentPreparationWorkspace({ context, can, api, perform, busy }) {
const initial = can('issuer_documents.view') ? 'issuer' : can('client_documents.view') ? 'client' : 'billing_drafts';
const [kind, setKind] = useState(initial);
return <DocumentPreparationArea key={kind} kind={kind} onSelect={setKind} context={context} can={can} api={api} perform={perform} busy={busy} />;
}
function DocumentPreparationArea({ kind, onSelect, context, can, api, perform, busy }) {
const prefix = kind => kind === 'client' ? 'client_documents' : isDraft(kind) ? 'draft_documents' : 'issuer_documents';
const permitted = (kind, action) => can(`${prefix(kind)}.${action}`, isDraft(kind) ? context.unit?.id ?? null : null);
const options = Object.keys(names).filter(kind => permitted(kind, 'view') && (!isDraft(kind) || context.unit));
const [records, setRecords] = useState([]); const [selectedId, setSelectedId] = useState(null);
const [record, setRecord] = useState(null); const [form, setForm] = useState({}); const [payments, setPayments] = useState([]); const [error, setError] = useState('');
const path = `/api/document-preparation/${kind}`;
async function load() { const result = await api(path); setRecords(result.records); }
async function open(id) {
setError('');
try {
const result = await api(`${path}/${id}`); setRecord(result); setSelectedId(id);
setForm({ ...result.data, ...(isDraft(kind) ? { currency: result.currency || '', price_mode: result.data.price_mode || 'undefined', units: result.lines.map((_, i) => result.data.units?.[i] || ''), payment_instruction_ids: result.data.payment_instruction_ids || [] } : {}) });
} catch (e) { setError(e.message); }
}
useEffect(() => { setRecord(null); setSelectedId(null); setError(''); load().then(() => { if (kind === 'issuer') open(context.company.id); }).catch(e => setError(e.message)); if (isDraft(kind) && can('issuer_documents.view')) api('/api/document-preparation/payment').then(data => setPayments(data.records)).catch(e => setError(e.message)); }, [kind, context.token]);
function change(field, value) { setForm(current => ({ ...current, [field]: value })); }
async function save(e) {
e.preventDefault();
const fields = kind === 'payment' ? [...Object.keys(paymentFields), 'instructions'] : isDraft(kind) ? ['service_date','period_start','period_end','payment_terms','proposed_due_date','price_mode','units','payment_instruction_ids','currency'] : Object.keys(profileFields);
const data = Object.fromEntries(fields.map(field => [field, form[field] === undefined || form[field] === '' ? null : form[field]]));
data.version = record?.version || 0;
if (isDraft(kind)) data.draft_version = record.draft_version;
await perform(async () => { const result = await api(`${path}/${selectedId ?? 0}`, 'PATCH', data, context.token); await load(); await open(result.id); }, 'Preparação guardada com histórico. Preenchido não significa validado.');
}
const editable = permitted(kind, 'edit') && !record?.locked;
const text = (field, label, type = 'text') => <label key={field}>{label}<input type={type} value={form[field] || ''} readOnly={!editable} maxLength={500} onChange={e => change(field, e.target.value)} /></label>;
return <><div className="page-title"><p className="eyebrow">PREPARAÇÃO DOCUMENTAL</p><h1>Dados para facturação</h1><p>Completar e reutilizar informação. Sem emissão fiscal, recebimentos ou lançamentos.</p></div><div className="actions">{options.map(option => <button key={option} className={kind === option ? 'primary' : ''} onClick={() => onSelect(option)}>{names[option]}</button>)}</div>
{error && <div className="alert error" role="alert">{error}</div>}
{!options.length ? <section className="panel"><p>Sem acesso documental explícito neste contexto.</p></section> : <div className="a-grid"><section className="panel a-list"><h2>{names[kind]}</h2>{kind === 'payment' && editable && <button className="primary" onClick={() => { setSelectedId(0); setRecord({ version: 0, data: {}, review: [] }); setForm({}); }}>Novas instruções</button>}{!records.length && <p>Ainda não existem registos. Clientes e rascunhos são criados nos menus existentes.</p>}{records.map(item => <button key={item.id} onClick={() => open(item.id)}>{item.name || item.data?.label || `#${item.id}`}</button>)}</section>
<section className="panel a-detail">{record ? <><h2>{record.name || names[kind]}</h2><p className="help">{record.origin || 'Instruções do emitente'} · Versão {record.version}</p>{record.locked && <p className="alert">Confirmação B1 preservada. Esta preparação está bloqueada para edição.</p>}<form onSubmit={save}>
{['issuer','client'].includes(kind) && <><label>Denominação reutilizada do cadastro<input value={record.name || ''} readOnly /></label><p className="help">O nome não é duplicado. Para o alterar, use Empresa ou Clientes, conforme os seus direitos. O cliente pode continuar a ser criado apenas com nome.</p><div className="form-grid">{Object.entries(profileFields).map(([field,label]) => text(field,label))}</div></>}
{kind === 'payment' && <><p>Configuração do emitente para facilitar o pagamento. Não é uma pergunta ao cliente sobre como pagará e não cria qualquer recebimento.</p><p className="help">Não introduza palavras-passe, tokens, PIN, códigos de segurança ou dados de cartões. Dados introduzidos pelo utilizador, sem verificação bancária.</p><div className="form-grid">{Object.entries(paymentFields).map(([field,label]) => text(field,label))}</div><label>Texto de instruções<textarea value={form.instructions || ''} readOnly={!editable} maxLength={2000} onChange={e => change('instructions',e.target.value)} /></label></>}
{isDraft(kind) && <><p className="help">Versão actual do rascunho: {record.draft_version}. Valores e linhas existentes são reutilizados; este ecrã não recalcula impostos.</p>{text('currency','Moeda do rascunho (opcional)')}<label>Modo de preço<select disabled={!editable} value={form.price_mode} onChange={e => change('price_mode',e.target.value)}><option value="undefined">Por definir</option><option value="included">Imposto incluído</option><option value="added">Imposto acrescido</option></select></label><p className="help">Esta escolha prepara uma intenção; não muda os preços existentes, a base aritmética ou os impostos.</p><div className="form-grid">{text('service_date','Data do serviço (opcional)','date')}{text('period_start','Início do período (opcional)','date')}{text('period_end','Fim do período (opcional)','date')}{text('proposed_due_date','Vencimento proposto (não é emissão)','date')}</div><label>Condições de pagamento<textarea value={form.payment_terms || ''} readOnly={!editable} maxLength={1000} onChange={e => change('payment_terms',e.target.value)} /></label><p className="help">Confira prazo e vencimento. A compatibilidade fica pendente de validação; nenhuma data é corrigida automaticamente.</p>{record.lines.map((line,i) => <label key={i}>Unidade de medida — linha {i + 1}: {line.service_name}<input value={form.units?.[i] || ''} readOnly={!editable} maxLength={80} onChange={e => change('units',form.units.map((unit,index) => index === i ? e.target.value : unit))} /></label>)}<h3>Instruções do emitente aplicáveis a este rascunho</h3><p>Seleccione instruções já configuradas, sem redigitação. A selecção não regista um pagamento.</p>{!payments.length && <p className="help">Não há instruções disponíveis, ou falta acesso explícito ao emitente.</p>}{payments.map(payment => <label className="doc-instruction-option" key={payment.id}><input type="checkbox" disabled={!editable} checked={form.payment_instruction_ids.includes(payment.id)} onChange={e => change('payment_instruction_ids',e.target.checked ? [...form.payment_instruction_ids,payment.id] : form.payment_instruction_ids.filter(id => id !== payment.id))} />{payment.data.label} · versão {payment.version} · não verificado pelo banco</label>)}</>}
{editable && <button className="primary" disabled={busy}>Guardar preparação</button>}{!permitted(kind,'edit') && <p>O seu acesso permite apenas consulta.</p>}
</form><section className="a-pending"><h3>Completude e validações</h3><p>Sem validação fiscal. Dados preenchidos não significam aptidão para emissão.</p><ul>{record.review.map((check,i) => <li key={i}><strong>{check.label}: {states[check.state]}</strong><p className="help">Origem: {check.origin}. Próxima acção: {check.next}</p></li>)}</ul>{Object.entries(record.related_profiles || {}).map(([profile,value]) => <details key={profile}><summary>{profile === 'issuer' ? 'Emitente' : 'Cliente'}: {value.name} · versão documental {value.version} · reutilizado do cadastro</summary><ul>{value.review.map((check,i) => <li key={i}>{check.label}: {states[check.state]} — {check.next}</li>)}</ul></details>)}</section></> : <p>Seleccione um registo para consultar ou preparar os dados existentes.</p>}</section></div>}
</>;
}
+68
View File
@@ -0,0 +1,68 @@
import React, { useEffect, useState } from 'react';
const notice = 'RASCUNHO — SEM VALIDADE FISCAL';
const missing = historical => historical ? 'Não capturado nesta confirmação' : 'Por completar';
const present = (value, historical) => value === null || value === undefined || value === '' ? missing(historical) : value;
const modes = { undefined: 'Por definir', included: 'Imposto incluído — indicação proposta', added: 'Imposto acrescido — indicação proposta' };
export default function DocumentPreview({ id, historical, context, api, onClose }) {
const [view, setView] = useState(null); const [error, setError] = useState(''); const [stale, setStale] = useState(false); const [loading, setLoading] = useState(true);
const path = `/api/document-preview/${historical ? 'history' : 'draft'}/${id}`;
async function update() {
setLoading(true); setError('');
try { const result = await api(path); setView(result); setStale(false); }
catch (e) { setView(null); setError(e.message); }
finally { setLoading(false); }
}
useEffect(() => {
let disposed = false;
setView(null); setLoading(true);
api(path).then(result => { if (!disposed) { setView(result); setLoading(false); } }).catch(e => { if (!disposed) { setError(e.message); setLoading(false); } });
return () => { disposed = true; };
}, [id, historical, context.token]);
useEffect(() => {
if (!view) return;
let disposed = false; let checking = false;
async function check() {
if (document.hidden || checking) return; checking = true;
try { const result = await api(`${path}/revision`); if (!disposed && result.revision !== view.revision) setStale(true); }
catch (e) { if (!disposed) { setView(null); setError(e.message); } }
finally { checking = false; }
}
const timer = setInterval(check, 10000); window.addEventListener('focus',check); document.addEventListener('visibilitychange',check);
return () => { disposed = true; clearInterval(timer); window.removeEventListener('focus',check); document.removeEventListener('visibilitychange',check); };
}, [view?.revision, context.token]);
return <div className="preview-shell"><div className="preview-mandatory" role="note">{notice}</div><div className="preview-controls"><button onClick={onClose}>Voltar ao registo</button><button disabled={loading} onClick={update}>Actualizar pré-visualização</button><p className="help">Consulta sem emissão ou alteração de dados. Verificação de actualidade a cada 10 segundos e ao regressar à janela.</p></div>
{error && <div className="alert error" role="alert">{error}</div>}{loading && <p role="status">A preparar a consulta…</p>}
{stale && <div className="preview-stale" role="alert"><strong>Pré-visualização desactualizada.</strong> Os dados abaixo mantêm a versão consultada. Use Actualizar pré-visualização para carregar explicitamente a nova versão.</div>}
{view && <article className="preview-sheet" aria-label={historical ? 'Confirmação interna histórica' : 'Rascunho de factura de serviço'}><div className="preview-document-heading"><div>{view.logo && <img src={view.logo} alt="Logótipo local do emitente" className="preview-logo" />}<p className="eyebrow">{historical ? 'CONSULTA DA FOTOGRAFIA PRESERVADA' : 'PRÉ-VISUALIZAÇÃO DOCUMENTAL'}</p><h1>{view.title}</h1></div><p>{notice}</p></div>
<p><strong>{view.reference}</strong> · Versão do rascunho {view.draft_version}{historical ? ` · Fotografia esquema ${view.snapshot_schema}` : ` · Preparação ${view.preparation_version} · Apresentação ${view.model_version}`}</p>
<p className="help">Instante desta consulta: {view.consulted_at}. Não é data de emissão.</p>
{historical && <p className="preview-historical">Confirmação interna histórica de {view.confirmed_at}, autor #{view.actor_id}. Conteúdo exclusivamente da fotografia; campos ausentes não foram reconstruídos com dados actuais.</p>}
<div className="preview-parties">{[['Emitente',view.issuer],['Cliente',view.client]].map(([label,party]) => <section key={label}><h2>{label}</h2><strong>{present(party.name,historical)}</strong><p>NUIT: {present(party.data.nuit,historical)}</p><p>{present(party.data.address,historical)}<br />{present(party.data.locality,historical)} · {present(party.data.country,historical)}</p></section>)}</div>
{view.contacts && <section><h2>Contactos do emitente</h2><p className="preview-text">{view.contacts}</p></section>}
<section><h2>Serviço e condições propostas</h2><div className="preview-dates"><p>Data do serviço: <strong>{present(view.prepared.service_date,historical)}</strong></p><p>Período: <strong>{present(view.prepared.period_start,historical)} — {present(view.prepared.period_end,historical)}</strong></p><p>Vencimento proposto: <strong>{present(view.prepared.proposed_due_date,historical)}</strong></p></div><p className="preview-text">Condições: {present(view.prepared.payment_terms,historical)}</p><p>Moeda: <strong>{view.data.currency || 'Moeda por definir'}</strong> · {modes[view.prepared.price_mode || 'undefined']}</p><p className="help">Origem: prestação #{view.source_id}, versão {view.data.source_version ?? 'não capturada'}. Os dados não confirmam execução do serviço.</p></section>
<section><h2>Linhas do rascunho</h2><div className="preview-lines">{view.data.lines.map((line,i) => <div className="preview-line" key={i}><div><strong>{i + 1}. {line.service_name}</strong>{line.description && <p className="preview-text">{line.description}</p>}</div><p>Quantidade<br /><strong>{present(line.quantity,historical)}</strong></p><p>Unidade<br /><strong>{present(view.prepared.units?.[i],historical)}</strong></p><p>Preço proposto<br /><strong>{present(line.unit_price,historical)}</strong></p></div>)}</div></section>
<section className="preview-values"><p>Valor aritmético provisório <strong>{view.base_amount ?? 'Por completar'} {view.data.currency || '(moeda por definir)'}</strong></p><p>Imposto por apurar <strong>Não apurado</strong></p><p>Total definitivo por apurar <strong>Não apurado</strong></p><p className="help">Valores exactos propostos, sem cálculo fiscal, novas taxas ou arredondamentos. Não representam base tributável ou montante monetário definitivo. Um preço com imposto incluído desconhecido não foi decomposto.</p></section>
{!historical && view.calculation && <section aria-label="Cálculo de serviço"><h2>Cálculo de serviço</h2>{view.calculation.pending.map((text,i)=><p key={i}>{text}</p>)}
{view.calculation.environment === 'synthetic_test_only' && <><strong>ENSAIO SINTÉTICO — APENAS TESTES</strong><p>{view.calculation.policy} · Truncagem operacional proposta; não é obrigação legal comprovada.</p>
{view.calculation.can_calculate && view.calculation.profiles.map(profile=><button key={profile.id} disabled={loading || stale} onClick={async()=>{setLoading(true);try { await api(`/api/service-calculations/${id}`,'POST',{draft_version:view.calculation.draft_version,preparation_version:view.calculation.preparation_version,profile_id:profile.id,profile_version:profile.version}); await update(); } catch(e) {setError(e.message);setLoading(false);} }}>Calcular ensaio · {profile.identifier} v{profile.version}</button>)}
{view.calculation.history.map(entry=><div key={entry.id} className="preview-values"><h3>Resultado de ensaio #{entry.id} — {entry.current ? 'Actual para estas versões' : 'Desactualizado — apenas histórico'}</h3><p>Rascunho v{entry.snapshot.input.draft_version} · Preparação v{entry.snapshot.input.preparation_version} · Perfil {entry.snapshot.input.profile_identifier} v{entry.snapshot.input.profile_version}</p>
<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>}
{!!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>
</article>}
</div>;
}
export function DocumentPreviewConfiguration({ api, perform, can, busy, context }) {
const [model,setModel] = useState(null); const [form,setForm] = useState(null); const [error,setError] = useState('');
async function load() { const result = await api('/api/document-preview/model'); setModel(result); setForm(result.data); }
useEffect(() => { load().catch(e => setError(e.message)); }, [context.token]);
const editable = can('document_preview.configure');
return <section className="panel"><p className="eyebrow">APRESENTAÇÃO POR EMPRESA</p><h1>Apresentação do rascunho</h1><p>Configuração delimitada; o aviso de rascunho e todas as pendências são obrigatórios.</p>{error && <p role="alert">{error}</p>}{model && <form onSubmit={e => { e.preventDefault(); perform(async () => { await api('/api/document-preview/model','PATCH',{ version:model.version,...form }); await load(); }, 'Apresentação guardada; não altera documentos ou fotografias B1.'); }}><p>Versão {model.version} · {model.local_logo_available ? 'Logótipo local disponível' : 'Logótipo não disponível: falta recurso local autorizado'}</p>{Object.entries({show_logo:'Apresentar logótipo local da empresa',show_contacts:'Apresentar contactos',show_observations:'Apresentar observações',show_payment_instructions:'Apresentar instruções de pagamento seleccionadas no rascunho'}).map(([field,label]) => <label className="doc-instruction-option" key={field}><input type="checkbox" disabled={!editable} checked={form[field]} onChange={e => setForm({...form,[field]:e.target.checked})} />{label}</label>)}<label>Contactos de apresentação<textarea readOnly={!editable} value={form.contact_text} maxLength={2000} onChange={e => setForm({...form,contact_text:e.target.value})} /></label><label>Observações de apresentação<textarea readOnly={!editable} value={form.observations} maxLength={2000} onChange={e => setForm({...form,observations:e.target.value})} /></label><p className="help">Apenas texto, sem HTML, recursos externos ou segredos. A escolha de instruções é reutilizada da preparação documental; não é uma pergunta sobre como o cliente pagará.</p>{editable ? <button className="primary" disabled={busy}>Guardar apresentação</button> : <p>Apenas consulta; não tem configuração concedida.</p>}</form>}</section>;
}
+4
View File
@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import DocumentPreview from './DocumentPreview.jsx';
export const serviceAreas = { clients: 'Clientes', services: 'Serviços', service_drafts: 'Prestações em rascunho', billing_drafts: 'Rascunhos de facturação' };
const actions = { view: 'Consultar', create: 'Criar', edit: 'Editar', manage: 'Gerir concessões' };
@@ -16,6 +17,7 @@ export default function ServicesWorkspace({ area, context, can, api, perform, bu
const [key, setKey] = useState(() => crypto.randomUUID());
const [access, setAccess] = useState(false);
const [reviewConfirmation, setReviewConfirmation] = useState(false);
const [previewMode, setPreviewMode] = useState(null);
const path = `/api/a/${area}`;
async function load() { const result = await api(path); setRecords(result.records); }
@@ -67,6 +69,7 @@ export default function ServicesWorkspace({ area, context, can, api, perform, bu
function changeLine(index, field, value) { setForm({ ...form, lines: form.lines.map((line, n) => n === index ? { ...line, [field]: value } : line) }); }
if (!allowed('view')) return <section className="panel"><h1>{serviceAreas[area]}</h1><p>Sem consulta autorizada neste contexto.</p></section>;
if (previewMode && selected) return <DocumentPreview id={selected.id} historical={previewMode === 'history'} context={context} api={api} onClose={() => setPreviewMode(null)} />;
return <>
<div className="a-heading"><div><p className="eyebrow">SERVIÇOS · PREPARAÇÃO E CONFIRMAÇÃO INTERNA</p><h1>{serviceAreas[area]}</h1><p className="muted">{isDraft(area) ? `Unidade: ${context.unit?.name || 'não seleccionada'}` : 'Cadastro partilhado pelas unidades da empresa'}</p></div><div className="actions">{allowed('create') && <button className="primary" disabled={busy} onClick={() => start()}>Novo registo</button>}{allowed('manage') && <button onClick={() => { setAccess(!access); setForm(null); }}>Gerir acessos desta área</button>}</div></div>
{error && <div role="alert" className="alert error">{error}</div>}
@@ -91,6 +94,7 @@ export default function ServicesWorkspace({ area, context, can, api, perform, bu
{area === 'billing_drafts' && allowed('edit') && !selected.confirmation && <button disabled={busy} onClick={() => perform(async () => { await api(`${path}/${selected.id}`, 'PATCH', { version: selected.version, reload_source: true }, context.token, key); await open(selected.id); await load(); setKey(crypto.randomUUID()); }, 'Dados da prestação reutilizados explicitamente numa nova versão do rascunho.')}>Actualizar explicitamente da prestação</button>}
{area === 'billing_drafts' && !selected.confirmation && can('billing_drafts.confirm_internal', context.unit?.id) && <div className="a-pending"><h3>Confirmação interna</h3><p>Fixa esta versão e os dados que a sustentam. Bloqueia a edição deste rascunho. Não confirma a execução do serviço, não emite factura nem reconhece receita.</p>{reviewConfirmation ? <><p>Confirmar a versão {selected.version} neste contexto: {context.company.name} / {context.unit?.name}? Quantidades e preços devem estar completos; moeda e regras desconhecidas ficam pendentes na fotografia.</p><div className="actions"><button className="primary" disabled={busy} onClick={() => perform(async () => { await api(`/api/b1/billing-drafts/${selected.id}/confirm-internal`, 'POST', { version: selected.version }, context.token, key); await open(selected.id); await load(); setKey(crypto.randomUUID()); }, 'Confirmado internamente — sem validade fiscal')}>Confirmar internamente esta versão</button><button onClick={() => setReviewConfirmation(false)}>Voltar ao rascunho</button></div></> : <button disabled={busy} onClick={() => { setKey(crypto.randomUUID()); setReviewConfirmation(true); }}>Rever confirmação interna</button>}</div>}
{selected.confirmation && <InternalDocument confirmation={selected.confirmation} />}
{area === 'billing_drafts' && (selected.confirmation ? <button onClick={() => setPreviewMode('history')}>Abrir confirmação interna histórica</button> : can('issuer_documents.view') && can('client_documents.view') && can('draft_documents.view',context.unit?.id) && <button className="primary" onClick={() => setPreviewMode('draft')}>Pré-visualizar rascunho</button>)}
</>}
<details className="a-history"><summary>Histórico e captura durável ({selected.history?.length || 0} versões)</summary>{selected.history?.map(entry => <div key={entry.id}><strong>Versão {entry.version}</strong><p className="help">{entry.created_at} · Autor #{entry.actor_id} · Evento #{entry.event?.id} · Capturado, sem processamento contabilístico</p><details><summary>Dados preservados nesta versão</summary><pre>{JSON.stringify(entry.snapshot, null, 2)}</pre></details></div>)}</details>
</> : <><h2>Seleccione um registo</h2><p>Os dados e o histórico aparecem aqui. As operações disponíveis dependem das suas concessões explícitas.</p></>}
+8 -2
View File
@@ -2,8 +2,11 @@ import React, { useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import '../css/app.css';
import ServicesWorkspace, { serviceAreas } from './ServicesWorkspace.jsx';
import DocumentPreparationWorkspace from './DocumentPreparationWorkspace.jsx';
import { DocumentPreviewConfiguration } from './DocumentPreview.jsx';
import CatalogueAccountingWorkspace from './CatalogueAccountingWorkspace.jsx';
const labels = { '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' };
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' };
function App() {
const [state, setState] = useState(null);
@@ -46,7 +49,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']] : []), ...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('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}>
@@ -54,6 +57,9 @@ function App() {
{page === 'company' && <section className="panel"><p className="eyebrow">DADOS BÁSICOS</p><h1>Empresa</h1><NameForm key={context.company.id} label="Nome da empresa" initial={context.company.name} editable={can('company.edit')} busy={busy} onSave={name => perform(() => api(`/api/companies/${context.company.id}`, 'PATCH', { name }, context.token), 'Nome da empresa actualizado.')} />{!can('company.edit') && <p className="help">O seu acesso permite consulta. A edição exige uma concessão própria.</p>}</section>}
{page === 'units' && <><section className="panel"><p className="eyebrow">DADOS BÁSICOS</p><h1>Unidades autorizadas</h1>{!context.units.length && <p>Não tem consulta concedida a nenhuma unidade desta empresa.</p>}<ul className="unit-list">{context.units.map(u => <li key={u.id}><strong>{u.name}</strong><button disabled={busy || context.unit?.id === u.id} onClick={() => select(context.company.id, u.id)}>{context.unit?.id === u.id ? 'Seleccionada' : 'Seleccionar'}</button></li>)}</ul>{context.unit && <NameForm label="Nome da unidade" initial={context.unit.name} editable={can('unit.edit', context.unit.id)} busy={busy} onSave={name => perform(() => api(`/api/units/${context.unit.id}`, 'PATCH', { name }, context.token), 'Nome da unidade actualizado.')} />}</section>{can('unit.create') && <section className="panel"><h2>Criar unidade</h2><p className="help">A unidade será criada nesta empresa. Receberá consulta e edição explícitas apenas para a nova unidade.</p><NameForm label="Nome da nova unidade" initial="" editable busy={busy} button="Criar unidade" onSave={name => perform(() => api('/api/units', 'POST', { name }, context.token), 'Unidade criada com os seus acessos explícitos.')} /></section>}</>}
{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 === '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>}
</main><footer>SGF TEMPO · Rascunhos e confirmação interna · Sem emissão fiscal ou lançamentos</footer></div></div>;