xpeditis2.0/apps/frontend/src/components/shell/nav-config.ts
David f0eb45131b feat(ui): espace de conversation de l assistant
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
2026-09-07 21:40:52 +02:00

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;
}