Files
SGF/resources/js/LedgerReportsWorkspace.jsx
2026-11-11 09:00:17 +02:00

18 lines
6.5 KiB
React

import React,{useEffect,useRef,useState} from 'react';
import {minorAmount,ledgerReportQuery,checkedReport,reportBasis,reportPageCsv} from './ledger-report-view.js';
export function LedgerReportTable({data,onAccount}){
const trial=data.kind==='trial_balance';return <div className="table-scroll"><table><thead><tr>{(trial?['Conta','Designação','Moeda','Abertura líquida','Débito','Crédito','Fecho líquido','Consultar']:['Data / lançamento','Origem e fundamento','Débito','Crédito','Saldo progressivo']).map(t=><th key={t}>{t}</th>)}</tr></thead><tbody>{data.rows.map(r=>trial?<tr key={r.account_id}><td>{r.code}</td><td>{r.label}</td><td>{r.currency}</td><td>{minorAmount(r.opening_net_minor)}</td><td>{minorAmount(r.debit_minor)}</td><td>{minorAmount(r.credit_minor)}</td><td>{minorAmount(r.closing_net_minor)}</td><td><button onClick={()=>onAccount(r.account_id,r.code+' · '+r.label)}>Abrir razão desta conta</button></td></tr>:<tr key={r.line_id}><td>{r.journal_on}<small>Lançamento #{r.journal_id} · linha #{r.line_id}</small></td><td>{r.reason}<small>Proposta #{r.proposal_id} · revisão #{r.review_id} · {r.effect_type??'Origem no diário'}</small></td><td>{minorAmount(r.debit_minor)}</td><td>{minorAmount(r.credit_minor)}</td><td>{minorAmount(r.balance_minor)}</td></tr>)}</tbody></table>{!data.rows.length&&<p>Nenhum movimento elegível para a consulta autorizada e as datas escolhidas.</p>}</div>;
}
export default function LedgerReportsWorkspace({context,api}){
const initial={kind:'trial-balance',from_on:'',to_on:'',account_id:'',per_page:'25'};
const [form,setForm]=useState(initial),[query,setQuery]=useState(null),[data,setData]=useState(null),[error,setError]=useState(''),[loading,setLoading]=useState(false);const basis=useRef(null);
useEffect(()=>{let current=true;setData(null);setError('');if(!query)return()=>{current=false;};setLoading(true);const expected=query.params.page>1?basis.current:null;api('/api/general-ledger/'+query.kind+'?'+new URLSearchParams(query.params)).then(value=>{const result=checkedReport(value,query,expected);if(current){basis.current=reportBasis(result);setData(result);}}).catch(e=>{if(current)setError(e.message);}).finally(()=>{if(current)setLoading(false);});return()=>{current=false;};},[context.token,query]);
const load=(value,page=1)=>{try{const q=ledgerReportQuery(value,page);if(page===1)basis.current=null;setQuery(q);setError('');}catch(e){setError(e.message);}};
const account=(id,label)=>{const value={...form,...data.query,kind:'account-ledger',account_id:String(id),account_label:label};setForm(value);load(value);};
const download=()=>{const url=URL.createObjectURL(new Blob(['\uFEFF'+reportPageCsv(data)],{type:'text/csv;charset=utf-8'}));const a=document.createElement('a');a.href=url;a.download='sgf-'+data.kind+'-pagina-'+data.pagination.page+'.csv';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);};
return <section className="panel"><h2>Balancete e razão por período</h2><p>Escolha datas explícitas. Abertura considera lançamentos anteriores ao início; movimentos abrangem o intervalo e o fecho inclui o último dia. Só entram lançamentos aplicados.</p><form data-sgf-read-filters="true" onSubmit={e=>{e.preventDefault();load(form);}}><div className="form-grid"><label>Consulta<select value={form.kind} onChange={e=>setForm({...form,kind:e.target.value})}><option value="trial-balance">Balancete</option><option value="account-ledger">Razão de uma conta</option></select></label><label>Desde<input type="date" required value={form.from_on} onChange={e=>setForm({...form,from_on:e.target.value})}/></label><label>Até<input type="date" required value={form.to_on} onChange={e=>setForm({...form,to_on:e.target.value})}/></label><label>Conta seleccionada<input readOnly value={form.account_label??(form.account_id?'Conta #'+form.account_id:'Todas as contas do balancete')}/></label><label>Por página<select value={form.per_page} onChange={e=>setForm({...form,per_page:e.target.value})}><option value="25">25</option><option value="100">100</option></select></label></div><button disabled={loading}>Consultar</button><button type="button" disabled={loading} onClick={()=>{const next={...form,kind:'trial-balance',account_id:'',account_label:undefined};setForm(next);load(next);}}>Consultar todas as contas</button></form>
{loading&&<p role="status">A consultar o livro autorizado…</p>}{error&&<div role="alert"><p>{error}</p><p>A falha não significa saldo zero.</p>{query&&<button onClick={()=>load({...query.params,kind:query.kind})}>Actualizar a primeira página</button>}</div>}{data&&<><h3>{data.kind==='trial_balance'?'Balancete':`Razão · ${data.account.code} · ${data.account.label}`}</h3><p>{data.query.from_on} a {data.query.to_on} · {data.currency} · página {data.pagination.page} de {data.pagination.last_page} · {data.pagination.total} registo(s).</p><dl className="overview-definitions">{[['Débitos do intervalo',data.totals.debit_minor],['Créditos do intervalo',data.totals.credit_minor],...(data.kind==='account_ledger'?[['Abertura líquida da conta',data.opening_net_minor],['Fecho líquido da conta',data.closing_net_minor]]:[])].map(([label,v])=><div key={label}><dt>{label}</dt><dd>{minorAmount(v)} {data.currency}</dd></div>)}</dl><p>Totais do filtro completo devolvidos pelo servidor; não são a soma apenas das linhas visíveis. No balancete, saldo líquido positivo é devedor; negativo é credor.</p><LedgerReportTable data={data} onAccount={account}/><div className="actions"><button disabled={loading||data.pagination.page<=1} onClick={()=>load({...data.query,kind:query.kind},data.pagination.page-1)}>Anterior</button><button disabled={loading||data.pagination.page>=data.pagination.last_page} onClick={()=>load({...data.query,kind:query.kind},data.pagination.page+1)}>Seguinte</button><button onClick={download}>Exportar apenas esta página CSV</button><a href={'/general-ledger/'+query.kind+'?'+new URLSearchParams(query.params)} target="_blank" rel="noopener">Abrir nova leitura para impressão</a></div><p className="help">Referência do livro: {data.basis.journals} lançamento(s), último #{data.basis.last_journal_id}, até {data.basis.through_on}. O saldo progressivo do razão inclui as páginas anteriores. Se o livro mudar entre páginas, exige-se nova consulta. Exportar não arquiva nem certifica um relatório.</p></>}
<p className="help">Contas artificiais do exercício local. Sem balanço, demonstração de resultados ou classificação legal inferida. Consultar e exportar esta página não gera lançamentos, ajustamentos ou fecho.</p></section>;
}