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,456 lines (1,422 loc) 240 kB
var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __objRest = (source, exclude) => { var target = {}; for (var prop in source) if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0) target[prop] = source[prop]; if (source != null && __getOwnPropSymbols) for (var prop of __getOwnPropSymbols(source)) { if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop)) target[prop] = source[prop]; } return target; }; // src/components/app/HelpdeskApp.tsx import { Alert, AppBar, Box as Box10, Chip as Chip3, Container, CssBaseline, FormControl as FormControl4, InputLabel as InputLabel4, MenuItem as MenuItem4, Paper as Paper2, Select as Select4, Snackbar, ThemeProvider, Toolbar, Typography as Typography11, createTheme } from "@mui/material"; // src/context/HelpdeskContext.tsx import React, { createContext, useContext } from "react"; var defaultConfig = { categories: [ { value: "technical", label: "Technique", statuses: [ { value: "open", label: "Ouvert", color: "primary" }, { value: "in_progress", label: "En cours", color: "warning" }, { value: "resolved", label: "R\xE9solu", color: "success" }, { value: "closed", label: "Ferm\xE9", color: "default" } ], defaultStatus: "open" }, { value: "billing", label: "Facturation", statuses: [ { value: "open", label: "Ouvert", color: "primary" }, { value: "in_progress", label: "En traitement", color: "warning" }, { value: "resolved", label: "Trait\xE9", color: "success" }, { value: "closed", label: "Ferm\xE9", color: "default" } ], defaultStatus: "open" }, { value: "account", label: "Compte utilisateur", statuses: [ { value: "open", label: "Demande re\xE7ue", color: "primary" }, { value: "in_progress", label: "En cours de traitement", color: "warning" }, { value: "resolved", label: "Compte modifi\xE9", color: "success" }, { value: "closed", label: "Termin\xE9", color: "default" } ], defaultStatus: "open" }, { value: "feature", label: "Fonctionnalit\xE9", statuses: [ { value: "open", label: "Demande re\xE7ue", color: "primary" }, { value: "in_progress", label: "En d\xE9veloppement", color: "warning" }, { value: "in_test", label: "En test", color: "info" }, { value: "resolved", label: "Livr\xE9", color: "success" }, { value: "closed", label: "Ferm\xE9", color: "default" } ], defaultStatus: "open" }, { value: "bug", label: "Bug", statuses: [ { value: "open", label: "Signal\xE9", color: "error" }, { value: "in_progress", label: "En correction", color: "warning" }, { value: "in_test", label: "En test", color: "info" }, { value: "resolved", label: "Corrig\xE9", color: "success" }, { value: "closed", label: "Ferm\xE9", color: "default" } ], defaultStatus: "open" }, { value: "other", label: "Autre", statuses: [ { value: "open", label: "Ouvert", color: "primary" }, { value: "in_progress", label: "En cours", color: "warning" }, { value: "resolved", label: "R\xE9solu", color: "success" }, { value: "closed", label: "Ferm\xE9", color: "default" } ], defaultStatus: "open" } ], priorities: [ { value: "low", label: "Basse", color: "success" }, { value: "medium", label: "Moyenne", color: "warning" }, { value: "high", label: "\xC9lev\xE9e", color: "error" } ], // Statuts globaux pour la compatibilité statuses: [ { value: "open", label: "Ouvert", color: "primary" }, { value: "in_progress", label: "En cours", color: "warning" }, { value: "in_test", label: "En test", color: "info" }, { value: "resolved", label: "R\xE9solu", color: "success" }, { value: "closed", label: "Ferm\xE9", color: "default" } ], defaultPriority: "medium", allowFileUpload: true, maxFileSize: 10, allowedFileTypes: [".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx"], enableNotifications: true, enableAutoAssign: false }; var HelpdeskContext = createContext( void 0 ); var HelpdeskProvider = ({ children, config: userConfig = {}, userRole = "user", currentUser: initialUser, users: initialUsers, onTagAdded, onTagRemoved }) => { const [config, setConfig] = React.useState(__spreadValues(__spreadValues({}, defaultConfig), userConfig)); const [role, setRole] = React.useState(userRole); const [currentUser, setCurrentUser] = React.useState(initialUser); const [users, setUsers] = React.useState(initialUsers); const updateConfig = React.useCallback( (newConfig) => { setConfig((prev) => __spreadValues(__spreadValues({}, prev), newConfig)); }, [] ); const updateUserRole = React.useCallback((newRole) => { setRole(newRole); }, []); const getStatusesForCategory2 = React.useCallback( (category) => { const categoryConfig = config.categories.find( (c) => c.value === category ); if (categoryConfig) { return categoryConfig.statuses; } return config.statuses || []; }, [config] ); const getDefaultStatusForCategory2 = React.useCallback( (category) => { const categoryConfig = config.categories.find( (c) => c.value === category ); return categoryConfig == null ? void 0 : categoryConfig.defaultStatus; }, [config] ); const getTagsForCategory = React.useCallback( (category) => { const categoryConfig = config.categories.find( (c) => c.value === category ); return (categoryConfig == null ? void 0 : categoryConfig.tags) || []; }, [config] ); const addTagToCategory = React.useCallback( async (category, tag, callback) => { let finalTag = tag; if (onTagAdded) { try { finalTag = await onTagAdded(category, tag); } catch (error) { console.error("Erreur lors de l'ajout du tag:", error); throw error; } } setConfig((prevConfig) => { const updatedCategories = prevConfig.categories.map((cat) => { if (cat.value === category) { const existingTags = cat.tags || []; const tagExists = existingTags.some((t) => t.id === finalTag.id); if (!tagExists) { const updatedTags = [...existingTags, finalTag]; if (callback) { callback(category, finalTag); } return __spreadProps(__spreadValues({}, cat), { tags: updatedTags }); } } return cat; }); return __spreadProps(__spreadValues({}, prevConfig), { categories: updatedCategories }); }); return finalTag; }, [onTagAdded] ); const removeTagFromCategory = React.useCallback( async (category, tagId, callback) => { if (onTagRemoved) { try { await onTagRemoved(category, tagId); } catch (error) { console.error("Erreur lors de la suppression du tag:", error); throw error; } } setConfig((prevConfig) => { const updatedCategories = prevConfig.categories.map((cat) => { if (cat.value === category) { const existingTags = cat.tags || []; const updatedTags = existingTags.filter((t) => t.id !== tagId); if (callback) { callback(category, tagId); } return __spreadProps(__spreadValues({}, cat), { tags: updatedTags }); } return cat; }); return __spreadProps(__spreadValues({}, prevConfig), { categories: updatedCategories }); }); }, [onTagRemoved] ); const value = { config, userRole: role, currentUser, users, isAdmin: role === "admin", isAgent: role === "agent" || role === "admin", isUser: role === "user", updateConfig, updateUserRole, setCurrentUser, setUsers, getStatusesForCategory: getStatusesForCategory2, getDefaultStatusForCategory: getDefaultStatusForCategory2, getTagsForCategory, addTagToCategory, removeTagFromCategory }; return /* @__PURE__ */ React.createElement(HelpdeskContext.Provider, { value }, children); }; var useHelpdesk = () => { const context = useContext(HelpdeskContext); if (context === void 0) { throw new Error("useHelpdesk must be used within a HelpdeskProvider"); } return context; }; // src/components/app/HelpdeskApp.tsx import React18, { useState as useState4 } from "react"; // src/components/ticket-form/create/CreateTicketButton.tsx import { Button as Button4, Fab } from "@mui/material"; import React16, { useState as useState3 } from "react"; import { Add } from "@mui/icons-material"; // src/components/ticket-form/create/CreateTicketForm.tsx import { Box as Box8, Button as Button3, Dialog as Dialog3, DialogActions as DialogActions2, DialogContent as DialogContent2, DialogTitle as DialogTitle2, Typography as Typography9 } from "@mui/material"; // src/schemas/ticket.ts import { z } from "zod"; var createTicketSchema = z.object({ title: z.string().min(5, "Le titre doit contenir au moins 5 caract\xE8res").max(100, "Le titre ne peut pas d\xE9passer 100 caract\xE8res"), description: z.string().min(10, "La description doit contenir au moins 10 caract\xE8res").max(1e3, "La description ne peut pas d\xE9passer 1000 caract\xE8res"), category: z.string().min(1, "Veuillez s\xE9lectionner une cat\xE9gorie"), priority: z.enum(["low", "medium", "high"], { required_error: "Veuillez s\xE9lectionner une priorit\xE9" }), assignedTo: z.string().optional(), tags: z.array(z.object({ id: z.string(), label: z.string(), color: z.string().optional() // Support des couleurs hexadécimales })).optional(), files: z.any().optional().refine( (files) => !files || Array.isArray(files) && files.every((f) => f instanceof File), { message: "Les fichiers doivent \xEAtre de type File" } ) }); var updateTicketSchema = createTicketSchema.extend({ status: z.string().optional(), hoursSpent: z.number().min(0, "Le nombre d'heures ne peut pas \xEAtre n\xE9gatif").max(1e3, "Le nombre d'heures ne peut pas d\xE9passer 1000").optional(), startDate: z.date().optional(), endDate: z.date().optional() }); // src/components/ticket-form/create/CreateTicketForm.tsx import { FormProvider, useForm } from "react-hook-form"; import React15, { useState as useState2 } from "react"; // src/components/ticket-form/common/FilePreviewDialog.tsx import { Box, Dialog, Typography } from "@mui/material"; import React2 from "react"; var FilePreviewDialog = ({ open, onClose, previewImage }) => { if (!previewImage) return null; return /* @__PURE__ */ React2.createElement(Dialog, { open, onClose, maxWidth: "md" }, /* @__PURE__ */ React2.createElement(Box, { p: 2, display: "flex", flexDirection: "column", alignItems: "center" }, previewImage.file.type.startsWith("image/") ? /* @__PURE__ */ React2.createElement( "img", { src: previewImage.url, alt: previewImage.file.name, style: { maxWidth: 600, maxHeight: 400, borderRadius: 8 } } ) : previewImage.file.type === "application/pdf" ? /* @__PURE__ */ React2.createElement( "iframe", { src: previewImage.url, width: "600", height: "400", style: { border: "none", borderRadius: 8 }, title: previewImage.file.name } ) : null, /* @__PURE__ */ React2.createElement(Typography, { variant: "body2", mt: 2 }, previewImage.file.name), /* @__PURE__ */ React2.createElement(Typography, { variant: "caption", color: "text.secondary" }, (previewImage.file.size / 1024).toFixed(1), " Ko"))); }; // src/components/ticket-form/create/TicketAssignmentField.tsx import React5 from "react"; // src/components/common/UserSelect.tsx import { Box as Box3, FormControl, IconButton, InputLabel, MenuItem, Select, Typography as Typography2 } from "@mui/material"; import { Controller } from "react-hook-form"; import { Close } from "@mui/icons-material"; import React4 from "react"; // src/components/common/UserAvatar.tsx import { Avatar, Box as Box2 } from "@mui/material"; import React3 from "react"; var UserAvatar = (_a) => { var _b = _a, { user, size = 40, sx } = _b, avatarProps = __objRest(_b, [ "user", "size", "sx" ]); const getInitials = (name) => { return name.split(" ").map((word) => word.charAt(0)).join("").toUpperCase().slice(0, 2); }; const renderAvatarContent = () => { if (user.avatar) { if (typeof user.avatar === "string") { return null; } else { return user.avatar; } } return getInitials(user.name); }; const avatarStyles = { width: size, height: size, borderRadius: "50%", display: "flex", alignItems: "center", justifyContent: "center", backgroundColor: "#e0e0e0", color: "#666", fontSize: size * 0.4, fontWeight: "bold" }; if (typeof user.avatar === "string") { return /* @__PURE__ */ React3.createElement( Avatar, __spreadValues({ src: user.avatar, alt: user.name, sx: __spreadValues(__spreadValues({}, avatarStyles), sx) }, avatarProps) ); } return /* @__PURE__ */ React3.createElement( Box2, { sx: __spreadValues(__spreadValues({}, avatarStyles), sx) }, renderAvatarContent() ); }; // src/components/common/UserSelect.tsx var UserSelect = ({ name, control, users, label = "Assigner \xE0", error = false, helperText, placeholder = "S\xE9lectionner un utilisateur..." }) => { const assignableUsers = users.filter( (user) => user.role === "admin" || user.role === "agent" ); return /* @__PURE__ */ React4.createElement( Controller, { name, control, render: ({ field }) => /* @__PURE__ */ React4.createElement(FormControl, { fullWidth: true, error }, /* @__PURE__ */ React4.createElement(InputLabel, null, label), /* @__PURE__ */ React4.createElement( Select, __spreadProps(__spreadValues({}, field), { label, renderValue: (selected) => { if (!selected) { return ""; } const selectedUser = assignableUsers.find( (user) => user.id === selected ); if (!selectedUser) return null; return /* @__PURE__ */ React4.createElement( Box3, { display: "flex", alignItems: "center", gap: 1, sx: { width: "100%" } }, /* @__PURE__ */ React4.createElement(UserAvatar, { user: selectedUser, size: 24 }), /* @__PURE__ */ React4.createElement(Box3, { sx: { flexGrow: 1 } }, /* @__PURE__ */ React4.createElement(Typography2, { variant: "body2" }, selectedUser.name), /* @__PURE__ */ React4.createElement(Typography2, { variant: "caption", color: "text.secondary" }, selectedUser.role)), /* @__PURE__ */ React4.createElement( IconButton, { size: "small", onMouseDown: (e) => { e.preventDefault(); e.stopPropagation(); }, onClick: (e) => { e.preventDefault(); e.stopPropagation(); field.onChange(""); }, sx: { p: 0.5, "&:hover": { bgcolor: "action.hover" } } }, /* @__PURE__ */ React4.createElement(Close, { fontSize: "small" }) ) ); } }), /* @__PURE__ */ React4.createElement(MenuItem, { value: "" }, /* @__PURE__ */ React4.createElement(Typography2, { color: "text.secondary" }, placeholder)), assignableUsers.map((user) => /* @__PURE__ */ React4.createElement(MenuItem, { key: user.id, value: user.id }, /* @__PURE__ */ React4.createElement(Box3, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ React4.createElement(UserAvatar, { user, size: 24 }), /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Typography2, { variant: "body2" }, user.name), /* @__PURE__ */ React4.createElement(Typography2, { variant: "caption", color: "text.secondary" }, user.role, " \u2022 ", user.email))))) ), helperText && /* @__PURE__ */ React4.createElement(Typography2, { variant: "caption", color: "error" }, helperText)) } ); }; // src/components/ticket-form/create/TicketAssignmentField.tsx var TicketAssignmentField = ({ control, users, currentUser }) => { const canAssign = users.length > 0 && ((currentUser == null ? void 0 : currentUser.role) === "admin" || (currentUser == null ? void 0 : currentUser.role) === "agent"); if (!canAssign) { return null; } return /* @__PURE__ */ React5.createElement( UserSelect, { name: "assignedTo", control, users, label: "Assigner \xE0", placeholder: "S\xE9lectionner un agent ou admin..." } ); }; // src/components/ticket-form/create/TicketBasicFields.tsx import { Controller as Controller2 } from "react-hook-form"; import React6 from "react"; import { TextField } from "@mui/material"; // src/utils/permissions.ts var canEditTicket = (ticket, currentUser) => { if (currentUser.role === "admin") return true; if (currentUser.role === "agent") return ticket.author.id === currentUser.id; return false; }; var canDeleteTicket = (currentUser) => { return currentUser.role === "admin"; }; var canViewTicket = (ticket, currentUser) => { if (currentUser.role === "admin" || currentUser.role === "agent") return true; return ticket.author.id === currentUser.id; }; var filterTicketsByPermission = (tickets, currentUser) => { if (currentUser.role === "admin" || currentUser.role === "agent") { return tickets; } return tickets.filter((ticket) => ticket.author.id === currentUser.id); }; var canViewAllTickets = (currentUser) => { return currentUser.role === "admin" || currentUser.role === "agent"; }; var canViewOwnTickets = (currentUser) => { return currentUser.role === "user"; }; // src/utils/string.ts var capitalizeFirstChar = (value) => { if (!value) return value; return value.charAt(0).toUpperCase() + value.slice(1); }; var capitalizeWords = (value) => { if (!value) return value; return value.split(" ").map((word) => capitalizeFirstChar(word)).join(" "); }; var toTitleCase = (value) => { if (!value) return value; return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase(); }; // src/utils/status.ts var getStatusColor = (statusValue, statuses) => { const status = statuses.find((s) => s.value === statusValue); return (status == null ? void 0 : status.color) || "default"; }; var getStatusLabel = (statusValue, statuses) => { const status = statuses.find((s) => s.value === statusValue); return (status == null ? void 0 : status.label) || statusValue; }; var isValidStatus = (statusValue, statuses) => { return statuses.some((s) => s.value === statusValue); }; function getStatusesForCategory(category, config) { if (category) { const cat = config.categories.find((c) => c.value.toLowerCase() === category.toLowerCase()); if (cat && cat.statuses && cat.statuses.length > 0) { return cat.statuses; } } return config.statuses || []; } var getDefaultStatusForCategory = (category, config) => { const categoryConfig = config.categories.find((c) => c.value.toLowerCase() === category.toLowerCase()); return categoryConfig == null ? void 0 : categoryConfig.defaultStatus; }; // src/utils/priority.ts var getPriorityColor = (priorityValue, priorities) => { const priority = priorities.find((p) => p.value === priorityValue); return (priority == null ? void 0 : priority.color) || "default"; }; var getPriorityLabel = (priorityValue, priorities) => { const priority = priorities.find((p) => p.value === priorityValue); return (priority == null ? void 0 : priority.label) || priorityValue; }; var isValidPriority = (priorityValue, priorities) => { return priorities.some((p) => p.value === priorityValue); }; // src/utils/category.ts var getCategoryLabel = (categoryValue, categories) => { const category = categories.find((c) => c.value === categoryValue); return (category == null ? void 0 : category.label) || categoryValue; }; var isValidCategory = (categoryValue, categories) => { return categories.some((c) => c.value === categoryValue); }; var getCategoryConfig = (categoryValue, categories) => { return categories.find((c) => c.value === categoryValue); }; // src/components/ticket-form/create/TicketBasicFields.tsx var TicketBasicFields = ({ control, errors }) => { return /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement( Controller2, { name: "title", control, render: ({ field }) => { var _a; return /* @__PURE__ */ React6.createElement( TextField, __spreadProps(__spreadValues({}, field), { label: "Titre du ticket", fullWidth: true, error: !!errors.title, helperText: (_a = errors.title) == null ? void 0 : _a.message, placeholder: "Ex: Probl\xE8me de connexion \xE0 l'application", onChange: (e) => { const capitalizedValue = capitalizeFirstChar(e.target.value); field.onChange(capitalizedValue); } }) ); } } ), /* @__PURE__ */ React6.createElement( Controller2, { name: "description", control, render: ({ field }) => { var _a; return /* @__PURE__ */ React6.createElement( TextField, __spreadProps(__spreadValues({}, field), { label: "Description", fullWidth: true, multiline: true, rows: 4, error: !!errors.description, helperText: (_a = errors.description) == null ? void 0 : _a.message, placeholder: "D\xE9crivez votre probl\xE8me en d\xE9tail...", onChange: (e) => { const capitalizedValue = capitalizeFirstChar(e.target.value); field.onChange(capitalizedValue); } }) ); } } )); }; // src/components/ticket-form/create/TicketCategoryField.tsx import { Controller as Controller3 } from "react-hook-form"; import { FormControl as FormControl2, InputLabel as InputLabel2, MenuItem as MenuItem2, Select as Select2, Typography as Typography3 } from "@mui/material"; import React7 from "react"; var TicketCategoryField = ({ control, errors }) => { const { config } = useHelpdesk(); return /* @__PURE__ */ React7.createElement( Controller3, { name: "category", control, render: ({ field }) => /* @__PURE__ */ React7.createElement(FormControl2, { fullWidth: true, error: !!errors.category }, /* @__PURE__ */ React7.createElement(InputLabel2, null, "Cat\xE9gorie"), /* @__PURE__ */ React7.createElement(Select2, __spreadProps(__spreadValues({}, field), { label: "Cat\xE9gorie" }), config.categories.map((category) => /* @__PURE__ */ React7.createElement(MenuItem2, { key: category.value, value: category.value }, category.label))), errors.category && /* @__PURE__ */ React7.createElement(Typography3, { variant: "caption", color: "error" }, errors.category.message)) } ); }; // src/components/ticket-form/common/TicketFileUpload.tsx import { Box as Box5, Button, Typography as Typography5 } from "@mui/material"; import { useFormContext, useWatch } from "react-hook-form"; import React9, { useRef } from "react"; // src/components/ticket-form/common/FilePreview.tsx import { Avatar as Avatar3, Box as Box4, IconButton as IconButton2, Typography as Typography4 } from "@mui/material"; import DeleteIcon from "@mui/icons-material/Delete"; import DescriptionIcon from "@mui/icons-material/Description"; import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf"; import React8 from "react"; import TableChartIcon from "@mui/icons-material/TableChart"; var FilePreview = ({ file, onPreview, onRemove }) => { const [previewUrl, setPreviewUrl] = React8.useState(null); React8.useEffect(() => { if (file.type.startsWith("image/")) { const url = URL.createObjectURL(file); setPreviewUrl(url); return () => URL.revokeObjectURL(url); } else if (file.type === "application/pdf") { const url = URL.createObjectURL(file); setPreviewUrl(url); return () => URL.revokeObjectURL(url); } else { setPreviewUrl(null); } }, [file]); const getFileIcon = () => { if (file.type === "application/pdf") { return /* @__PURE__ */ React8.createElement(PictureAsPdfIcon, { color: "error" }); } else if (file.type.includes("spreadsheet") || file.name.endsWith(".xlsx") || file.name.endsWith(".xls")) { return /* @__PURE__ */ React8.createElement(TableChartIcon, { color: "success" }); } else if (file.type.includes("document") || file.name.endsWith(".docx") || file.name.endsWith(".doc")) { return /* @__PURE__ */ React8.createElement(DescriptionIcon, { color: "primary" }); } else { return /* @__PURE__ */ React8.createElement(InsertDriveFileIcon, { color: "action" }); } }; const canPreview = file.type.startsWith("image/") || file.type === "application/pdf"; return /* @__PURE__ */ React8.createElement( Box4, { display: "flex", alignItems: "center", gap: 1, sx: { position: "relative" } }, previewUrl && file.type.startsWith("image/") ? /* @__PURE__ */ React8.createElement( Avatar3, { src: previewUrl, alt: file.name, variant: "rounded", sx: { width: 40, height: 40, cursor: onPreview ? "pointer" : void 0 }, onClick: () => onPreview && previewUrl && onPreview(file, previewUrl) } ) : /* @__PURE__ */ React8.createElement( Avatar3, { variant: "rounded", sx: { width: 40, height: 40, bgcolor: "grey.200", cursor: canPreview && onPreview ? "pointer" : void 0 }, onClick: () => canPreview && onPreview && previewUrl && onPreview(file, previewUrl) }, getFileIcon() ), /* @__PURE__ */ React8.createElement(Box4, { flex: 1 }, /* @__PURE__ */ React8.createElement(Typography4, { variant: "body2", noWrap: true, maxWidth: 120, title: file.name }, file.name), /* @__PURE__ */ React8.createElement(Typography4, { variant: "caption", color: "text.secondary" }, (file.size / 1024).toFixed(1), " Ko")), onRemove && /* @__PURE__ */ React8.createElement( IconButton2, { size: "small", onClick: () => onRemove(file), sx: { color: "error.main", "&:hover": { backgroundColor: "error.light" } } }, /* @__PURE__ */ React8.createElement(DeleteIcon, { fontSize: "small" }) ) ); }; // src/components/ticket-form/common/TicketFileUpload.tsx var TicketFileUpload = ({ control, errors, loading = false, onPreview, onRemove }) => { var _a; const { config } = useHelpdesk(); const fileInputRef = useRef(null); const selectedFiles = useWatch({ control, name: "files" }) || []; const { setValue } = useFormContext(); const handleFilesChange = (e) => { const files = e.target.files ? Array.from(e.target.files) : []; const all = [...selectedFiles, ...files]; const unique = all.filter( (file, idx, arr) => arr.findIndex( (f) => f.name === file.name && f.size === file.size ) === idx ); setValue("files", unique); }; const handleRemoveFile = (fileToRemove) => { const filtered = selectedFiles.filter( (file) => file.name !== fileToRemove.name || file.size !== fileToRemove.size ); setValue("files", filtered); }; return /* @__PURE__ */ React9.createElement(Box5, null, /* @__PURE__ */ React9.createElement( "input", { ref: fileInputRef, type: "file", multiple: true, style: { display: "none" }, onChange: handleFilesChange, accept: (_a = config.allowedFileTypes) == null ? void 0 : _a.join(",") } ), /* @__PURE__ */ React9.createElement( Button, { variant: "outlined", onClick: () => { var _a2; return (_a2 = fileInputRef.current) == null ? void 0 : _a2.click(); }, disabled: loading }, "Ajouter des fichiers" ), selectedFiles.length > 0 && /* @__PURE__ */ React9.createElement(Box5, { mt: 1 }, /* @__PURE__ */ React9.createElement(Typography5, { variant: "caption", color: "text.secondary" }, "Fichiers s\xE9lectionn\xE9s :"), /* @__PURE__ */ React9.createElement(Box5, { display: "flex", flexDirection: "column", gap: 1, mt: 1 }, selectedFiles.map((file, idx) => /* @__PURE__ */ React9.createElement( FilePreview, { key: idx, file, onPreview, onRemove: handleRemoveFile } )))), errors.files && /* @__PURE__ */ React9.createElement(Typography5, { variant: "caption", color: "error" }, errors.files.message)); }; // src/components/ticket-form/create/TicketPriorityField.tsx import { Controller as Controller4 } from "react-hook-form"; import { FormControl as FormControl3, InputLabel as InputLabel3, MenuItem as MenuItem3, Select as Select3, TextField as TextField2, Typography as Typography6 } from "@mui/material"; import React10 from "react"; var TicketPriorityField = ({ control, errors, currentUser }) => { var _a; const { config } = useHelpdesk(); const canEditPriority = (currentUser == null ? void 0 : currentUser.role) === "admin" || (currentUser == null ? void 0 : currentUser.role) === "agent"; if (canEditPriority) { return /* @__PURE__ */ React10.createElement( Controller4, { name: "priority", control, render: ({ field }) => /* @__PURE__ */ React10.createElement(FormControl3, { fullWidth: true, error: !!errors.priority }, /* @__PURE__ */ React10.createElement(InputLabel3, null, "Priorit\xE9"), /* @__PURE__ */ React10.createElement(Select3, __spreadProps(__spreadValues({}, field), { label: "Priorit\xE9" }), config.priorities.map((priority) => /* @__PURE__ */ React10.createElement(MenuItem3, { key: priority.value, value: priority.value }, priority.label))), errors.priority && /* @__PURE__ */ React10.createElement(Typography6, { variant: "caption", color: "error" }, errors.priority.message)) } ); } return /* @__PURE__ */ React10.createElement( TextField2, { label: "Priorit\xE9", value: ((_a = config.priorities.find((p) => p.value === config.defaultPriority)) == null ? void 0 : _a.label) || config.defaultPriority, fullWidth: true, disabled: true, helperText: "Seuls les agents et admins peuvent modifier la priorit\xE9" } ); }; // src/components/ticket-form/create/TicketTagsDisplay.tsx import { Box as Box6, Typography as Typography7 } from "@mui/material"; import React12 from "react"; // src/components/common/TagChip.tsx import { Chip } from "@mui/material"; import React11 from "react"; var TagChip = (_a) => { var _b = _a, { tag, showValue = false, size = "small", category, onDelete, deletable = false, globalDelete = false } = _b, chipProps = __objRest(_b, [ "tag", "showValue", "size", "category", "onDelete", "deletable", "globalDelete" ]); var _a2, _b2; const { isAdmin, removeTagFromCategory } = useHelpdesk(); if (globalDelete && !removeTagFromCategory) { deletable = false; globalDelete = false; } const label = showValue ? `${tag.id}: ${tag.label}` : tag.label; const handleDelete = () => { if (onDelete) { onDelete(tag.id); } else if (globalDelete && category && isAdmin) { removeTagFromCategory(category, tag.id); } }; const canDelete = deletable && (onDelete || globalDelete && isAdmin && category); return /* @__PURE__ */ React11.createElement( Chip, __spreadValues({ label, color: "default", size, onDelete: canDelete ? handleDelete : void 0, sx: __spreadValues({ bgcolor: ((_a2 = tag.color) == null ? void 0 : _a2.startsWith("#")) ? tag.color : void 0, color: ((_b2 = tag.color) == null ? void 0 : _b2.startsWith("#")) ? "white" : void 0 }, chipProps.sx) }, chipProps) ); }; // src/components/ticket-form/create/TicketTagsDisplay.tsx var TicketTagsDisplay = ({ category }) => { const { getTagsForCategory } = useHelpdesk(); if (!getTagsForCategory) { return null; } const availableTags = getTagsForCategory(category); if (availableTags.length === 0) { return /* @__PURE__ */ React12.createElement( Box6, { sx: { p: 2, bgcolor: "grey.50", borderRadius: 1, border: 1, borderColor: "grey.200" } }, /* @__PURE__ */ React12.createElement(Typography7, { variant: "body2", color: "text.secondary" }, "\u{1F4CB} ", /* @__PURE__ */ React12.createElement("strong", null, "Tags disponibles :"), " Aucun tag d\xE9fini pour cette cat\xE9gorie.") ); } return /* @__PURE__ */ React12.createElement( Box6, { sx: { p: 2, bgcolor: "grey.50", borderRadius: 1, border: 1, borderColor: "grey.200" } }, /* @__PURE__ */ React12.createElement(Typography7, { variant: "body2", color: "text.secondary", sx: { mb: 1 } }, "\u{1F4CB} ", /* @__PURE__ */ React12.createElement("strong", null, "Tags disponibles pour cette cat\xE9gorie :")), /* @__PURE__ */ React12.createElement(Box6, { sx: { display: "flex", gap: 1, flexWrap: "wrap" } }, availableTags.map((tag) => /* @__PURE__ */ React12.createElement( TagChip, { key: tag.id, tag, size: "small", category, deletable: true, globalDelete: true } ))) ); }; // src/components/ticket-form/create/TicketTagsField.tsx import { Controller as Controller5 } from "react-hook-form"; import React14 from "react"; // src/components/common/TagSelect.tsx import { Autocomplete, Box as Box7, Button as Button2, Chip as Chip2, Dialog as Dialog2, DialogActions, DialogContent, DialogTitle, IconButton as IconButton4, TextField as TextField3, Typography as Typography8 } from "@mui/material"; import { Delete } from "@mui/icons-material"; import React13, { useState } from "react"; var TagSelect = ({ category, value, onChange, label = "Tags", placeholder = "S\xE9lectionner ou cr\xE9er des tags...", error = false, helperText, disabled = false, deletable = true, onTagAdded }) => { const { getTagsForCategory, addTagToCategory, isAdmin, removeTagFromCategory } = useHelpdesk(); if (!getTagsForCategory || !addTagToCategory) { return null; } const [inputValue, setInputValue] = useState(""); const [showColorDialog, setShowColorDialog] = useState(false); const [newTagData, setNewTagData] = useState(null); const availableTags = getTagsForCategory(category).filter( (tag) => !value.some((selectedTag) => selectedTag.id === tag.id) ); const handleCreateTag = (tagValue) => { setNewTagData({ label: tagValue, color: "#1976d2" }); setShowColorDialog(true); return null; }; const handleConfirmTagCreation = async () => { if (!newTagData) return; const newTag = { id: `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, // ID temporaire label: newTagData.label, color: newTagData.color }; try { const finalTag = await addTagToCategory(category, newTag, onTagAdded); const updatedTags = [...value, finalTag]; onChange(updatedTags); setShowColorDialog(false); setNewTagData(null); setInputValue(""); } catch (error2) { console.error("Erreur lors de la cr\xE9ation du tag:", error2); } }; const handleInputChange = (event, newInputValue) => { setInputValue(newInputValue); }; const handleChange = (event, newValue) => { const processedTags = newValue.map((item) => { if (typeof item === "string") { return handleCreateTag(item); } return item; }).filter((tag) => tag !== null); onChange(processedTags); }; const handleDeleteTag = (tagId) => { const updatedTags = value.filter((tag) => tag.id !== tagId); onChange(updatedTags); }; return /* @__PURE__ */ React13.createElement(React13.Fragment, null, /* @__PURE__ */ React13.createElement( Autocomplete, { multiple: true, freeSolo: true, options: availableTags, value, onChange: handleChange, inputValue, onInputChange: handleInputChange, getOptionLabel: (option) => { if (typeof option === "string") { return option; } return option.label; }, renderInput: (params) => /* @__PURE__ */ React13.createElement( TextField3, __spreadProps(__spreadValues({}, params), { label, placeholder, error, helperText, disabled }) ), renderTags: (tagValue, getTagProps) => tagValue.map((option, index) => { const _a = getTagProps({ index }), { onDelete: _ } = _a, tagProps = __objRest(_a, ["onDelete"]); return /* @__PURE__ */ React13.createElement( TagChip, __spreadValues({ tag: typeof option === "string" ? { id: `temp_${option}`, label: option, color: "default" } : option, size: "small", category, deletable, globalDelete: false, onDelete: handleDeleteTag }, tagProps) ); }), renderOption: (props, option) => { var _a, _b; return /* @__PURE__ */ React13.createElement( Box7, __spreadProps(__spreadValues({ component: "li" }, props), { sx: { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" } }), /* @__PURE__ */ React13.createElement(Box7, { sx: { display: "flex", alignItems: "center", flex: 1 } }, /* @__PURE__ */ React13.createElement( Chip2, { label: option.label, color: "default", size: "small", sx: { mr: 1, bgcolor: ((_a = option.color) == null ? void 0 : _a.startsWith("#")) ? option.color : void 0, color: ((_b = option.color) == null ? void 0 : _b.startsWith("#")) ? "white" : void 0 } } ), /* @__PURE__ */ React13.createElement(Typography8, { variant: "body2" }, option.label)), isAdmin && /* @__PURE__ */ React13.createElement( IconButton4, { size: "small", onClick: (e) => { e.stopPropagation(); removeTagFromCategory(category, option.id); }, sx: { ml: 1 } }, /* @__PURE__ */ React13.createElement(Delete, { fontSize: "small" }) ) ); }, disabled } ), /* @__PURE__ */ React13.createElement( Dialog2, { open: showColorDialog, onClose: () => setShowColorDialog(false), maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ React13.createElement(DialogTitle, null, "Cr\xE9er un nouveau tag"), /* @__PURE__ */ React13.createElement(DialogContent, null, /* @__PURE__ */ React13.createElement(Box7, { sx: { mt: 2 } }, /* @__PURE__ */ React13.createElement( TextField3, { fullWidth: true, label: "Nom du tag", value: (newTagData == null ? void 0 : newTagData.label) || "", onChange: (e) => setNewTagData( (prev) => prev ? __spreadProps(__spreadValues({}, prev), { label: e.target.value }) : null ), sx: { mb: 3 } } ), /* @__PURE__ */ React13.createElement(Typography8, { variant: "subtitle2", gutterBottom: true }, "Couleur du tag"), /* @__PURE__ */ React13.createElement(Box7, { sx: { display: "flex", gap: 2, flexWrap: "wrap", mb: 2 } }, [ "#1976d2", "#dc004e", "#2e7d32", "#ed6c02", "#9c27b0", "#0288d1", "#d32f2f", "#388e3c" ].map((color) => /* @__PURE__ */ React13.createElement( Box7, { key: color, sx: { width: 40, height: 40, borderRadius: 1, bgcolor: color, cursor: "pointer", border: (newTagData == null ? void 0 : newTagData.color) === color ? "3px solid #000" : "1px solid #ccc", "&:hover": { border: "2px solid #000" } }, onClick: () => setNewTagData((prev) => prev ? __spreadProps(__spreadValues({}, prev), { color }) : null) } ))), /* @__PURE__ */ React13.createElement(Box7, { sx: { display: "flex", alignItems: "center", gap: 2 } }, /* @__PURE__ */ React13.createElement(Typography8, { variant: "body2" }, "Couleur personnalis\xE9e :"), /* @__PURE__ */ React13.createElement( "input", { type: "color", value: (newTagData == null ? void 0 : newTagData.color) || "#1976d2", onChange: (e) => setNewTagData( (prev) => prev ? __spreadProps(__spreadValues({}, prev), { color: e.target.value }) : null ), style: { width: 50, height: 40, border: "none", borderRadius: 4 } } ), /* @__PURE__ */ React13.createElement(Typography8, { variant: "body2", sx: { fontFamily: "monospace" } }, newTagData == null ? void 0 : newTagData.color)), newTagData && /* @__PURE__ */ React13.createElement(Box7, { sx: { mt: 3, p: 2, bgcolor: "grey.50", borderRadius: 1 } }, /* @__PURE__ */ React13.createElement(Typography8, { variant: "subtitle2", gutterBottom: true }, "Aper\xE7u :"), /* @__PURE__ */ React13.createElement( Chip2, { label: newTagData.label, sx: { bgcolor: newTagData.color, color: "white" } } )))), /* @__PURE__ */ React13.createElement(DialogActions, null, /* @__PURE__ */ React13.createElement(Button2, { onClick: () => setShowColorDialog(false) }, "Annuler"), /* @__PURE__ */ React13.createElement( Button2, { onClick: handleConfirmTagCreation, variant: "contained", disabled: !(newTagData == null ? void 0 : newTagData.label) }, "Cr\xE9er le tag" )) )); }; // src/components/ticket-form/create/TicketTagsField.tsx var TicketTagsField = ({ control, errors, category }) => { return /* @__PURE__ */ React14.createElement( Controller5, { name: "tags", control, render: ({ field }) => { var _a; return /* @__PURE__ */ React14.createElement( TagSelect, { category, value: field.value || [], onChange: field.onChange, label: "Tags", placeholder: "S\xE9lectionner ou cr\xE9er des tags...", error: !!errors.tags, helperText: (_a = errors.tags) == null ? void 0 : _a.message } ); } } ); }; // src/components/ticket-form/create/CreateTicketForm.tsx import { zodResolver } from "@hookform/resolvers/zod"; var CreateTicketForm = ({ open, onClose, onSubmit, loading = false }) => { const { config, currentUser, users } = useHelpdesk(); const [previewImage, setPreviewImage] = useState2(null); const [isSubmitting, setIsSubmitting] = useState2(false); const methods = useForm({ resolver: zodResolver(createTicketSchema), defaultValues: { title: "", description: "", category: "", priority: config.defaultPriority, assignedTo: "", tags: [], files: [] } }); const { control, handleSubmit, reset, watch, formState: { errors } } = methods; const selectedCategory = watch("category"); const handleFormSubmit = async (data) => { try { setIsSubmitting(true); await onSubmit(data); reset(); } catch (error) { console.error("Erreur lors de la cr\xE9ation du ticket:", error); } finally { setIsSubmitting(false); } }; const handleClose = () => { reset(); onClose(); }; const handlePreview = (file, url) => { setPreviewImage({ file, url }); }; const handleRemoveFile = (fileToRemove) => { }; const isButtonLoading = loading || isSubmitting; return /* @__PURE__ */ React15.createElement(Dialog3, { open, onClose: handleClose, maxWidth: "md", fullWidth: true }, /* @__PURE__ */ React15.createElement(DialogTitle2, { sx: { borderBottom: 1, borderColor: "divider" } }, /* @__PURE__ */ React15.createElement(Typography9, { variant: "h6" }, "Cr\xE9er un nouveau ticket")), /* @__PURE__ */ React15.createElement(FormProvider, __spreadValues({}, methods), /* @__PURE__ */ React15.createElement("form", { onSubmit: handleSubmit(handleFormSubmit) }, /* @__PURE__ */ React15.createElement(DialogContent2, null, /* @__PURE__ */ React15.createElement(Box8, { display: "flex", flexDirection: "column", gap: 3 }, /* @__PURE__ */ React15.createElement(TicketCategoryField, { control, errors }), /* @__PURE__ */ React15.createElement( TicketPriorityField, { control, errors, currentUser } ), /* @__PURE__ */ React15.createElement(TicketBasicFields, { control, errors }), /* @__PURE__ */ React15.createElement( TicketAssignmentField, { control, users, currentUser } ), selectedCategory && (currentUser.role === "admin" || currentUser.role === "agent" ? /* @__PURE__ */ React15.createElement( TicketTagsField, { control, errors, category: selectedCategory } ) : /* @__PURE__ */ React15.createElement(TicketTagsDisplay, { category: selectedCategory })), /* @__PURE__ */ React15.createElement( TicketFileUpload, { control, errors, loading: isButtonLoading, onPreview: handlePreview, onRemove: handleRemoveFile } ))), /* @__PURE__ */ React15.createElement(DialogActions2, { sx: { borderTop: 1, borderColor: "divider" } }, /* @__PURE__ */ React15.createElement(Button3, { onClick: handleClose, disabled: isButtonLoading }, "Annuler"), /* @__PURE__ */ React15.createElement( Button3, { type: "submit", variant: "contained", disabled: isButtonLoading }, isButtonLoading ? "Cr\xE9ation..." : "Cr\xE9er le ticket" )))), /* @__PURE__ */ React15.createElement( FilePreviewDialog, { open: !!previewImage, onClose: () => setPreviewImage(null), previewImage } )); }; // src/components/ticket-form/create/CreateTicketButton.tsx var CreateTicketButton = ({ onSubmit, loading = false, variant = "button", buttonText = "Nouveau ticket" }) => { const [open, setOpen] = useState3(false); const handleOpen = () => setOpen(true); const handleClose = () => setOpen(false); const handleSubmit = async (data) => { await onSubmit(data); handleClose(); }; return /* @__PURE__ */ React16.createElement(React16.Fragment, null, variant === "fab" ? /* @__PURE__ */ React16.createElement( Fab, { color: "primary", "aria-label": "Cr\xE9er un ticket", onClick: handleOpen, disabled: loading, sx: { position: "fixed", bottom: 16, right: 16, zIndex: 1e3 } }, /* @__PURE__ */ React16.createElement(Add, null) ) : /* @__PURE__ */ React16.createElement( Button4, { variant: "contained", startIcon: /* @__PURE__ */ React16.createElement(Add, null), onClick: handleOpen, disabled: loading }, buttonText ), /* @__PURE__ */ React16.createElement( CreateTicketForm, { open, onClose: handleClose, onSubmit: handleSubmit, loading } )); }; // src/components/dashboard/Dashboard.tsx import { Box as Box9, Card, CardContent, Grid, Typography as Typography10 } from "@mui/material"; import { BugReport, CheckCircle, Schedule, Warning } from "@mui/icons-material"; import React17 from "react"; var Dashboard = ({ stats }) => { const statCards = [ { title: "Total Tickets", value: stats.totalTicke