Preserva B1 aceite de confirmacao interna sem validade fiscal
This commit is contained in:
@@ -15,10 +15,11 @@ export default function ServicesWorkspace({ area, context, can, api, perform, bu
|
||||
const [error, setError] = useState('');
|
||||
const [key, setKey] = useState(() => crypto.randomUUID());
|
||||
const [access, setAccess] = useState(false);
|
||||
const [reviewConfirmation, setReviewConfirmation] = useState(false);
|
||||
const path = `/api/a/${area}`;
|
||||
|
||||
async function load() { const result = await api(path); setRecords(result.records); }
|
||||
async function open(id) { setError(''); try { setSelected(await api(`${path}/${id}`)); setForm(null); } catch (e) { setError(e.message); } }
|
||||
async function open(id) { setError(''); setReviewConfirmation(false); try { setSelected(await api(`${path}/${id}`)); setForm(null); } catch (e) { setError(e.message); } }
|
||||
useEffect(() => { load().catch(e => setError(e.message)); if (initialId) open(initialId); }, [area, context.token]);
|
||||
|
||||
async function start(record = null) {
|
||||
@@ -67,11 +68,11 @@ export default function ServicesWorkspace({ area, context, can, api, perform, bu
|
||||
|
||||
if (!allowed('view')) return <section className="panel"><h1>{serviceAreas[area]}</h1><p>Sem consulta autorizada neste contexto.</p></section>;
|
||||
return <>
|
||||
<div className="a-heading"><div><p className="eyebrow">ENTREGA A · PREPARAÇÃO</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>
|
||||
<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>}
|
||||
{isDraft(area) && <div className="a-draft-notice">Apenas rascunhos. Sem emissão fiscal, confirmação de serviço ou lançamentos contabilísticos.</div>}
|
||||
{isDraft(area) && <div className="a-draft-notice">{area === 'billing_drafts' ? 'Rascunhos e confirmação interna — sem validade fiscal. ' : 'Apenas rascunhos. '}Sem emissão fiscal, confirmação de serviço ou lançamentos contabilísticos.</div>}
|
||||
{access ? <AreaAccess area={area} context={context} api={api} perform={perform} busy={busy} /> : <>
|
||||
<div className="a-grid"><section className="panel a-list"><h2>Registos</h2>{!records.length && <p className="muted">Ainda não há registos neste âmbito.</p>}{records.map(record => <button key={record.id} className={selected?.id === record.id ? 'chosen' : ''} onClick={() => open(record.id)}><strong>{record.name}</strong><small>#{record.id} · Versão {record.version}{isDraft(area) ? ' · Rascunho' : ''}</small></button>)}</section><section className="panel a-detail">
|
||||
<div className="a-grid"><section className="panel a-list"><h2>Registos</h2>{!records.length && <p className="muted">Ainda não há registos neste âmbito.</p>}{records.map(record => <button key={record.id} className={selected?.id === record.id ? 'chosen' : ''} onClick={() => open(record.id)}><strong>{record.name}</strong><small>#{record.id} · Versão {record.version}{record.internal_confirmation ? ' · ' + record.internal_confirmation.label : isDraft(area) ? ' · Rascunho' : ''}</small></button>)}</section><section className="panel a-detail">
|
||||
{form ? <form onSubmit={save}><h2>{selected ? 'Editar' : 'Criar'} {isDraft(area) ? 'rascunho' : 'cadastro'}</h2><label>{area === 'clients' ? 'Nome do cliente' : area === 'services' ? 'Nome do serviço' : 'Designação do rascunho (opcional)'}<input value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} maxLength={255} required={!isDraft(area)} /></label>
|
||||
{area !== 'clients' && <label>Descrição (opcional)<textarea value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} maxLength={2000} /></label>}
|
||||
{area === 'billing_drafts' && !selected ? <><label>Prestação de origem<select value={form.source_id} onChange={e => setForm({ ...form, source_id: e.target.value })} required><option value="">Seleccionar prestação em rascunho</option>{references.source.map(record => <option key={record.id} value={record.id}>{record.name} · versão {record.version}</option>)}</select></label><p className="help">Cliente, linhas e moeda serão reutilizados da prestação, sem redigitação. Poderá editar o rascunho depois de o preparar.</p></> : isDraft(area) && <>
|
||||
@@ -82,12 +83,14 @@ export default function ServicesWorkspace({ area, context, can, api, perform, bu
|
||||
{area === 'service_drafts' && form.lines.length < 100 && <button type="button" onClick={() => setForm({ ...form, lines: [...form.lines, newLine()] })}>Adicionar linha</button>}
|
||||
</>}
|
||||
<div className="actions a-form-actions"><button className="primary" disabled={busy}>Guardar {isDraft(area) ? 'rascunho' : 'cadastro'}</button><button type="button" onClick={() => setForm(null)}>Cancelar edição</button></div>
|
||||
</form> : selected ? <><div className="a-heading"><h2>{selected.name}</h2>{allowed('edit') && <button disabled={busy} onClick={() => start(selected)}>Editar registo</button>}</div><p className="help">Identificador interno #{selected.id} · Versão {selected.version}{isDraft(area) ? ' · Estado: rascunho' : ''}</p>
|
||||
</form> : selected ? <><div className="a-heading"><h2>{selected.name}</h2>{allowed('edit') && !selected.confirmation && <button disabled={busy} onClick={() => start(selected)}>Editar registo</button>}</div><p className="help">Identificador interno #{selected.id} · Versão {selected.version}{isDraft(area) ? ' · Origem em rascunho' : ''}</p>
|
||||
{selected.data.description && <p>{selected.data.description}</p>}
|
||||
{isDraft(area) && <><p><strong>Cliente:</strong> {selected.data.client_name}</p>{selected.source_id && <p>Origem: prestação #{selected.source_id}, versão {selected.data.source_version}</p>}<p><strong>Moeda:</strong> {selected.data.currency || 'Não indicada — pendente'}</p><div className="table-scroll"><table><thead><tr><th>Serviço</th><th>Quantidade</th><th>Preço proposto</th></tr></thead><tbody>{selected.data.lines.map((line, i) => <tr key={i}><td>{line.service_name}</td><td>{line.quantity ?? 'Por preencher'}</td><td>{line.unit_price ?? 'Por preencher'}</td></tr>)}</tbody></table></div><p><strong>Base aritmética:</strong> {selected.base_amount ?? 'Incompleta'} {selected.data.currency || '(moeda pendente)'}</p><p className="help">Esta base não é um total fiscal ou valor definitivo a pagar. Imposto por validar; contabilidade não processada.</p>
|
||||
{Object.keys(selected.pending).length > 0 && <div className="a-pending"><h3>Pendências</h3><ul>{Object.entries(selected.pending).map(([code, message]) => <li key={code}>{message}</li>)}</ul></div>}
|
||||
{area === 'service_drafts' && can('billing_drafts.create', context.unit?.id) && <button className="primary" disabled={busy} onClick={prepareBilling}>Preparar rascunho de facturação</button>}
|
||||
{area === 'billing_drafts' && allowed('edit') && <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' && 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} />}
|
||||
</>}
|
||||
<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></>}
|
||||
@@ -95,6 +98,11 @@ export default function ServicesWorkspace({ area, context, can, api, perform, bu
|
||||
</>;
|
||||
}
|
||||
|
||||
function InternalDocument({ confirmation }) {
|
||||
const snapshot = confirmation.snapshot;
|
||||
return <section className="a-pending"><h3>{confirmation.label}</h3><p><strong>Referência interna:</strong> {confirmation.internal_reference} (não é numeração fiscal)</p><p>Autor #{snapshot.actor_id} · {snapshot.confirmed_at}</p><p><strong>Cliente preservado:</strong> {snapshot.client.name} · versão {snapshot.client.version}</p><p><strong>Origem preservada:</strong> prestação #{snapshot.service_draft.id}, versão {snapshot.service_draft.version}; rascunho #{snapshot.billing_draft.id}, versão {snapshot.billing_draft.version}</p><p><strong>Moeda preservada:</strong> {snapshot.data.currency || 'Desconhecida — pendente'}</p><ul>{snapshot.data.lines.map((line, i) => <li key={i}>{line.service_name} · quantidade {line.quantity} · preço {line.unit_price}</li>)}</ul><p>Base aritmética exacta: {snapshot.base_amount}. Não é total monetário definitivo ou fiscal.</p><p>Evento #{confirmation.event?.id} · Capturado · Contabilização bloqueada por pendências. Nenhum lançamento produzido.</p><h4>Pendências preservadas</h4><ul>{Object.entries(snapshot.pending).map(([code, message]) => <li key={code}>{message}</li>)}</ul><p>Fotografia imutável. Não há edição, eliminação, reabertura, correcção ou anulação neste incremento.</p><details><summary>Fotografia histórica completa</summary><pre>{JSON.stringify(snapshot, null, 2)}</pre></details></section>;
|
||||
}
|
||||
|
||||
function AreaAccess({ area, context, api, perform, busy }) {
|
||||
const [state, setState] = useState(null); const [error, setError] = useState('');
|
||||
const [email, setEmail] = useState(''); const [action, setAction] = useState('view'); const [reason, setReason] = useState('');
|
||||
|
||||
@@ -56,7 +56,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 === '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 · Entrega A: preparação em rascunho · Sem emissão fiscal ou lançamentos</footer></div></div>;
|
||||
</main><footer>SGF TEMPO · Rascunhos e confirmação interna · Sem emissão fiscal ou lançamentos</footer></div></div>;
|
||||
}
|
||||
function Login({ busy, onSubmit }) {
|
||||
const [email, setEmail] = useState(''); const [password, setPassword] = useState('');
|
||||
|
||||
Reference in New Issue
Block a user