Files
SGF/resources/js/CatalogueAccountingWorkspace.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

48 lines
13 KiB
React

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