Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
214 lines
5.5 KiB
TypeScript
214 lines
5.5 KiB
TypeScript
import {
|
|
BarChart3,
|
|
Bot,
|
|
BookOpen,
|
|
Building2,
|
|
FileText,
|
|
Key,
|
|
Newspaper,
|
|
Package,
|
|
ScrollText,
|
|
Search,
|
|
Users,
|
|
type LucideIcon,
|
|
} from 'lucide-react';
|
|
import type { PlanFeature } from '@/lib/api/subscriptions';
|
|
|
|
/**
|
|
* Definition unique de la navigation applicative.
|
|
*
|
|
* Les deux espaces (produit et administration) partagent la meme structure, ce
|
|
* qui permet a `AppShell` de rendre l'un ou l'autre sans duplication.
|
|
*/
|
|
export interface NavItem {
|
|
key: string;
|
|
label: string;
|
|
href: string;
|
|
icon: LucideIcon;
|
|
/** Fonctionnalite requise par le plan d'abonnement. */
|
|
requiredFeature?: PlanFeature;
|
|
}
|
|
|
|
export interface NavGroup {
|
|
key: string;
|
|
label: string;
|
|
items: NavItem[];
|
|
}
|
|
|
|
export type ShellVariant = 'app' | 'admin';
|
|
|
|
type Translate = (key: string) => string;
|
|
|
|
interface AppNavParams {
|
|
/** Traductions du namespace `dashboard`. */
|
|
t: Translate;
|
|
/** Traductions du namespace `shell`. */
|
|
tShell: Translate;
|
|
role?: string;
|
|
}
|
|
|
|
export function buildAppNav({ t, tShell, role }: AppNavParams): NavGroup[] {
|
|
const canManageUsers = role === 'ADMIN' || role === 'MANAGER';
|
|
|
|
return [
|
|
{
|
|
key: 'pilotage',
|
|
label: tShell('groups.pilotage'),
|
|
items: [
|
|
{
|
|
key: 'dashboard',
|
|
label: t('nav.dashboard'),
|
|
href: '/dashboard',
|
|
icon: BarChart3,
|
|
requiredFeature: 'dashboard',
|
|
},
|
|
{ key: 'bookings', label: t('nav.bookings'), href: '/dashboard/bookings', icon: Package },
|
|
{
|
|
key: 'documents',
|
|
label: t('nav.documents'),
|
|
href: '/dashboard/documents',
|
|
icon: FileText,
|
|
},
|
|
{
|
|
key: 'tracking',
|
|
label: t('nav.tracking'),
|
|
href: '/dashboard/track-trace',
|
|
icon: Search,
|
|
requiredFeature: 'dashboard',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: 'resources',
|
|
label: tShell('groups.resources'),
|
|
items: [
|
|
{
|
|
key: 'assistant',
|
|
label: t('nav.assistant'),
|
|
href: '/dashboard/assistant',
|
|
icon: Bot,
|
|
},
|
|
{
|
|
key: 'wiki',
|
|
label: t('nav.wiki'),
|
|
href: '/dashboard/wiki',
|
|
icon: BookOpen,
|
|
requiredFeature: 'wiki',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: 'organization',
|
|
label: tShell('groups.organization'),
|
|
items: [
|
|
{
|
|
key: 'organization',
|
|
label: t('nav.organization'),
|
|
href: '/dashboard/settings/organization',
|
|
icon: Building2,
|
|
},
|
|
...(canManageUsers
|
|
? [
|
|
{
|
|
key: 'users',
|
|
label: t('nav.users'),
|
|
href: '/dashboard/settings/users',
|
|
icon: Users,
|
|
requiredFeature: 'user_management' as PlanFeature,
|
|
},
|
|
]
|
|
: []),
|
|
{
|
|
key: 'apiKeys',
|
|
label: t('nav.apiKeys'),
|
|
href: '/dashboard/settings/api-keys',
|
|
icon: Key,
|
|
requiredFeature: 'api_access' as PlanFeature,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
interface AdminNavParams {
|
|
/** Traductions du namespace `components.adminPanelDropdown`. */
|
|
t: Translate;
|
|
/** Traductions du namespace `shell`. */
|
|
tShell: Translate;
|
|
}
|
|
|
|
export function buildAdminNav({ t, tShell }: AdminNavParams): NavGroup[] {
|
|
return [
|
|
{
|
|
key: 'management',
|
|
label: tShell('groups.management'),
|
|
items: [
|
|
{ key: 'users', label: t('items.users'), href: '/admin/users', icon: Users },
|
|
{
|
|
key: 'organizations',
|
|
label: t('items.organizations'),
|
|
href: '/admin/organizations',
|
|
icon: Building2,
|
|
},
|
|
{ key: 'bookings', label: t('items.bookings'), href: '/admin/bookings', icon: Package },
|
|
{
|
|
key: 'documents',
|
|
label: t('items.documents'),
|
|
href: '/admin/documents',
|
|
icon: FileText,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
key: 'content',
|
|
label: tShell('groups.content'),
|
|
items: [
|
|
{
|
|
key: 'csvRates',
|
|
label: t('items.csvRates'),
|
|
href: '/admin/csv-rates',
|
|
icon: BarChart3,
|
|
},
|
|
{ key: 'blog', label: t('items.blog'), href: '/admin/blog', icon: Newspaper },
|
|
],
|
|
},
|
|
{
|
|
key: 'system',
|
|
label: tShell('groups.system'),
|
|
items: [{ key: 'logs', label: t('items.logs'), href: '/admin/logs', icon: ScrollText }],
|
|
},
|
|
];
|
|
}
|
|
|
|
/** Racine de l'espace, utilisee pour l'etat actif exact et le fil d'Ariane. */
|
|
export const SHELL_ROOT: Record<ShellVariant, string> = {
|
|
app: '/dashboard',
|
|
admin: '/admin',
|
|
};
|
|
|
|
/**
|
|
* Un item est actif si la route courante est l'item lui-meme ou l'un de ses
|
|
* descendants. La racine de l'espace exige une correspondance exacte, faute de
|
|
* quoi elle resterait active sur toutes les pages.
|
|
*/
|
|
export function isItemActive(href: string, pathname: string, variant: ShellVariant): boolean {
|
|
if (href === SHELL_ROOT[variant]) return pathname === href;
|
|
return pathname === href || pathname.startsWith(href + '/');
|
|
}
|
|
|
|
/** Retrouve le groupe et l'item correspondant a la route courante. */
|
|
export function findActiveNav(groups: NavGroup[], pathname: string, variant: ShellVariant) {
|
|
let best: { group: NavGroup; item: NavItem } | null = null;
|
|
|
|
for (const group of groups) {
|
|
for (const item of group.items) {
|
|
if (!isItemActive(item.href, pathname, variant)) continue;
|
|
// On garde la correspondance la plus specifique (href le plus long).
|
|
if (!best || item.href.length > best.item.href.length) {
|
|
best = { group, item };
|
|
}
|
|
}
|
|
}
|
|
return best;
|
|
}
|