49 lines
8.0 KiB
React
49 lines
8.0 KiB
React
import React,{useEffect,useState} from 'react';
|
|
import {billingSections,visibleBillingSections,filterBillingRows,loadBillingRows} from './billing-menu.js';
|
|
|
|
const emptyFilters={search:'',type:'',currency:'',series:'',state:'',author:'',from:'',to:''};
|
|
const value=v=>v===null||v===undefined||v===''?'Por determinar':String(v);
|
|
|
|
export function BillingDocumentList({rows,filters,onFilter,onClear,onOpen}) {
|
|
const filtered=filterBillingRows(rows,filters);
|
|
const options=field=>[...new Set(rows.map(r=>r[field]).filter(v=>v!==null&&v!==undefined&&v!==''))].sort();
|
|
return <section className="panel billing-document-list"><h2>Lista de documentos</h2><div className="billing-filters">
|
|
<label>Pesquisar número, cliente ou NUIT<input type="search" value={filters.search} onChange={e=>onFilter('search',e.target.value)}/></label>
|
|
<label>Tipo<select value={filters.type} onChange={e=>onFilter('type',e.target.value)}><option value="">Todos os tipos disponíveis</option>{options('type').map(t=><option value={t} key={t}>{t==='invoice'?'Factura de teste':'Rascunho'}</option>)}</select></label>
|
|
{['currency','series','state','author'].map(field=><label key={field}>{{currency:'Moeda',series:'Série',state:'Estado documental',author:'Autor (identificador registado)'}[field]}<select value={filters[field]} onChange={e=>onFilter(field,e.target.value)}><option value="">Todos</option>{options(field).map(v=><option key={v} value={v}>{v}</option>)}</select></label>)}
|
|
<label>Emissão desde<input type="date" value={filters.from} onChange={e=>onFilter('from',e.target.value)}/></label><label>Emissão até<input type="date" value={filters.to} onChange={e=>onFilter('to',e.target.value)}/></label>
|
|
</div><button onClick={onClear}>Limpar filtros</button>{filters.from&&filters.to&&filters.from>filters.to?<p role="alert">A data inicial deve ser anterior ou igual à data final.</p>:<>
|
|
<p role="status">{filtered.length} documento(s) nesta consulta.</p><div className="table-scroll"><table><thead><tr><th>Número / referência</th><th>Tipo</th><th>Cliente</th><th>Emissão</th><th>Vencimento</th><th>Total</th><th>Moeda</th><th>Saldo por receber</th><th>Estados</th><th>Acções</th></tr></thead><tbody>{filtered.map(r=><tr key={r.key}><td>{r.number}{r.name&&<small>{r.name}</small>}{r.version&&<small>Versão {r.version}</small>}</td><td>{r.typeLabel}</td><td>{value(r.client)}{r.nuit&&<small>NUIT {r.nuit}</small>}</td><td>{r.type==='draft'?'Não emitido':value(r.issuedDate)}</td><td>{r.type==='draft'?'Não aplicável':value(r.dueDate)}</td><td className="billing-number">{r.type==='draft'?'Provisório — por determinar':value(r.total)}{r.base!==undefined&&r.base!==null&&<small>Base aritmética: {r.base} (não é total fiscal)</small>}</td><td>{value(r.currency)}</td><td>{r.type==='draft'?'Não aplicável':'Consultar percurso financeiro'}</td><td>{r.state}<small>Pagamento: {r.payment}</small></td><td><button onClick={()=>onOpen(r)}>Abrir</button></td></tr>)}</tbody></table></div>{!filtered.length&&<p>Nenhum documento corresponde aos filtros nas fontes disponíveis.</p>}</>}
|
|
<p className="help">Valores documentais preservados, sem recálculo. Não são somadas moedas diferentes. Ausência de saldo ou vencimento não significa zero, factura paga ou atraso. Filtros de emissão excluem rascunhos e datas desconhecidas.</p>
|
|
</section>;
|
|
}
|
|
|
|
export default function BillingMenuWorkspace({section,context,can,api,navigate}) {
|
|
const unit=context.unit?.id??null;
|
|
const [result,setResult]=useState(null),[loading,setLoading]=useState(false),[reload,setReload]=useState(0),[filters,setFilters]=useState(emptyFilters),[selected,setSelected]=useState(null);
|
|
const active=billingSections.find(s=>s.id===section);
|
|
useEffect(()=>{setSelected(null);},[section]);
|
|
useEffect(()=>{
|
|
let current=true;setResult(null);setSelected(null);
|
|
if(unit===null||active?.pending)return ()=>{current=false;};
|
|
setLoading(true);
|
|
loadBillingRows(api,can,unit).then(r=>{if(current){setResult(r);setLoading(false);}});
|
|
return ()=>{current=false;};
|
|
},[context.token,section,reload]);
|
|
if(!active)return null;
|
|
const newDraft=()=>navigate('billing_drafts',null,true);
|
|
return <div className="billing-menu-workspace"><div className="page-title"><p className="eyebrow">FACTURAÇÃO</p><h1>{active.label}</h1><p>Documentos comerciais no contexto de {context.company.name}{context.unit?` / ${context.unit.name}`:''}.</p></div>
|
|
<nav className="billing-section-tabs" aria-label="Submenus de facturação">{visibleBillingSections(can,unit).map(s=><button key={s.id} aria-current={s.id===section?'page':undefined} onClick={()=>navigate(s.id)}>{s.label}</button>)}</nav>
|
|
<p className="fiscal-notice"><strong>Demonstração — sem validade fiscal.</strong> A entrada e a consulta não habilitam emissão real. Documento, dívida e recebimento são factos distintos.</p>
|
|
{!context.unit&&<section className="panel"><h2>Seleccione uma unidade</h2><p>Escolha uma unidade autorizada para consultar os seus documentos.</p></section>}
|
|
{active.pending?<section className="panel"><h2>Capacidade ainda não disponível</h2><p>{active.pending}</p>{section==='billing_receipts'&&can('service_receivables.view',unit)&&<button onClick={()=>navigate('service_receivables')}>Abrir recebimentos e afectações de teste</button>}{section==='billing_credit_notes'&&can('service_invoices.view',unit)&&<button onClick={()=>navigate('service_invoice_setup')}>Consultar facturas e pedidos de correcção existentes</button>}<p>Este submenu identifica o âmbito previsto; não contém documentos simulados nem executa efeitos financeiros.</p></section>:context.unit&&<>
|
|
<div className="actions billing-toolbar">{can('billing_drafts.view',unit)&&can('billing_drafts.create',unit)&&<button className="primary" onClick={newDraft}>Novo documento — preparar rascunho</button>}{can('service_invoices.view',unit)&&<button onClick={()=>navigate('service_invoice_setup')}>Séries e emissão de teste</button>}<button disabled={loading} onClick={()=>setReload(r=>r+1)}>Actualizar lista</button></div>
|
|
<p className="help">A criação reutiliza uma prestação existente e não emite automaticamente. Guardar rascunho, confirmar internamente, emitir em teste e receber mantêm os seus circuitos próprios.</p>
|
|
{loading&&<p role="status">A carregar documentos autorizados…</p>}
|
|
{result?.sources.map(s=>s.error?<p className="alert error" role="alert" key={s.label}>{s.label}: consulta indisponível. {s.error} Use Actualizar lista para tentar novamente.</p>:s.notice?<p className="alert home-notice" key={s.label}>{s.label}: {s.notice}</p>:null)}
|
|
{result&&!loading&&(result.sources.some(s=>!s.error&&!s.notice)?<BillingDocumentList rows={result.rows.filter(r=>section!=='service_invoices'||r.type==='invoice')} filters={filters} onFilter={(k,v)=>setFilters(f=>({...f,[k]:v}))} onClear={()=>setFilters(emptyFilters)} onOpen={r=>r.type==='draft'?navigate('billing_drafts',r.id):setSelected(r)}/>:<p>Não há uma fonte documental disponível para esta consulta. Não é possível concluir que existam zero documentos.</p>)}
|
|
{selected&&<article className="panel billing-document-detail"><h2>{selected.number}</h2><p>Factura de teste — fotografia preservada, sem edição directa.</p><button onClick={()=>setSelected(null)}>Fechar ficha</button>{selected.detail.document_lines.map(([kind,text],i)=>kind==='title'?<h3 key={i}>{text}</h3>:kind==='section'?<h4 key={i}>{text}</h4>:<p key={i}>{text}</p>)}<a href={`/api/service-invoices/${selected.id}/pdf`}>Descarregar PDF de teste</a><h3>Pedidos de correcção</h3>{selected.detail.corrections.length?selected.detail.corrections.map(c=><p key={c.id}>#{c.id}: {c.reason} · {c.created_at} · Pendente, sem efeito fiscal.</p>):<p>Nenhum pedido registado.</p>}<p className="help">Enviar ao cliente, duplicar documentos, exportar selecções e emitir notas de crédito ainda não estão disponíveis neste menu. Não existe emissão ou rectificação em massa.</p></article>}
|
|
</>}
|
|
</div>;
|
|
}
|