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
{notice}

Consulta sem emissão ou alteração de dados. Verificação de actualidade a cada 10 segundos e ao regressar à janela.

{error &&
{error}
}{loading &&

A preparar a consulta…

} {stale &&
Pré-visualização desactualizada. Os dados abaixo mantêm a versão consultada. Use Actualizar pré-visualização para carregar explicitamente a nova versão.
} {view &&
{view.logo && Logótipo local do emitente}

{historical ? 'CONSULTA DA FOTOGRAFIA PRESERVADA' : 'PRÉ-VISUALIZAÇÃO DOCUMENTAL'}

{view.title}

{notice}

{view.reference} · Versão do rascunho {view.draft_version}{historical ? ` · Fotografia esquema ${view.snapshot_schema}` : ` · Preparação ${view.preparation_version} · Apresentação ${view.model_version}`}

Instante desta consulta: {view.consulted_at}. Não é data de emissão.

{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.

}
{[['Emitente',view.issuer],['Cliente',view.client]].map(([label,party]) =>

{label}

{present(party.name,historical)}

NUIT: {present(party.data.nuit,historical)}

{present(party.data.address,historical)}
{present(party.data.locality,historical)} · {present(party.data.country,historical)}

)}
{view.contacts &&

Contactos do emitente

{view.contacts}

}

Serviço e condições propostas

Data do serviço: {present(view.prepared.service_date,historical)}

Período: {present(view.prepared.period_start,historical)} — {present(view.prepared.period_end,historical)}

Vencimento proposto: {present(view.prepared.proposed_due_date,historical)}

Condições: {present(view.prepared.payment_terms,historical)}

Moeda: {view.data.currency || 'Moeda por definir'} · {modes[view.prepared.price_mode || 'undefined']}

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.

Linhas do rascunho

{view.data.lines.map((line,i) =>
{i + 1}. {line.service_name}{line.description &&

{line.description}

}

Quantidade
{present(line.quantity,historical)}

Unidade
{present(view.prepared.units?.[i],historical)}

Preço proposto
{present(line.unit_price,historical)}

)}

Valor aritmético provisório {view.base_amount ?? 'Por completar'} {view.data.currency || '(moeda por definir)'}

Imposto por apurar Não apurado

Total definitivo por apurar Não apurado

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.

{!historical && view.calculation &&

Cálculo de serviço

{view.calculation.pending.map((text,i)=>

{text}

)} {view.calculation.environment === 'synthetic_test_only' && <>ENSAIO SINTÉTICO — APENAS TESTES

{view.calculation.policy} · Truncagem operacional proposta; não é obrigação legal comprovada.

{view.calculation.can_calculate && view.calculation.profiles.map(profile=>)} {view.calculation.history.map(entry=>

Resultado de ensaio #{entry.id} — {entry.current ? 'Actual para estas versões' : 'Desactualizado — apenas histórico'}

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}

Base aritmética exacta: {entry.snapshot.result.base_exact}

IVA exacto: {entry.snapshot.result.vat_exact}

IVA truncado proposto: {entry.snapshot.result.vat_document_proposed}

Diferença descartada: {entry.snapshot.result.discarded_difference}

Total documental proposto: {entry.snapshot.result.total_document_proposed ?? 'Pendente: representação da base/total'} {entry.snapshot.result.currency}

{entry.snapshot.result.pending.map((text,i)=>

{text}

)}

Sem validação fiscal. {notice}

)} }
} {!historical && view.fiscal_history?.length>0 &&

Cálculo proposto do serviço — condicionado

{view.fiscal_history.map(entry=>

Resultado #{entry.id} · {entry.current?'Actual para as versões validadas':'Desactualizado — histórico'}

Perfil {entry.snapshot.input.profile_identifier} v{entry.snapshot.input.profile_version} · decisão temporal v{entry.snapshot.input.temporal_version}

Base exacta: {entry.snapshot.result.base_exact} {entry.snapshot.result.currency}

IVA exacto: {entry.snapshot.result.vat_exact}

IVA truncado proposto: {entry.snapshot.result.vat_document_proposed}

Diferença preservada: {entry.snapshot.result.discarded_difference}

Total proposto: {entry.snapshot.result.total_document_proposed ?? 'Pendente'}

Exigibilidade: {entry.snapshot.result.exigibility_date ?? 'Não determinada'}

Período de apuramento: {entry.snapshot.result.assessment_period ?? 'Não determinado'}

{entry.snapshot.result.pending.map((p,i)=>

{p}

)}

{notice} · Não representa emissão ou apuramento líquido do imposto.

)}
} {!!view.payments.length &&

Instruções do emitente para facilitar o pagamento

Informação introduzida pelo utilizador, sem verificação bancária. Não regista recebimento nem inicia operação bancária.

{view.payments.map(payment =>

{payment.data.label} · versão {payment.version}

{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]) =>

{label}: {payment.data[key]}

)}{payment.data.instructions &&

{payment.data.instructions}

}
)}
} {view.observations &&

Observações

{view.observations}

}

Dados em falta e validações pendentes

Preenchido não significa validado. Imposto e total definitivo continuam por apurar.

    {Object.entries(view.pending).map(([code,text]) =>
  • {text}
  • )}

{notice}

}
; } 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

APRESENTAÇÃO POR EMPRESA

Apresentação do rascunho

Configuração delimitada; o aviso de rascunho e todas as pendências são obrigatórios.

{error &&

{error}

}{model &&
{ 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.'); }}>

Versão {model.version} · {model.local_logo_available ? 'Logótipo local disponível' : 'Logótipo não disponível: falta recurso local autorizado'}

{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]) => )}