Files
SGF/resources/js/DocumentPreparationWorkspace.jsx
T
nala f8d57b52aa 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.
2026-10-26 13:47:11 +02:00

55 lines
10 KiB
React

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