Files
SGF/resources/js/WorkspaceNavigation.jsx
T
2026-10-30 08:13:34 +02:00

78 lines
7.5 KiB
React

import React, {useEffect, useRef, useState} from 'react';
import {visibleBillingSections} from './billing-menu.js';
import {visibleWorkshopSections} from './workshop-menu.js';
import {visibleStockSections} from './stock-menu.js';
import {visibleAccountingSections} from './accounting-menu.js';
export function navigationGroups(can, unit) {
const item=(id,label,permission,scope=null)=>({id,label,visible:can(permission,scope)});
return [
{id:'start',label:'Início',items:[{id:'home',label:'Página inicial',visible:true}]},
{id:'purchasing',label:'Compras',items:[{id:'purchasing_home',label:'Área de Compras',visible:can('company.view')},item('purchasing_budget','Controlo orçamental sintético','purchasing_budget.view',unit),item('supplier_payables','Obrigações a fornecedores — ensaio','supplier_payables.view',unit),item('supplier_payments','Pagamentos e afectações — ensaio','supplier_payables.view',unit)]},
{id:'stock',label:'Stock',status:'Em preparação',items:visibleStockSections(can).map(s=>({id:s.id,label:s.label,informational:true,visible:true}))},
{id:'services',label:'Clientes e serviços',items:[item('clients','Clientes','clients.view'),item('services','Catálogo de serviços','services.view'),item('service_drafts','Prestações em rascunho','service_drafts.view',unit)]},
{id:'billing',label:'Facturação',items:visibleBillingSections(can,unit).map(s=>({id:s.id,label:s.label,visible:true}))},
{id:'billing_support',label:'Configuração documental',items:[{id:'documents',label:'Preparação documental',visible:can('issuer_documents.view')||can('client_documents.view')||can('draft_documents.view',unit)},item('service_invoice_setup','Séries e emissão de teste','service_invoices.view',unit),item('preview_model','Apresentação do rascunho','issuer_documents.view')]},
{id:'receivables',label:'Recebimentos',items:[item('service_receivables','Recebimentos, afectações e reconciliação de teste','service_receivables.view',unit)]},
{id:'workshop',label:'Oficinas',items:[...visibleWorkshopSections(can,unit).map(s=>({id:s.id,label:s.label,visible:true})),item('workshop_billing','Oficina → rascunho','workshop_billing.view',unit),item('workshop_corrections','Correcção e cancelamento comercial','workshop_corrections.view',unit)]},
{id:'costs',label:'Custos e orçamento',items:[item('direct_costs','Custo directo de teste','direct_costs.view',unit),item('industrial_costs','Lotes e custos industriais de teste','industrial_costs.view',unit),item('industrial_budgets','Orçamentos e comparação','industrial_budgets.view',unit)]},
{id:'accounting',label:'Contabilidade e fiscalidade',items:[...visibleAccountingSections(can).map(s=>({id:s.id,label:s.label,informational:true,visible:true})),item('catalogue_accounting','Classificação contabilística','catalogue_accounting.view'),item('service_fiscal','Perfis fiscais do serviço','service_fiscal.view')]},
{id:'admin',label:'Administração',items:[item('company','Empresa','company.view'),{id:'units',label:'Unidades autorizadas',visible:can('company.view')},item('access','Acessos','access.manage')]},
].map(g=>({...g,items:g.items.filter(i=>i.visible)})).filter(g=>g.items.length);
}
export function navigationTopics(groups) {
const layout=[
{id:'start',label:'Início',groups:['start']},
{id:'commercial',label:'Comercial',groups:['services','billing']},
{id:'purchasing',label:'Compras',groups:['purchasing']},
{id:'stock',label:'Stock',groups:['stock']},
{id:'workshop',label:'Oficinas',groups:['workshop']},
{id:'finance',label:'Financeiro',groups:['receivables','costs']},
{id:'accounting',label:'Contabilidade',groups:['accounting']},
{id:'settings',label:'Configuração',groups:['billing_support','admin']},
];
const known=new Set(layout.flatMap(t=>t.groups));
return [...layout.map(t=>({...t,sections:t.groups.map(id=>groups.find(g=>g.id===id)).filter(Boolean)})),
...groups.filter(g=>!known.has(g.id)).map(g=>({id:g.id,label:g.label,sections:[g]}))]
.filter(t=>t.sections.some(g=>g.items.length));
}
export default function WorkspaceNavigation({groups,page,navigate,user,busy,logout}) {
const [open,setOpen]=useState(null);const root=useRef(null);const triggers=useRef({});
const topics=navigationTopics(groups);
useEffect(()=>{
const outside=e=>{if(root.current&&!root.current.contains(e.target))setOpen(null);};
const escape=e=>{if(e.key==='Escape'&&open){e.preventDefault();setOpen(null);triggers.current[open]?.focus();}};
document.addEventListener('pointerdown',outside);document.addEventListener('keydown',escape);
return ()=>{document.removeEventListener('pointerdown',outside);document.removeEventListener('keydown',escape);};
},[open]);
useEffect(()=>{setOpen(null);},[page,JSON.stringify(groups)]);
function choose(id){navigate(id);setOpen(null);}
function focusItem(e,topic){
if(!['ArrowDown','ArrowUp','Home','End'].includes(e.key))return;
const items=[...root.current.querySelectorAll(`#topic-${topic} .topic-item`)];
if(!items.length)return;e.preventDefault();const index=items.indexOf(e.target);
const next=e.key==='Home'?0:e.key==='End'?items.length-1:e.key==='ArrowDown'?(index+1)%items.length:(index-1+items.length)%items.length;
items[next].focus();
}
function openWithKeyboard(e,id){
if(e.key==='ArrowDown'||e.key==='ArrowUp'){
e.preventDefault();setOpen(id);
requestAnimationFrame(()=>{const items=root.current?.querySelectorAll(`#topic-${id} .topic-item`);(e.key==='ArrowUp'?items?.[items.length-1]:items?.[0])?.focus();});
}
}
return <div className="workspace-topbar" ref={root} onBlur={e=>{if(e.relatedTarget&&!e.currentTarget.contains(e.relatedTarget))setOpen(null);}}>
<a className="topbar-brand" href="/">TEMPO <span>SGF</span></a>
<nav className="topic-navigation" aria-label="Navegação principal por assunto">{topics.map((t,index)=>{
const active=t.sections.some(g=>g.items.some(i=>i.id===page));
if(t.id==='start')return <button className={`topic-trigger ${active?'topic-active':''}`} aria-current={active?'page':undefined} key={t.id} onClick={()=>choose('home')}>Início</button>;
return <div className={`navigation-topic ${index>=4?'topic-align-right':''}`} key={t.id}>
<button className={`topic-trigger ${active?'topic-active':''}`} ref={el=>{triggers.current[t.id]=el;}} aria-expanded={open===t.id} aria-controls={`topic-${t.id}`} onClick={()=>setOpen(open===t.id?null:t.id)} onKeyDown={e=>openWithKeyboard(e,t.id)}>{t.label}<span aria-hidden="true"> ▾</span>{active&&<span className="sr-only"> — área actual</span>}</button>
<div id={`topic-${t.id}`} className="topic-dropdown" hidden={open!==t.id} onKeyDown={e=>focusItem(e,t.id)}><div className="topic-sections">{t.sections.map(g=><section key={g.id}><h2>{g.label}{g.status&&<small>{g.status}</small>}</h2>{g.items.map(i=><button className={`topic-item ${page===i.id?'topic-item-active':''}`} aria-current={page===i.id?'page':undefined} key={i.id} onClick={()=>choose(i.id)}>{i.label}{i.informational&&<small>Em preparação</small>}</button>)}</section>)}</div></div>
</div>;
})}</nav>
<div className="topbar-account"><div><strong>{user.name}</strong><small>{user.email}</small></div><button disabled={busy} onClick={logout}>Sair</button></div>
</div>;
}