Files
SGF/resources/js/billing-menu.js
T
2026-10-29 22:42:37 +02:00

66 lines
4.8 KiB
JavaScript

// Navigation and read models only. Financial values are copied from preserved sources.
export const billingSections = [
{id:'billing_all',label:'Todos os documentos'},
{id:'service_invoices',label:'Facturas'},
{id:'billing_invoice_receipts',label:'Facturas-recibo',pending:'A emissão de facturas-recibo ainda não está implementada. Uma factura e um recebimento não serão reunidos automaticamente neste tipo de documento.'},
{id:'billing_receipts',label:'Recibos',pending:'O documento fiscal Recibo ainda não está implementado. O percurso existente de recebimentos e afectações de teste conserva a sua identidade própria.'},
{id:'billing_credit_notes',label:'Notas de crédito',pending:'A emissão de notas de crédito ainda não está implementada. Pedidos de correcção existentes não são notas de crédito e não alteram automaticamente a dívida, o stock ou o dinheiro.'},
{id:'billing_quotes',label:'Orçamentos',pending:'A proposta comercial ao cliente e a sua conversão em rascunho ainda não estão implementadas. Este orçamento comercial é distinto do controlo orçamental de Compras e do orçamento de custos industriais.'},
{id:'billing_drafts',label:'Rascunhos'},
];
export function billingAccess(can,unit) {
return (unit!==null && (can('service_invoices.view',unit)||can('billing_drafts.view',unit)||can('service_receivables.view',unit)))
|| can('issuer_documents.view')||can('client_documents.view')||(unit!==null&&can('draft_documents.view',unit));
}
export function visibleBillingSections(can,unit) {
if(!billingAccess(can,unit))return [];
return billingSections.filter(s=>s.id==='service_invoices'?unit!==null&&can('service_invoices.view',unit)
:s.id==='billing_drafts'?unit!==null&&can('billing_drafts.view',unit):true);
}
export function invoiceRow(record,detail) {
const snapshot=detail.snapshot,doc=snapshot.document;
return {key:`invoice:${record.id}`,id:record.id,type:'invoice',typeLabel:'Factura de teste',number:`${record.series_code} / ${record.number}`,
client:doc.client?.name??null,nuit:doc.client?.data?.nuit??null,issuedDate:(snapshot.issued_local??record.issued_at)?.slice(0,10)??null,
dueDate:null,total:snapshot.calculation?.total_document_proposed??null,currency:doc.data?.currency??null,
series:record.series_code,author:null,state:'Emitido apenas em teste',balance:null,payment:'Indeterminado',detail};
}
export function draftRow(record) {
return {key:`draft:${record.id}`,id:record.id,type:'draft',typeLabel:'Rascunho',number:`Interno #${record.id}`,name:record.name,
client:record.data?.client_name??null,nuit:null,issuedDate:null,dueDate:null,total:null,base:record.base_amount??null,currency:record.data?.currency??null,
series:null,author:record.actor_id??null,state:record.workshop_commercial_state==='superseded'?'Histórico / substituído':record.workshop_commercial_state==='cancelled'?'Cancelado':record.internal_confirmation?'Confirmado internamente':'Rascunho',
balance:null,payment:'Não aplicável',version:record.version,updatedAt:record.updated_at};
}
export function filterBillingRows(rows,filters) {
const normalise=v=>String(v??'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLocaleLowerCase('pt');
const query=normalise(filters.search).trim();
return rows.filter(r=>(!query||[r.number,r.name,r.client,r.nuit].some(v=>normalise(v).includes(query)))
&&(!filters.type||r.type===filters.type)&&(!filters.currency||r.currency===filters.currency)
&&(!filters.series||r.series===filters.series)&&(!filters.state||r.state===filters.state)
&&(!filters.author||String(r.author??'')===filters.author)
&&(!filters.from||(r.issuedDate&&r.issuedDate>=filters.from))&&(!filters.to||(r.issuedDate&&r.issuedDate<=filters.to)));
}
export async function loadBillingRows(api,can,unit) {
const jobs=[];
if(unit!==null&&can('service_invoices.view',unit))jobs.push(['Facturas',async()=>{
const list=await api('/api/service-invoices');
if(!list.test_enabled)return {rows:[],notice:list.notice??'Documentos de teste indisponíveis neste ambiente.'};
const rows=[];
// Bound simultaneous requests while reusing the existing scoped immutable document endpoint.
for(let start=0;start<list.invoices.length;start+=6){
rows.push(...await Promise.all(list.invoices.slice(start,start+6).map(async r=>invoiceRow(r,await api(`/api/service-invoices/${r.id}`)))));
}
return {rows};
}]);
if(unit!==null&&can('billing_drafts.view',unit))jobs.push(['Rascunhos',async()=>({rows:(await api('/api/a/billing_drafts')).records.map(draftRow)})]);
const sources=await Promise.all(jobs.map(async([label,job])=>{
try{return {label,...await job()};}catch(e){return {label,error:e.message,rows:[]};}
}));
return {rows:sources.flatMap(s=>s.rows),sources};
}