Files
compras/resources/js/lib/procurement-navigation.ts
nala 63d20b10fc
tests / ci (push) Canceled after 0s
commit de armazens
2026-11-04 09:40:26 +02:00

616 lines
21 KiB
TypeScript

import {
BadgeCheck,
ClipboardList,
Scale,
ShieldCheck,
Users,
Wallet,
Warehouse,
FileBarChart,
} from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
export type ProcurementEntry = {
id: string;
label: string;
path: string;
roles?: string[];
fullPage?: boolean;
section?: string;
};
export type ProcurementGroup = {
id: string;
label: string;
icon: LucideIcon;
entries: ProcurementEntry[];
};
const readers = ['admin', 'procurement', 'finance', 'department_head'];
const finance = ['admin', 'finance'];
const inventory = [...readers, 'warehouse'];
export function procurementNavigation(
role: string | string[],
payables: boolean,
publicPaths?: string[],
orderCancellation = false,
): ProcurementGroup[] {
const groups: ProcurementGroup[] = [
{
id: 'requests',
label: 'Requisições',
icon: ClipboardList,
entries: [
{
id: 'new_request',
label: 'Nova requisição',
path: '/requisicoes/criar',
},
{
id: 'my_requests',
label: 'Minhas requisições',
path: '/requisicoes?view=mine',
},
{
id: 'my_workflow_tasks',
label: 'As minhas tarefas',
path: '/requisicoes/tarefas',
},
{
id: 'my_drafts',
label: 'Os meus rascunhos',
path: '/requisicoes?view=mine&status=draft',
},
{
id: 'all_requests',
label: 'Todas as autorizadas',
path: '/requisicoes?view=all',
},
{ id: 'planning', label: 'Planeamento', path: '/planeamento' },
],
},
{
id: 'approvals',
label: 'Aprovações',
icon: BadgeCheck,
entries: [
{
id: 'pending_approvals',
label: 'Pendentes para mim',
path: '/requisicoes?view=approvals',
roles: readers,
},
{
id: 'workflow_notices',
label: 'Avisos de aprovação',
path: '/requisicoes/avisos',
},
{
id: 'decisions',
label: 'Minhas decisões',
path: '/requisicoes?view=decided',
roles: readers,
},
],
},
{
id: 'sourcing',
label: 'Consultas e adjudicações',
icon: Scale,
entries: [
{
id: 'quotations',
label: 'Cotações e comparação',
path: '/cotacoes',
roles: readers,
},
{
id: 'orders',
label: 'Adjudicações e ordens',
path: '/ordens',
roles: readers,
},
{
id: 'issued_order_cancellations',
label: 'Cancelamento de ordens',
path: '/ordens/cancelamentos',
roles: [
'admin',
'procurement',
'finance',
'requester',
'department_head',
],
},
{
id: 'purchase_review',
label: 'Revisão e cancelamento',
path: '/revisao-compras',
},
{
id: 'supply_agreements',
label: 'Fornecimento habitual',
path: '/compras/fornecimento?view=agreements',
roles: [
'admin',
'supply_read',
'supply_prepare',
'supply_review',
],
},
{
id: 'supply_calendar',
label: 'Reposição e calendário',
path: '/compras/fornecimento?view=calendar',
roles: [
'admin',
'supply_read',
'supply_prepare',
'supply_review',
],
},
{
id: 'supply_suggestions',
label: 'Sugestões de reposição',
path: '/compras/fornecimento?view=suggestions',
roles: [
'admin',
'supply_read',
'supply_suggest_prepare',
'supply_suggest_review',
'supply_suggest_convert',
],
},
{
id: 'supply_imports',
label: 'Importações',
path: '/compras/fornecimento?view=imports',
roles: [
'admin',
'import_read',
'import_prepare',
'import_customs',
],
},
{
id: 'supply_alternatives',
label: 'Alternativas técnicas',
path: '/compras/fornecimento?view=alternatives',
roles: [
'admin',
'supply_read',
'supply_technical_prepare',
'supply_technical_review',
],
},
{
id: 'supply_deliveries',
label: 'Entregas confirmadas',
path: '/compras/fornecimento?view=deliveries',
roles: ['admin', 'supply_read', 'supply_delivery'],
},
],
},
{
id: 'suppliers',
label: 'Fornecedores',
icon: Users,
entries: [
{
id: 'suppliers',
label: 'Cadastro e documentos',
path: '/fornecedores',
roles: readers,
},
{
id: 'supplier_evaluations',
label: 'Avaliação de fornecedores',
path: '/fornecedores/avaliacoes',
roles: readers,
},
],
},
{
id: 'people',
label: 'Pessoas e contratações',
icon: Users,
entries: [
{
id: 'people_workflow_tasks',
label: 'As minhas tarefas',
path: '/pessoas/tarefas',
roles: [
'admin',
'people_service_prepare',
'people_service_review',
'people_staff_prepare',
'people_staff_review',
],
},
{
id: 'people_registry',
label: 'Cadastro reutilizável de pessoas',
path: '/pessoas/cadastro',
roles: [
'admin',
'people_service_prepare',
'people_staff_prepare',
'people_registry_read',
'people_registry_manage',
],
},
{
id: 'people_hiring',
label: 'Processos e equipas',
path: '/pessoas',
roles: [
'admin',
'people_service_prepare',
'people_service_review',
'people_staff_prepare',
'people_staff_review',
],
},
{
id: 'people_services',
label: 'Serviços individuais',
path: '/pessoas?kind=service',
roles: [
'admin',
'people_service_prepare',
'people_service_review',
],
},
{
id: 'people_staff',
label: 'Recrutamento e selecção',
path: '/pessoas?kind=staff',
roles: [
'admin',
'people_staff_prepare',
'people_staff_review',
],
},
{
id: 'people_reports',
label: 'Relatórios de pessoas e contratações',
path: '/pessoas/relatorios',
roles: [
'admin',
'people_service_prepare',
'people_service_review',
'people_staff_prepare',
'people_staff_review',
],
},
],
},
{
id: 'receiving',
label: 'Recepções e armazém',
icon: Warehouse,
entries: [
{
id: 'receipts',
label: 'Recepções e divergências',
path: '/recepcoes',
roles: inventory,
},
{
id: 'supply_reservations',
label: 'Reservas de necessidades',
path: '/compras/fornecimento?view=reservations',
roles: ['admin', 'supply_read', 'supply_reserve'],
},
{
id: 'supply_history',
label: 'Histórico de consumo',
path: '/compras/fornecimento?view=history',
roles: [
'admin',
'supply_read',
'supply_history_prepare',
'supply_history_review',
],
},
{
id: 'warehouse_availability',
label: 'Disponibilidade para requisições',
path: '/stock/disponibilidade',
roles: [
'admin',
'warehouse',
'procurement',
'finance',
'warehouse_availability_read',
],
},
{
id: 'warehouse_replenishment',
label: 'Reposição automática',
path: '/stock/reposicao',
roles: [
'admin',
'supply_read',
'supply_prepare',
'supply_review',
],
},
{
id: 'warehouse_pilot',
label: 'Preparação do piloto',
path: '/stock/piloto',
roles: ['admin'],
},
{
id: 'warehouse_requests',
label: 'Requisições ao armazém',
path: '/stock/requisicoes',
roles: [
'admin',
'warehouse_request_read',
'warehouse_request_prepare',
'warehouse_request_review',
'warehouse_request_deliver',
],
},
{
id: 'inventory_lots',
label: 'Lotes, validade e quarentena',
path: '/stock/lotes',
roles: [
'admin',
'inventory_lot_read',
'inventory_lot_prepare',
'inventory_lot_review',
'inventory_lot_hold',
'inventory_lot_release',
],
},
{
id: 'stock',
label: 'Artigos e stock',
path: '/stock',
roles: ['admin', 'procurement', 'finance', 'warehouse'],
},
{
id: 'inventory_dispositions',
label: 'Devoluções e correcções',
path: '/stock/ocorrencias',
roles: [
'admin',
'warehouse',
'inventory_disposition_read',
'inventory_disposition_prepare',
'inventory_disposition_review',
'inventory_reversal_read',
'inventory_reversal_prepare',
'inventory_reversal_review',
],
},
{
id: 'stock_operations',
label: 'Saídas e inventários',
path: '/stock/operacoes',
roles: [
'admin',
'procurement',
'finance',
'warehouse',
'inventory_adjust_review',
],
},
{
id: 'equipment',
label: 'Equipamentos e peças',
path: '/equipamentos',
roles: inventory,
},
],
},
{
id: 'finance',
label: 'Finanças',
icon: Wallet,
entries: [
{
id: 'invoices',
label: 'Facturas e conferência',
path: '/facturas',
roles: finance,
},
...(payables
? [
{
id: 'payables',
label: 'Obrigações a fornecedores',
path: '/obrigacoes-fornecedores',
roles: finance,
fullPage: true,
},
]
: []),
{
id: 'payments',
label: 'Pagamentos externos',
path: '/pagamentos',
roles: finance,
},
{
id: 'budgets',
label: 'Orçamentos e reforços',
path: '/orcamentos',
roles: finance,
},
{
id: 'investment',
label: 'Regras de investimento',
path: '/regras-investimento',
roles: ['admin', 'procurement', 'finance'],
},
],
},
{
id: 'administration',
label: 'Administração e controlo',
icon: ShieldCheck,
entries: [
{
id: 'company',
label: 'Empresa, equipa e centros',
path: '/empresa',
roles: ['admin'],
},
{
id: 'organization_parameters',
label: 'Parametrização da aplicação',
path: '/administracao/parametrizacao',
roles: ['admin'],
},
{
id: 'admission_policy',
label: 'Política de admissão',
path: '/politica-admissao',
roles: ['admin'],
},
{
id: 'recommendation_policy',
label: 'Política de recomendação',
path: '/politica-recomendacao',
roles: ['admin'],
},
{
id: 'conflicts',
label: 'Conflitos de interesses',
path: '/conflitos',
},
],
},
{
id: 'audit',
label: 'Auditoria',
icon: ShieldCheck,
entries: [
{
id: 'audit_metadata',
label: 'Eventos dos módulos',
path: '/auditoria/eventos',
roles: ['audit_metadata_read'],
},
],
},
{
id: 'reports',
label: 'Relatórios',
icon: FileBarChart,
entries: [
{
id: 'procurement_reports',
label: 'Relatórios de compras',
path: '/relatorios',
roles: readers,
},
{
id: 'supplier_improvement_reports',
label: 'Medidas de melhoria',
path: '/relatorios/medidas-fornecedores',
roles: readers,
},
],
},
];
const purchasing = groups.filter((group) =>
['requests', 'approvals', 'sourcing', 'suppliers'].includes(group.id),
);
const compact = [
{
id: 'purchasing',
label: 'Compras',
icon: ClipboardList,
entries: purchasing.flatMap((group) =>
group.entries.map((entry) => ({
...entry,
section: group.label,
})),
),
},
...groups
.filter(
(group) =>
![
'requests',
'approvals',
'sourcing',
'suppliers',
].includes(group.id),
)
.map((group) => ({
...group,
label:
group.id === 'administration'
? 'Administração'
: group.id === 'receiving'
? 'Armazém e activos'
: group.label,
})),
];
return compact
.map((group) => ({
...group,
entries: group.entries.filter((entry) =>
publicPaths
? publicPaths.includes(
new URL(entry.path, 'http://local').pathname,
)
: (entry.id !== 'issued_order_cancellations' ||
orderCancellation) &&
(entry.id !== 'new_request' ||
!Array.isArray(role) ||
role.includes('requester')) &&
(!entry.roles ||
entry.roles.some((permission) =>
(Array.isArray(role) ? role : [role]).includes(
permission,
),
)),
),
}))
.filter((group) => group.entries.length);
}
export function activeProcurementEntry(
groups: ProcurementGroup[],
url: string,
): string | undefined {
const current = new URL(url, 'http://local');
if (/^\/ordens\/\d+\/recepcoes/.test(current.pathname)) return 'receipts';
if (/^\/requisicoes\/\d+\/adjudicacao(?:\/|$)/.test(current.pathname))
return 'orders';
if (
/^\/requisicoes\/\d+\/(cotacoes|abertura|qualificacao|admissao|recomendacao|relatorio)(?:\/|$)/.test(
current.pathname,
)
)
return 'quotations';
const matches = groups
.flatMap((group) => group.entries)
.filter((entry) => {
const target = new URL(entry.path, 'http://local');
if (
current.pathname !== target.pathname &&
!current.pathname.startsWith(target.pathname + '/')
)
return false;
return [...target.searchParams].every(
([key, value]) =>
(current.searchParams.get(key) ??
(key === 'view' ? 'all' : null)) === value,
);
});
const specificity = (path: string) => {
const target = new URL(path, 'http://local');
return target.pathname.length * 100 + target.searchParams.size;
};
matches.sort((a, b) => specificity(b.path) - specificity(a.path));
return matches[0]?.id;
}