UNPKG

@next-helpdesk/core

Version:

Une bibliothèque React/Next.js complète pour systèmes de support/ticketing avec Kanban, diagramme de Gantt et gestion des tickets

1 lines 478 kB
{"version":3,"sources":["../src/components/app/HelpdeskApp.tsx","../src/context/HelpdeskContext.tsx","../src/components/ticket-form/create/CreateTicketButton.tsx","../src/components/ticket-form/create/CreateTicketForm.tsx","../src/schemas/ticket.ts","../src/components/ticket-form/common/FilePreviewDialog.tsx","../src/components/ticket-form/create/TicketAssignmentField.tsx","../src/components/common/UserSelect.tsx","../src/components/common/UserAvatar.tsx","../src/components/ticket-form/create/TicketBasicFields.tsx","../src/utils/permissions.ts","../src/utils/string.ts","../src/utils/status.ts","../src/utils/priority.ts","../src/utils/category.ts","../src/components/ticket-form/create/TicketCategoryField.tsx","../src/components/ticket-form/common/TicketFileUpload.tsx","../src/components/ticket-form/common/FilePreview.tsx","../src/components/ticket-form/create/TicketPriorityField.tsx","../src/components/ticket-form/create/TicketTagsDisplay.tsx","../src/components/common/TagChip.tsx","../src/components/ticket-form/create/TicketTagsField.tsx","../src/components/common/TagSelect.tsx","../src/components/dashboard/Dashboard.tsx","../src/components/ticket/TicketList.tsx","../src/components/ticket-form/edit/TicketDetailDialog.tsx","../src/components/common/StatusSelect.tsx","../src/components/common/StatusChip.tsx","../src/components/common/PrioritySelect.tsx","../src/components/common/PriorityChip.tsx","../src/components/common/TimeTrackingFields.tsx","../src/components/common/TicketGanttChart.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/types/public-types.ts","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/helpers/date-helper.ts","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/task-list/task-list-header.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/task-list/task-list-table.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/other/tooltip.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/other/vertical-scroll.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/task-list/task-list.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/grid/grid-body.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/grid/grid.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/calendar/top-part-of-calendar.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/calendar/calendar.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/node_modules/babel-plugin-transform-async-to-promises/helpers.mjs","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/other/arrow.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/helpers/bar-helper.ts","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/helpers/other-helper.ts","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/task-item/bar/bar-display.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/task-item/bar/bar-date-handle.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/task-item/bar/bar-progress-handle.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/task-item/bar/bar.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/task-item/bar/bar-small.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/task-item/milestone/milestone.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/task-item/project/project.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/task-item/task-item.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/gantt/task-gantt-content.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/gantt/task-gantt.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/other/horizontal-scroll.tsx","../../../node_modules/.pnpm/gantt-task-react@0.3.9_react@18.3.1/node_modules/gantt-task-react/src/components/gantt/gantt.tsx","../src/components/common/kanban/TicketKanban.tsx","../src/components/common/kanban/KanbanCard.tsx","../src/components/common/kanban/hooks/useKanbanBoard.ts","../src/components/ticket-form/common/AttachmentPreview.tsx","../src/components/ticket-form/chat/TicketChat.tsx","../src/components/ticket/TicketCard.tsx"],"sourcesContent":["import {\n Alert,\n AppBar,\n Box,\n Chip,\n Container,\n CssBaseline,\n FormControl,\n InputLabel,\n MenuItem,\n Paper,\n Select,\n Snackbar,\n ThemeProvider,\n Toolbar,\n Typography,\n createTheme,\n} from \"@mui/material\";\nimport {\n HelpdeskConfig,\n HelpdeskProvider,\n UserRole,\n useHelpdesk,\n} from \"../../context/HelpdeskContext\";\nimport React, { useState } from \"react\";\n\nimport { CreateTicketButton } from \"../ticket-form/create/CreateTicketButton\";\nimport { CreateTicketFormData } from \"../../schemas/ticket\";\nimport { Dashboard } from \"../dashboard/Dashboard\";\nimport { User } from \"@/types\";\n\nconst theme = createTheme({\n palette: {\n primary: {\n main: \"#1976d2\",\n },\n secondary: {\n main: \"#dc004e\",\n },\n },\n});\n\nconst HelpdeskAppContent: React.FC = () => {\n const [loading, setLoading] = useState(false);\n const [showSuccess, setShowSuccess] = useState(false);\n const [lastSubmittedData, setLastSubmittedData] =\n useState<CreateTicketFormData | null>(null);\n const { config, userRole, isAdmin, isAgent, updateUserRole } = useHelpdesk();\n\n // Données de démonstration pour le dashboard\n const mockStats = {\n totalTickets: 156,\n openTickets: 23,\n resolvedTickets: 98,\n inProgressTickets: 35,\n };\n\n const handleCreateTicket = async (data: CreateTicketFormData) => {\n setLoading(true);\n\n // Simuler un appel API\n await new Promise((resolve) => setTimeout(resolve, 1000));\n\n // Récupérer le label de la catégorie à partir de la value\n const categoryLabel =\n config.categories.find((cat) => cat.value === data.category)?.label ||\n data.category;\n\n console.log(\"Nouveau ticket créé:\", {\n ...data,\n categoryValue: data.category, // Value pour la base de données\n categoryLabel: categoryLabel, // Label pour l'affichage\n createdBy: userRole, // Rôle de l'utilisateur qui a créé le ticket\n });\n\n setLastSubmittedData(data);\n setLoading(false);\n setShowSuccess(true);\n };\n\n const handleCloseSuccess = () => {\n setShowSuccess(false);\n };\n\n const getRoleColor = (role: UserRole) => {\n switch (role) {\n case \"admin\":\n return \"error\";\n case \"agent\":\n return \"warning\";\n case \"user\":\n return \"primary\";\n default:\n return \"default\";\n }\n };\n\n const getRoleLabel = (role: UserRole) => {\n switch (role) {\n case \"admin\":\n return \"Administrateur\";\n case \"agent\":\n return \"Agent\";\n case \"user\":\n return \"Utilisateur\";\n default:\n return role;\n }\n };\n\n return (\n <ThemeProvider theme={theme}>\n <CssBaseline />\n <Box sx={{ flexGrow: 1 }}>\n <AppBar position=\"static\">\n <Toolbar>\n <Typography variant=\"h6\" component=\"div\" sx={{ flexGrow: 1 }}>\n Next-Helpdesk\n </Typography>\n\n {/* Sélecteur de rôle pour la démonstration */}\n <FormControl size=\"small\" sx={{ minWidth: 120, mr: 2 }}>\n <InputLabel sx={{ color: \"white\" }}>Rôle</InputLabel>\n <Select\n value={userRole}\n onChange={(e) => updateUserRole(e.target.value as UserRole)}\n sx={{\n color: \"white\",\n \"& .MuiSelect-icon\": { color: \"white\" },\n \"& .MuiOutlinedInput-notchedOutline\": {\n borderColor: \"rgba(255,255,255,0.3)\",\n },\n }}\n label=\"Rôle\"\n >\n <MenuItem value=\"user\">Utilisateur</MenuItem>\n <MenuItem value=\"agent\">Agent</MenuItem>\n <MenuItem value=\"admin\">Administrateur</MenuItem>\n </Select>\n </FormControl>\n\n <Chip\n label={getRoleLabel(userRole)}\n color={getRoleColor(userRole) as any}\n size=\"small\"\n sx={{ mr: 2 }}\n />\n\n <CreateTicketButton\n onSubmit={handleCreateTicket}\n loading={loading}\n variant=\"button\"\n buttonText=\"Nouveau ticket\"\n />\n </Toolbar>\n </AppBar>\n\n <Container maxWidth=\"lg\" sx={{ mt: 4 }}>\n <Dashboard stats={mockStats} />\n\n {/* Le client peut maintenant utiliser TicketList directement ici */}\n <Box sx={{ mt: 4 }}>\n <Typography variant=\"h5\" gutterBottom>\n Utilisez le composant TicketList ici\n </Typography>\n <Paper sx={{ p: 3, bgcolor: \"grey.50\" }}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n Importez et utilisez le composant TicketList avec vos propres\n données :\n </Typography>\n <Box\n component=\"pre\"\n sx={{ mt: 2, p: 2, bgcolor: \"white\", borderRadius: 1 }}\n >\n {`import { TicketList } from '@next-helpdesk/core';\n\n<TicketList\n tickets={yourTickets}\n onViewTicket={handleViewTicket}\n onEditTicket={handleEditTicket}\n onDeleteTicket={handleDeleteTicket}\n title=\"Mes Tickets\"\n/>`}\n </Box>\n </Paper>\n </Box>\n\n {/* Affichage des données soumises pour démonstration */}\n {lastSubmittedData && (\n <Box sx={{ mt: 4 }}>\n <Typography variant=\"h6\" gutterBottom>\n Dernier ticket créé (démonstration)\n </Typography>\n <Paper sx={{ p: 2, bgcolor: \"grey.50\" }}>\n <Typography\n variant=\"body2\"\n component=\"pre\"\n sx={{ fontFamily: \"monospace\" }}\n >\n {JSON.stringify(\n {\n ...lastSubmittedData,\n categoryValue: lastSubmittedData.category,\n categoryLabel: config.categories.find(\n (cat) => cat.value === lastSubmittedData.category\n )?.label,\n createdBy: userRole,\n isAdmin,\n isAgent,\n },\n null,\n 2\n )}\n </Typography>\n </Paper>\n </Box>\n )}\n\n {/* Affichage des permissions pour la démonstration */}\n <Box sx={{ mt: 4 }}>\n <Typography variant=\"h6\" gutterBottom>\n Permissions actuelles\n </Typography>\n <Paper sx={{ p: 2, bgcolor: \"grey.50\" }}>\n <Box display=\"flex\" gap={1} flexWrap=\"wrap\">\n <Chip\n label={`Admin: ${isAdmin ? \"Oui\" : \"Non\"}`}\n color={isAdmin ? \"success\" : \"default\"}\n size=\"small\"\n />\n <Chip\n label={`Agent: ${isAgent ? \"Oui\" : \"Non\"}`}\n color={isAgent ? \"success\" : \"default\"}\n size=\"small\"\n />\n <Chip\n label={`Utilisateur: ${!isAgent ? \"Oui\" : \"Non\"}`}\n color={!isAgent ? \"success\" : \"default\"}\n size=\"small\"\n />\n </Box>\n </Paper>\n </Box>\n </Container>\n\n <Snackbar\n open={showSuccess}\n autoHideDuration={6000}\n onClose={handleCloseSuccess}\n >\n <Alert\n onClose={handleCloseSuccess}\n severity=\"success\"\n sx={{ width: \"100%\" }}\n >\n Ticket créé avec succès !\n </Alert>\n </Snackbar>\n </Box>\n </ThemeProvider>\n );\n};\n\ninterface HelpdeskAppProps {\n config?: Partial<HelpdeskConfig>;\n userRole?: UserRole;\n currentUser: User;\n users: User[];\n}\n\nexport const HelpdeskApp: React.FC<HelpdeskAppProps> = ({\n config,\n userRole = \"user\",\n currentUser,\n users,\n}) => {\n return (\n <HelpdeskProvider\n config={config}\n userRole={userRole}\n currentUser={currentUser}\n users={users}\n >\n <HelpdeskAppContent />\n </HelpdeskProvider>\n );\n};\n","import { Priority, User } from \"../types\";\nimport React, { ReactNode, createContext, useContext } from \"react\";\n\nexport type UserRole = \"user\" | \"agent\" | \"admin\";\n\nexport interface StatusConfig {\n value: string;\n label: string;\n color?:\n | \"primary\"\n | \"secondary\"\n | \"error\"\n | \"info\"\n | \"success\"\n | \"warning\"\n | \"default\";\n}\n\nexport interface TagConfig {\n id: string; // Identifiant unique du tag\n label: string;\n color?: string; // Code hexadécimal ou couleur Material-UI\n}\n\nexport interface CategoryConfig {\n value: string;\n label: string;\n statuses: StatusConfig[];\n defaultStatus?: string;\n tags?: TagConfig[];\n}\n\nexport interface HelpdeskConfig {\n categories: CategoryConfig[];\n priorities: Array<{\n value: Priority;\n label: string;\n color?:\n | \"primary\"\n | \"secondary\"\n | \"error\"\n | \"info\"\n | \"success\"\n | \"warning\"\n | \"default\";\n }>;\n // Statuts globaux pour la compatibilité (optionnel)\n statuses?: StatusConfig[];\n defaultPriority: Priority;\n allowFileUpload?: boolean;\n maxFileSize?: number; // en MB\n allowedFileTypes?: string[];\n enableNotifications?: boolean;\n enableAutoAssign?: boolean;\n}\n\nexport interface HelpdeskContextType {\n config: HelpdeskConfig;\n userRole: UserRole;\n currentUser: User;\n users: User[];\n isAdmin: boolean;\n isAgent: boolean;\n isUser: boolean;\n updateConfig: (newConfig: Partial<HelpdeskConfig>) => void;\n updateUserRole: (role: UserRole) => void;\n setCurrentUser: (user: User) => void;\n setUsers: (users: User[]) => void;\n getStatusesForCategory: (category: string) => StatusConfig[];\n getDefaultStatusForCategory: (category: string) => string | undefined;\n getTagsForCategory: (category: string) => TagConfig[];\n addTagToCategory: (\n category: string,\n tag: TagConfig,\n onTagAdded?: (category: string, tag: TagConfig) => Promise<TagConfig>\n ) => Promise<TagConfig>;\n removeTagFromCategory: (\n category: string,\n tagId: string,\n onTagRemoved?: (category: string, tagId: string) => Promise<void>\n ) => Promise<void>;\n}\n\nconst defaultConfig: HelpdeskConfig = {\n categories: [\n {\n value: \"technical\",\n label: \"Technique\",\n statuses: [\n { value: \"open\", label: \"Ouvert\", color: \"primary\" },\n { value: \"in_progress\", label: \"En cours\", color: \"warning\" },\n { value: \"resolved\", label: \"Résolu\", color: \"success\" },\n { value: \"closed\", label: \"Fermé\", color: \"default\" },\n ],\n defaultStatus: \"open\",\n },\n {\n value: \"billing\",\n label: \"Facturation\",\n statuses: [\n { value: \"open\", label: \"Ouvert\", color: \"primary\" },\n { value: \"in_progress\", label: \"En traitement\", color: \"warning\" },\n { value: \"resolved\", label: \"Traité\", color: \"success\" },\n { value: \"closed\", label: \"Fermé\", color: \"default\" },\n ],\n defaultStatus: \"open\",\n },\n {\n value: \"account\",\n label: \"Compte utilisateur\",\n statuses: [\n { value: \"open\", label: \"Demande reçue\", color: \"primary\" },\n {\n value: \"in_progress\",\n label: \"En cours de traitement\",\n color: \"warning\",\n },\n { value: \"resolved\", label: \"Compte modifié\", color: \"success\" },\n { value: \"closed\", label: \"Terminé\", color: \"default\" },\n ],\n defaultStatus: \"open\",\n },\n {\n value: \"feature\",\n label: \"Fonctionnalité\",\n statuses: [\n { value: \"open\", label: \"Demande reçue\", color: \"primary\" },\n { value: \"in_progress\", label: \"En développement\", color: \"warning\" },\n { value: \"in_test\", label: \"En test\", color: \"info\" },\n { value: \"resolved\", label: \"Livré\", color: \"success\" },\n { value: \"closed\", label: \"Fermé\", color: \"default\" },\n ],\n defaultStatus: \"open\",\n },\n {\n value: \"bug\",\n label: \"Bug\",\n statuses: [\n { value: \"open\", label: \"Signalé\", color: \"error\" },\n { value: \"in_progress\", label: \"En correction\", color: \"warning\" },\n { value: \"in_test\", label: \"En test\", color: \"info\" },\n { value: \"resolved\", label: \"Corrigé\", color: \"success\" },\n { value: \"closed\", label: \"Fermé\", color: \"default\" },\n ],\n defaultStatus: \"open\",\n },\n {\n value: \"other\",\n label: \"Autre\",\n statuses: [\n { value: \"open\", label: \"Ouvert\", color: \"primary\" },\n { value: \"in_progress\", label: \"En cours\", color: \"warning\" },\n { value: \"resolved\", label: \"Résolu\", color: \"success\" },\n { value: \"closed\", label: \"Fermé\", color: \"default\" },\n ],\n defaultStatus: \"open\",\n },\n ],\n priorities: [\n { value: \"low\", label: \"Basse\", color: \"success\" },\n { value: \"medium\", label: \"Moyenne\", color: \"warning\" },\n { value: \"high\", label: \"Élevée\", color: \"error\" },\n ],\n // Statuts globaux pour la compatibilité\n statuses: [\n { value: \"open\", label: \"Ouvert\", color: \"primary\" },\n { value: \"in_progress\", label: \"En cours\", color: \"warning\" },\n { value: \"in_test\", label: \"En test\", color: \"info\" },\n { value: \"resolved\", label: \"Résolu\", color: \"success\" },\n { value: \"closed\", label: \"Fermé\", color: \"default\" },\n ],\n defaultPriority: \"medium\",\n allowFileUpload: true,\n maxFileSize: 10,\n allowedFileTypes: [\".jpg\", \".jpeg\", \".png\", \".gif\", \".pdf\", \".doc\", \".docx\"],\n enableNotifications: true,\n enableAutoAssign: false,\n};\n\nconst HelpdeskContext = createContext<HelpdeskContextType | undefined>(\n undefined\n);\n\ninterface HelpdeskProviderProps {\n children: ReactNode;\n config?: Partial<HelpdeskConfig>;\n userRole?: UserRole;\n currentUser: User;\n users: User[];\n onTagAdded?: (category: string, tag: TagConfig) => Promise<TagConfig>;\n onTagRemoved?: (category: string, tagId: string) => Promise<void>;\n}\n\nexport const HelpdeskProvider: React.FC<HelpdeskProviderProps> = ({\n children,\n config: userConfig = {},\n userRole = \"user\",\n currentUser: initialUser,\n users: initialUsers,\n onTagAdded,\n onTagRemoved,\n}) => {\n const [config, setConfig] = React.useState<HelpdeskConfig>({\n ...defaultConfig,\n ...userConfig,\n });\n\n const [role, setRole] = React.useState<UserRole>(userRole);\n const [currentUser, setCurrentUser] = React.useState<User>(initialUser);\n const [users, setUsers] = React.useState<User[]>(initialUsers);\n\n const updateConfig = React.useCallback(\n (newConfig: Partial<HelpdeskConfig>) => {\n setConfig((prev) => ({ ...prev, ...newConfig }));\n },\n []\n );\n\n const updateUserRole = React.useCallback((newRole: UserRole) => {\n setRole(newRole);\n }, []);\n\n // Fonction utilitaire pour obtenir les statuts d'une catégorie\n const getStatusesForCategory = React.useCallback(\n (category: string): StatusConfig[] => {\n const categoryConfig = config.categories.find(\n (c) => c.value === category\n );\n if (categoryConfig) {\n return categoryConfig.statuses;\n }\n // Fallback vers les statuts globaux si la catégorie n'est pas trouvée\n return config.statuses || [];\n },\n [config]\n );\n\n // Fonction utilitaire pour obtenir le statut par défaut d'une catégorie\n const getDefaultStatusForCategory = React.useCallback(\n (category: string): string | undefined => {\n const categoryConfig = config.categories.find(\n (c) => c.value === category\n );\n return categoryConfig?.defaultStatus;\n },\n [config]\n );\n\n // Fonction utilitaire pour obtenir les tags d'une catégorie\n const getTagsForCategory = React.useCallback(\n (category: string): TagConfig[] => {\n const categoryConfig = config.categories.find(\n (c) => c.value === category\n );\n return categoryConfig?.tags || [];\n },\n [config]\n );\n\n // Fonction pour ajouter un tag à une catégorie\n const addTagToCategory = React.useCallback(\n async (\n category: string,\n tag: TagConfig,\n callback?: (category: string, tag: TagConfig) => Promise<TagConfig>\n ): Promise<TagConfig> => {\n let finalTag = tag;\n\n // Appeler le callback global si fourni (pour la gestion côté client)\n if (onTagAdded) {\n try {\n finalTag = await onTagAdded(category, tag);\n } catch (error) {\n console.error(\"Erreur lors de l'ajout du tag:\", error);\n throw error; // Propager l'erreur\n }\n }\n\n setConfig((prevConfig) => {\n const updatedCategories = prevConfig.categories.map((cat) => {\n if (cat.value === category) {\n const existingTags = cat.tags || [];\n // Vérifier si le tag existe déjà\n const tagExists = existingTags.some((t) => t.id === finalTag.id);\n if (!tagExists) {\n const updatedTags = [...existingTags, finalTag];\n // Appeler le callback local si fourni\n if (callback) {\n callback(category, finalTag);\n }\n return { ...cat, tags: updatedTags };\n }\n }\n return cat;\n });\n return { ...prevConfig, categories: updatedCategories };\n });\n\n return finalTag;\n },\n [onTagAdded]\n );\n\n // Fonction pour supprimer un tag d'une catégorie\n const removeTagFromCategory = React.useCallback(\n async (\n category: string,\n tagId: string,\n callback?: (category: string, tagId: string) => Promise<void>\n ): Promise<void> => {\n // Appeler le callback global si fourni (pour la gestion côté client)\n if (onTagRemoved) {\n try {\n await onTagRemoved(category, tagId);\n } catch (error) {\n console.error(\"Erreur lors de la suppression du tag:\", error);\n throw error; // Propager l'erreur\n }\n }\n\n setConfig((prevConfig) => {\n const updatedCategories = prevConfig.categories.map((cat) => {\n if (cat.value === category) {\n const existingTags = cat.tags || [];\n const updatedTags = existingTags.filter((t) => t.id !== tagId);\n // Appeler le callback local si fourni\n if (callback) {\n callback(category, tagId);\n }\n return { ...cat, tags: updatedTags };\n }\n return cat;\n });\n return { ...prevConfig, categories: updatedCategories };\n });\n },\n [onTagRemoved]\n );\n\n const value: HelpdeskContextType = {\n config,\n userRole: role,\n currentUser,\n users,\n isAdmin: role === \"admin\",\n isAgent: role === \"agent\" || role === \"admin\",\n isUser: role === \"user\",\n updateConfig,\n updateUserRole,\n setCurrentUser,\n setUsers,\n getStatusesForCategory,\n getDefaultStatusForCategory,\n getTagsForCategory,\n addTagToCategory,\n removeTagFromCategory,\n };\n\n return (\n <HelpdeskContext.Provider value={value}>\n {children}\n </HelpdeskContext.Provider>\n );\n};\n\nexport const useHelpdesk = (): HelpdeskContextType => {\n const context = useContext(HelpdeskContext);\n if (context === undefined) {\n throw new Error(\"useHelpdesk must be used within a HelpdeskProvider\");\n }\n return context;\n};\n","import { Button, Fab } from \"@mui/material\";\nimport React, { useState } from \"react\";\n\nimport { Add } from \"@mui/icons-material\";\nimport { CreateTicketForm } from \"./CreateTicketForm\";\nimport { CreateTicketFormData } from \"@/schemas/ticket\";\n\ninterface CreateTicketButtonProps {\n onSubmit: (data: CreateTicketFormData) => Promise<void>;\n loading?: boolean;\n variant?: \"button\" | \"fab\";\n buttonText?: string;\n}\n\nexport const CreateTicketButton: React.FC<CreateTicketButtonProps> = ({\n onSubmit,\n loading = false,\n variant = \"button\",\n buttonText = \"Nouveau ticket\",\n}) => {\n const [open, setOpen] = useState(false);\n\n const handleOpen = () => setOpen(true);\n const handleClose = () => setOpen(false);\n\n const handleSubmit = async (data: CreateTicketFormData) => {\n await onSubmit(data);\n handleClose();\n };\n\n return (\n <>\n {variant === \"fab\" ? (\n <Fab\n color=\"primary\"\n aria-label=\"Créer un ticket\"\n onClick={handleOpen}\n disabled={loading}\n sx={{\n position: \"fixed\",\n bottom: 16,\n right: 16,\n zIndex: 1000,\n }}\n >\n <Add />\n </Fab>\n ) : (\n <Button\n variant=\"contained\"\n startIcon={<Add />}\n onClick={handleOpen}\n disabled={loading}\n >\n {buttonText}\n </Button>\n )}\n\n <CreateTicketForm\n open={open}\n onClose={handleClose}\n onSubmit={handleSubmit}\n loading={loading}\n />\n </>\n );\n};\n","import {\n Box,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Typography,\n} from \"@mui/material\";\nimport {\n CreateTicketFormData,\n createTicketSchema,\n} from \"../../../schemas/ticket\";\nimport { FormProvider, useForm } from \"react-hook-form\";\nimport React, { useState } from \"react\";\n\nimport { FilePreviewDialog } from \"../common/FilePreviewDialog\";\nimport { TicketAssignmentField } from \"./TicketAssignmentField\";\nimport { TicketBasicFields } from \"./TicketBasicFields\";\nimport { TicketCategoryField } from \"./TicketCategoryField\";\nimport { TicketFileUpload } from \"../common/TicketFileUpload\";\nimport { TicketPriorityField } from \"./TicketPriorityField\";\nimport { TicketTagsDisplay } from \"./TicketTagsDisplay\";\nimport { TicketTagsField } from \"./TicketTagsField\";\nimport { User } from \"../../../types\";\nimport { useHelpdesk } from \"../../../context/HelpdeskContext\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\n\ninterface CreateTicketFormProps {\n open: boolean;\n onClose: () => void;\n onSubmit: (data: CreateTicketFormData) => Promise<void>;\n loading?: boolean;\n}\n\nexport const CreateTicketForm: React.FC<CreateTicketFormProps> = ({\n open,\n onClose,\n onSubmit,\n loading = false,\n}) => {\n const { config, currentUser, users } = useHelpdesk();\n const [previewImage, setPreviewImage] = useState<{\n file: File;\n url: string;\n } | null>(null);\n const [isSubmitting, setIsSubmitting] = useState(false);\n\n const methods = useForm<CreateTicketFormData>({\n resolver: zodResolver(createTicketSchema),\n defaultValues: {\n title: \"\",\n description: \"\",\n category: \"\",\n priority: config.defaultPriority,\n assignedTo: \"\",\n tags: [],\n files: [],\n },\n });\n\n const {\n control,\n handleSubmit,\n reset,\n watch,\n formState: { errors },\n } = methods;\n\n const selectedCategory = watch(\"category\");\n\n const handleFormSubmit = async (data: CreateTicketFormData) => {\n try {\n setIsSubmitting(true);\n await onSubmit(data);\n reset();\n } catch (error) {\n console.error(\"Erreur lors de la création du ticket:\", error);\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const handleClose = () => {\n reset();\n onClose();\n };\n\n const handlePreview = (file: File, url: string) => {\n setPreviewImage({ file, url });\n };\n\n const handleRemoveFile = (fileToRemove: File) => {\n // La logique de suppression sera gérée par le composant TicketFileUpload\n };\n\n const isButtonLoading = loading || isSubmitting;\n\n return (\n <Dialog open={open} onClose={handleClose} maxWidth=\"md\" fullWidth>\n <DialogTitle sx={{ borderBottom: 1, borderColor: \"divider\" }}>\n <Typography variant=\"h6\">Créer un nouveau ticket</Typography>\n </DialogTitle>\n\n <FormProvider {...methods}>\n <form onSubmit={handleSubmit(handleFormSubmit)}>\n <DialogContent>\n <Box display=\"flex\" flexDirection=\"column\" gap={3}>\n <TicketCategoryField control={control} errors={errors} />\n\n <TicketPriorityField\n control={control}\n errors={errors}\n currentUser={currentUser}\n />\n\n <TicketBasicFields control={control} errors={errors} />\n\n <TicketAssignmentField\n control={control}\n users={users}\n currentUser={currentUser}\n />\n\n {selectedCategory &&\n (currentUser.role === \"admin\" ||\n currentUser.role === \"agent\" ? (\n <TicketTagsField\n control={control}\n errors={errors}\n category={selectedCategory}\n />\n ) : (\n <TicketTagsDisplay category={selectedCategory} />\n ))}\n\n <TicketFileUpload\n control={control}\n errors={errors}\n loading={isButtonLoading}\n onPreview={handlePreview}\n onRemove={handleRemoveFile}\n />\n </Box>\n </DialogContent>\n\n <DialogActions sx={{ borderTop: 1, borderColor: \"divider\" }}>\n <Button onClick={handleClose} disabled={isButtonLoading}>\n Annuler\n </Button>\n <Button\n type=\"submit\"\n variant=\"contained\"\n disabled={isButtonLoading}\n >\n {isButtonLoading ? \"Création...\" : \"Créer le ticket\"}\n </Button>\n </DialogActions>\n </form>\n </FormProvider>\n\n <FilePreviewDialog\n open={!!previewImage}\n onClose={() => setPreviewImage(null)}\n previewImage={previewImage}\n />\n </Dialog>\n );\n};\n","import { z } from 'zod';\n\nexport const createTicketSchema = z.object({\n title: z\n .string()\n .min(5, 'Le titre doit contenir au moins 5 caractères')\n .max(100, 'Le titre ne peut pas dépasser 100 caractères'),\n description: z\n .string()\n .min(10, 'La description doit contenir au moins 10 caractères')\n .max(1000, 'La description ne peut pas dépasser 1000 caractères'),\n category: z\n .string()\n .min(1, 'Veuillez sélectionner une catégorie'),\n priority: z.enum(['low', 'medium', 'high'], {\n required_error: 'Veuillez sélectionner une priorité',\n }),\n assignedTo: z\n .string()\n .optional(),\n tags: z\n .array(z.object({\n id: z.string(),\n label: z.string(),\n color: z.string().optional(), // Support des couleurs hexadécimales\n }))\n .optional(),\n files: z\n .any()\n .optional()\n .refine(\n (files) =>\n !files ||\n (Array.isArray(files) && files.every((f) => f instanceof File)),\n {\n message: 'Les fichiers doivent être de type File',\n }\n ),\n});\n\nexport const updateTicketSchema = createTicketSchema.extend({\n status: z.string().optional(),\n hoursSpent: z\n .number()\n .min(0, 'Le nombre d\\'heures ne peut pas être négatif')\n .max(1000, 'Le nombre d\\'heures ne peut pas dépasser 1000')\n .optional(),\n startDate: z\n .date()\n .optional(),\n endDate: z\n .date()\n .optional(),\n});\n\nexport type CreateTicketFormData = z.infer<typeof createTicketSchema>;\nexport type UpdateTicketFormData = z.infer<typeof updateTicketSchema>; ","import { Box, Dialog, Typography } from \"@mui/material\";\r\n\r\nimport React from \"react\";\r\n\r\ninterface FilePreviewDialogProps {\r\n open: boolean;\r\n onClose: () => void;\r\n previewImage: {\r\n file: File;\r\n url: string;\r\n } | null;\r\n}\r\n\r\nexport const FilePreviewDialog: React.FC<FilePreviewDialogProps> = ({\r\n open,\r\n onClose,\r\n previewImage,\r\n}) => {\r\n if (!previewImage) return null;\r\n\r\n return (\r\n <Dialog open={open} onClose={onClose} maxWidth=\"md\">\r\n <Box p={2} display=\"flex\" flexDirection=\"column\" alignItems=\"center\">\r\n {previewImage.file.type.startsWith(\"image/\") ? (\r\n <img\r\n src={previewImage.url}\r\n alt={previewImage.file.name}\r\n style={{ maxWidth: 600, maxHeight: 400, borderRadius: 8 }}\r\n />\r\n ) : previewImage.file.type === \"application/pdf\" ? (\r\n <iframe\r\n src={previewImage.url}\r\n width=\"600\"\r\n height=\"400\"\r\n style={{ border: \"none\", borderRadius: 8 }}\r\n title={previewImage.file.name}\r\n />\r\n ) : null}\r\n <Typography variant=\"body2\" mt={2}>\r\n {previewImage.file.name}\r\n </Typography>\r\n <Typography variant=\"caption\" color=\"text.secondary\">\r\n {(previewImage.file.size / 1024).toFixed(1)} Ko\r\n </Typography>\r\n </Box>\r\n </Dialog>\r\n );\r\n};\r\n","import { Control } from \"react-hook-form\";\r\nimport { CreateTicketFormData } from \"@/schemas/ticket\";\r\nimport React from \"react\";\r\nimport { User } from \"@/types\";\r\nimport { UserSelect } from \"@/components/common/UserSelect\";\r\n\r\ninterface TicketAssignmentFieldProps {\r\n control: Control<CreateTicketFormData>;\r\n users: User[];\r\n currentUser?: User;\r\n}\r\n\r\nexport const TicketAssignmentField: React.FC<TicketAssignmentFieldProps> = ({\r\n control,\r\n users,\r\n currentUser,\r\n}) => {\r\n const canAssign =\r\n users.length > 0 &&\r\n (currentUser?.role === \"admin\" || currentUser?.role === \"agent\");\r\n\r\n if (!canAssign) {\r\n return null;\r\n }\r\n\r\n return (\r\n <UserSelect\r\n name=\"assignedTo\"\r\n control={control}\r\n users={users}\r\n label=\"Assigner à\"\r\n placeholder=\"Sélectionner un agent ou admin...\"\r\n />\r\n );\r\n};\r\n","import {\n Avatar,\n Box,\n FormControl,\n IconButton,\n InputLabel,\n MenuItem,\n Select,\n Typography,\n} from \"@mui/material\";\nimport { Control, Controller } from \"react-hook-form\";\n\nimport { Close } from \"@mui/icons-material\";\nimport React from \"react\";\nimport { User } from \"../../types\";\nimport { UserAvatar } from \"./UserAvatar\";\n\ninterface UserSelectProps {\n name: string;\n control: Control<any>;\n users: User[];\n label?: string;\n error?: boolean;\n helperText?: string;\n placeholder?: string;\n}\n\nexport const UserSelect: React.FC<UserSelectProps> = ({\n name,\n control,\n users,\n label = \"Assigner à\",\n error = false,\n helperText,\n placeholder = \"Sélectionner un utilisateur...\",\n}) => {\n // Filtrer les utilisateurs qui sont admin ou agent\n const assignableUsers = users.filter(\n (user) => user.role === \"admin\" || user.role === \"agent\"\n );\n\n return (\n <Controller\n name={name}\n control={control}\n render={({ field }) => (\n <FormControl fullWidth error={error}>\n <InputLabel>{label}</InputLabel>\n <Select\n {...field}\n label={label}\n renderValue={(selected) => {\n if (!selected) {\n return \"\";\n }\n const selectedUser = assignableUsers.find(\n (user) => user.id === selected\n );\n if (!selectedUser) return null;\n return (\n <Box\n display=\"flex\"\n alignItems=\"center\"\n gap={1}\n sx={{ width: \"100%\" }}\n >\n <UserAvatar user={selectedUser} size={24} />\n <Box sx={{ flexGrow: 1 }}>\n <Typography variant=\"body2\">{selectedUser.name}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {selectedUser.role}\n </Typography>\n </Box>\n <IconButton\n size=\"small\"\n onMouseDown={(e) => {\n e.preventDefault();\n e.stopPropagation();\n }}\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n field.onChange(\"\");\n }}\n sx={{\n p: 0.5,\n \"&:hover\": { bgcolor: \"action.hover\" },\n }}\n >\n <Close fontSize=\"small\" />\n </IconButton>\n </Box>\n );\n }}\n >\n <MenuItem value=\"\">\n <Typography color=\"text.secondary\">{placeholder}</Typography>\n </MenuItem>\n {assignableUsers.map((user) => (\n <MenuItem key={user.id} value={user.id}>\n <Box display=\"flex\" alignItems=\"center\" gap={1}>\n <UserAvatar user={user} size={24} />\n <Box>\n <Typography variant=\"body2\">{user.name}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {user.role} • {user.email}\n </Typography>\n </Box>\n </Box>\n </MenuItem>\n ))}\n </Select>\n {helperText && (\n <Typography variant=\"caption\" color=\"error\">\n {helperText}\n </Typography>\n )}\n </FormControl>\n )}\n />\n );\n};\n","import { Avatar, AvatarProps, Box } from \"@mui/material\";\n\nimport React from \"react\";\nimport { User } from \"../../types\";\n\nexport interface UserAvatarProps extends Omit<AvatarProps, \"src\" | \"alt\"> {\n user: Omit<User, \"role\">;\n size?: number;\n}\n\nexport const UserAvatar: React.FC<UserAvatarProps> = ({\n user,\n size = 40,\n sx,\n ...avatarProps\n}) => {\n const getInitials = (name: string) => {\n return name\n .split(\" \")\n .map((word) => word.charAt(0))\n .join(\"\")\n .toUpperCase()\n .slice(0, 2);\n };\n\n const renderAvatarContent = () => {\n if (user.avatar) {\n if (typeof user.avatar === \"string\") {\n // URL d'image - sera géré par la prop src du composant Avatar\n return null;\n } else {\n // ReactNode personnalisé\n return user.avatar;\n }\n }\n\n // Fallback: initiales\n return getInitials(user.name);\n };\n\n const avatarStyles = {\n width: size,\n height: size,\n borderRadius: \"50%\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n backgroundColor: \"#e0e0e0\",\n color: \"#666\",\n fontSize: size * 0.4,\n fontWeight: \"bold\",\n };\n\n // Utiliser Avatar seulement si user.avatar est une string (URL d'image)\n if (typeof user.avatar === \"string\") {\n return (\n <Avatar\n src={user.avatar}\n alt={user.name}\n sx={{\n ...avatarStyles,\n ...sx,\n }}\n {...avatarProps}\n />\n );\n }\n\n // Pour les autres cas (ReactNode personnalisé ou initiales), utiliser un Box\n return (\n <Box\n sx={{\n ...avatarStyles,\n ...sx,\n }}\n >\n {renderAvatarContent()}\n </Box>\n );\n};\n","import { Control, Controller } from \"react-hook-form\";\r\n\r\nimport { CreateTicketFormData } from \"@/schemas/ticket\";\r\nimport React from \"react\";\r\nimport { TextField } from \"@mui/material\";\r\nimport { capitalizeFirstChar } from \"@/utils\";\r\n\r\ninterface TicketBasicFieldsProps {\r\n control: Control<CreateTicketFormData>;\r\n errors: any;\r\n}\r\n\r\nexport const TicketBasicFields: React.FC<TicketBasicFieldsProps> = ({\r\n control,\r\n errors,\r\n}) => {\r\n return (\r\n <>\r\n <Controller\r\n name=\"title\"\r\n control={control}\r\n render={({ field }) => (\r\n <TextField\r\n {...field}\r\n label=\"Titre du ticket\"\r\n fullWidth\r\n error={!!errors.title}\r\n helperText={errors.title?.message}\r\n placeholder=\"Ex: Problème de connexion à l'application\"\r\n onChange={(e) => {\r\n const capitalizedValue = capitalizeFirstChar(e.target.value);\r\n field.onChange(capitalizedValue);\r\n }}\r\n />\r\n )}\r\n />\r\n\r\n <Controller\r\n name=\"description\"\r\n control={control}\r\n render={({ field }) => (\r\n <TextField\r\n {...field}\r\n label=\"Description\"\r\n fullWidth\r\n multiline\r\n rows={4}\r\n error={!!errors.description}\r\n helperText={errors.description?.message}\r\n placeholder=\"Décrivez votre problème en détail...\"\r\n onChange={(e) => {\r\n const capitalizedValue = capitalizeFirstChar(e.target.value);\r\n field.onChange(capitalizedValue);\r\n }}\r\n />\r\n )}\r\n />\r\n </>\r\n );\r\n};\r\n","import { Ticket, User } from \"../types\";\n\nexport type UserRole = \"user\" | \"agent\" | \"admin\";\n\n/**\n * Vérifie si un utilisateur peut modifier un ticket\n * @param ticket - Le ticket à vérifier\n * @param currentUser - L'utilisateur actuel\n * @returns true si l'utilisateur peut modifier le ticket\n */\nexport const canEditTicket = (\n ticket: Ticket,\n currentUser: User\n): boolean => {\n if (currentUser.role === \"admin\") return true; // Admins peuvent modifier tous les tickets\n if (currentUser.role === \"agent\") return ticket.author.id === currentUser.id; // Agents peuvent modifier leurs propres tickets\n return false; // Users ne peuvent pas modifier\n};\n\n/**\n * Vérifie si un utilisateur peut supprimer un ticket\n * @param currentUser - L'utilisateur actuel\n * @returns true si l'utilisateur peut supprimer le ticket\n */\nexport const canDeleteTicket = (currentUser: User): boolean => {\n return currentUser.role === \"admin\"; // Seuls les admins peuvent supprimer\n};\n\n/**\n * Vérifie si un utilisateur peut voir un ticket\n * @param ticket - Le ticket à vérifier\n * @param currentUser - L'utilisateur actuel\n * @returns true si l'utilisateur peut voir le ticket\n */\nexport const canViewTicket = (\n ticket: Ticket,\n currentUser: User\n): boolean => {\n if (currentUser.role === \"admin\" || currentUser.role === \"agent\") return true; // Agents et admins voient tous les tickets\n return ticket.author.id === currentUser.id; // Users voient seulement leurs tickets\n};\n\n/**\n * Filtre une liste de tickets selon les permissions de l'utilisateur\n * @param tickets - La liste des tickets\n * @param currentUser - L'utilisateur actuel\n * @returns La liste filtrée des tickets\n */\nexport const filterTicketsByPermission = (\n tickets: Ticket[],\n currentUser: User\n): Ticket[] => {\n if (currentUser.role === \"admin\" || currentUser.role === \"agent\") {\n return tickets; // Agents et admins voient tous les tickets\n }\n return tickets.filter(ticket => ticket.author.id === currentUser.id); // Users voient seulement leurs tickets\n};\n\n/**\n * Vérifie si un utilisateur peut voir tous les tickets\n * @param currentUser - L'utilisateur actuel\n * @returns true si l'utilisateur peut voir tous les tickets\n */\nexport const canViewAllTickets = (currentUser: User): boolean => {\n return currentUser.role === \"admin\" || currentUser.role === \"agent\";\n};\n\n/**\n * Vérifie si un utilisateur peut voir seulement ses propres tickets\n * @param currentUser - L'utilisateur actuel\n * @returns true si l'utilisateur peut voir seulement ses propres tickets\n */\nexport const canViewOwnTickets = (currentUser: User): boolean => {\n return currentUser.role === \"user\";\n}; ","/**\r\n * Utilitaires pour la manipulation de chaînes de caractères\r\n */\r\n\r\n/**\r\n * Capitalise le premier caractère d'une chaîne\r\n * @param value - La chaîne à capitaliser\r\n * @returns La chaîne avec le premier caractère en majuscule\r\n */\r\nexport const capitalizeFirstChar = (value: string): string => {\r\n if (!value) return value;\r\n return value.charAt(0).toUpperCase() + value.slice(1);\r\n};\r\n\r\n/**\r\n * Capitalise la première lettre de chaque mot dans une chaîne\r\n * @param value - La chaîne à capitaliser\r\n * @returns La chaîne avec chaque mot capitalisé\r\n */\r\nexport const capitalizeWords = (value: string): string => {\r\n if (!value) return value;\r\n return value\r\n .split(' ')\r\n .map(word => capitalizeFirstChar(word))\r\n .join(' ');\r\n};\r\n\r\n/**\r\n * Met en minuscules une chaîne avec capitalisation de la première lettre\r\n * @param value - La chaîne à formater\r\n * @returns La chaîne formatée (première lettre majuscule, reste en minuscules)\r\n */\r\nexport const toTitleCase = (value: string): string => {\r\n if (!value) return value;\r\n return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();\r\n}; ","import { HelpdeskConfig, StatusConfig } from \"../context/HelpdeskContext\";\r\n\r\ntype StatusColor = \"primary\" | \"secondary\" | \"error\" | \"info\" | \"success\" | \"warning\" | \"default\";\r\n\r\n/**\r\n * Obtient la couleur d'un statut basé sur la configuration\r\n * @param statusValue - La valeur du statut\r\n * @param statuses - La liste des statuts de la configuration\r\n * @returns La couleur du statut ou \"default\" si non trouvé\r\n */\r\nexport const getStatusColor = (\r\n statusValue: string,\r\n statuses: StatusConfig[]\r\n): StatusColor => {\r\n const status = statuses.find((s) => s.value === statusValue);\r\n return status?.color || \"default\";\r\n};\r\n\r\n/**\r\n * Obtient le label d'un statut basé sur la configuration\r\n * @param statusValue - La valeur du statut\r\n * @param statuses - La liste des statuts de la configuration\r\n * @returns Le label du statut ou la valeur si non trouvé\r\n */\r\nexport const getStatusLabel = (\r\n statusValue: string,\r\n statuses: StatusConfig[]\r\n): string => {\r\n const status = statuses.find((s) => s.value === statusValue);\r\n return status?.label || statusValue;\r\n};\r\n\r\n/**\r\n * Vérifie si un statut existe dans la configuration\r\n * @param statusValue - La valeur du statut\r\n * @param statuses - La liste des statuts de la configuration\r\n * @returns true si le statut existe, false sinon\r\n */\r\nexport const isValidStatus = (\r\n statusValue: string,\r\n statuses: StatusConfig[]\r\n): boolean => {\r\n return statuses.some((s) => s.value === statusValue);\r\n};\r\n\r\n/**\r\n * Obtient les statuts disponibles pour une catégorie donnée\r\n * @param category - La catégorie\r\n * @param config - La configuration du helpdesk\r\n * @returns La liste des statuts pour cette catégorie\r\n */\r\nexport function getStatusesForCategory(\r\n category: string | undefined,\r\n config: HelpdeskConfig\r\n): StatusConfig[] {\r\n if (category) {\r\n const cat = config.categories.find(c => c.value.toLowerCase() === category.toLowerCase());\r\n if (cat && cat.statu