77 lines
7.3 KiB
React
77 lines
7.3 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)]},
|
|
{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','accounting','costs']},
|
|
{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>;
|
|
}
|