588 lines
20 KiB
TypeScript
588 lines
20 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[],
|
|
): 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: '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: '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 !== '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;
|
|
}
|