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,275 lines (1,244 loc) 262 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropNames = Object.getOwnPropertyNames; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __getProtoOf = Object.getPrototypeOf; 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; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { CreateTicketButton: () => CreateTicketButton, CreateTicketForm: () => CreateTicketForm, Dashboard: () => Dashboard, HelpdeskApp: () => HelpdeskApp, HelpdeskProvider: () => HelpdeskProvider, PriorityChip: () => PriorityChip, PrioritySelect: () => PrioritySelect, StatusChip: () => StatusChip, StatusSelect: () => StatusSelect, TagChip: () => TagChip, TagSelect: () => TagSelect, TicketCard: () => TicketCard, TicketChat: () => TicketChat, TicketDetailDialog: () => TicketDetailDialog, TicketGanttChart: () => TicketGanttChart, TicketKanban: () => TicketKanban, TicketList: () => TicketList, TimeTrackingFields: () => TimeTrackingFields, UserAvatar: () => UserAvatar, UserSelect: () => UserSelect, canDeleteTicket: () => canDeleteTicket, canEditTicket: () => canEditTicket, canViewAllTickets: () => canViewAllTickets, canViewOwnTickets: () => canViewOwnTickets, canViewTicket: () => canViewTicket, capitalizeFirstChar: () => capitalizeFirstChar, capitalizeWords: () => capitalizeWords, createTicketSchema: () => createTicketSchema, filterTicketsByPermission: () => filterTicketsByPermission, getCategoryConfig: () => getCategoryConfig, getCategoryLabel: () => getCategoryLabel, getDefaultStatusForCategory: () => getDefaultStatusForCategory, getPriorityColor: () => getPriorityColor, getPriorityLabel: () => getPriorityLabel, getStatusColor: () => getStatusColor, getStatusLabel: () => getStatusLabel, getStatusesForCategory: () => getStatusesForCategory, isValidCategory: () => isValidCategory, isValidPriority: () => isValidPriority, isValidStatus: () => isValidStatus, toTitleCase: () => toTitleCase, updateTicketSchema: () => updateTicketSchema, useHelpdesk: () => useHelpdesk }); module.exports = __toCommonJS(index_exports); // src/components/app/HelpdeskApp.tsx var import_material15 = require("@mui/material"); // src/context/HelpdeskContext.tsx var import_react = __toESM(require("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 = (0, import_react.createContext)( void 0 ); var HelpdeskProvider = ({ children, config: userConfig = {}, userRole = "user", currentUser: initialUser, users: initialUsers, onTagAdded, onTagRemoved }) => { const [config, setConfig] = import_react.default.useState(__spreadValues(__spreadValues({}, defaultConfig), userConfig)); const [role, setRole] = import_react.default.useState(userRole); const [currentUser, setCurrentUser] = import_react.default.useState(initialUser); const [users, setUsers] = import_react.default.useState(initialUsers); const updateConfig = import_react.default.useCallback( (newConfig) => { setConfig((prev) => __spreadValues(__spreadValues({}, prev), newConfig)); }, [] ); const updateUserRole = import_react.default.useCallback((newRole) => { setRole(newRole); }, []); const getStatusesForCategory2 = import_react.default.useCallback( (category) => { const categoryConfig = config.categories.find( (c) => c.value === category ); if (categoryConfig) { return categoryConfig.statuses; } return config.statuses || []; }, [config] ); const getDefaultStatusForCategory2 = import_react.default.useCallback( (category) => { const categoryConfig = config.categories.find( (c) => c.value === category ); return categoryConfig == null ? void 0 : categoryConfig.defaultStatus; }, [config] ); const getTagsForCategory = import_react.default.useCallback( (category) => { const categoryConfig = config.categories.find( (c) => c.value === category ); return (categoryConfig == null ? void 0 : categoryConfig.tags) || []; }, [config] ); const addTagToCategory = import_react.default.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 = import_react.default.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__ */ import_react.default.createElement(HelpdeskContext.Provider, { value }, children); }; var useHelpdesk = () => { const context = (0, import_react.useContext)(HelpdeskContext); if (context === void 0) { throw new Error("useHelpdesk must be used within a HelpdeskProvider"); } return context; }; // src/components/app/HelpdeskApp.tsx var import_react18 = __toESM(require("react")); // src/components/ticket-form/create/CreateTicketButton.tsx var import_material13 = require("@mui/material"); var import_react16 = __toESM(require("react")); var import_icons_material3 = require("@mui/icons-material"); // src/components/ticket-form/create/CreateTicketForm.tsx var import_material12 = require("@mui/material"); // src/schemas/ticket.ts var import_zod = require("zod"); var createTicketSchema = import_zod.z.object({ title: import_zod.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: import_zod.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: import_zod.z.string().min(1, "Veuillez s\xE9lectionner une cat\xE9gorie"), priority: import_zod.z.enum(["low", "medium", "high"], { required_error: "Veuillez s\xE9lectionner une priorit\xE9" }), assignedTo: import_zod.z.string().optional(), tags: import_zod.z.array(import_zod.z.object({ id: import_zod.z.string(), label: import_zod.z.string(), color: import_zod.z.string().optional() // Support des couleurs hexadécimales })).optional(), files: import_zod.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: import_zod.z.string().optional(), hoursSpent: import_zod.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: import_zod.z.date().optional(), endDate: import_zod.z.date().optional() }); // src/components/ticket-form/create/CreateTicketForm.tsx var import_react_hook_form7 = require("react-hook-form"); var import_react15 = __toESM(require("react")); // src/components/ticket-form/common/FilePreviewDialog.tsx var import_material = require("@mui/material"); var import_react2 = __toESM(require("react")); var FilePreviewDialog = ({ open, onClose, previewImage }) => { if (!previewImage) return null; return /* @__PURE__ */ import_react2.default.createElement(import_material.Dialog, { open, onClose, maxWidth: "md" }, /* @__PURE__ */ import_react2.default.createElement(import_material.Box, { p: 2, display: "flex", flexDirection: "column", alignItems: "center" }, previewImage.file.type.startsWith("image/") ? /* @__PURE__ */ import_react2.default.createElement( "img", { src: previewImage.url, alt: previewImage.file.name, style: { maxWidth: 600, maxHeight: 400, borderRadius: 8 } } ) : previewImage.file.type === "application/pdf" ? /* @__PURE__ */ import_react2.default.createElement( "iframe", { src: previewImage.url, width: "600", height: "400", style: { border: "none", borderRadius: 8 }, title: previewImage.file.name } ) : null, /* @__PURE__ */ import_react2.default.createElement(import_material.Typography, { variant: "body2", mt: 2 }, previewImage.file.name), /* @__PURE__ */ import_react2.default.createElement(import_material.Typography, { variant: "caption", color: "text.secondary" }, (previewImage.file.size / 1024).toFixed(1), " Ko"))); }; // src/components/ticket-form/create/TicketAssignmentField.tsx var import_react5 = __toESM(require("react")); // src/components/common/UserSelect.tsx var import_material3 = require("@mui/material"); var import_react_hook_form = require("react-hook-form"); var import_icons_material = require("@mui/icons-material"); var import_react4 = __toESM(require("react")); // src/components/common/UserAvatar.tsx var import_material2 = require("@mui/material"); var import_react3 = __toESM(require("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__ */ import_react3.default.createElement( import_material2.Avatar, __spreadValues({ src: user.avatar, alt: user.name, sx: __spreadValues(__spreadValues({}, avatarStyles), sx) }, avatarProps) ); } return /* @__PURE__ */ import_react3.default.createElement( import_material2.Box, { 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__ */ import_react4.default.createElement( import_react_hook_form.Controller, { name, control, render: ({ field }) => /* @__PURE__ */ import_react4.default.createElement(import_material3.FormControl, { fullWidth: true, error }, /* @__PURE__ */ import_react4.default.createElement(import_material3.InputLabel, null, label), /* @__PURE__ */ import_react4.default.createElement( import_material3.Select, __spreadProps(__spreadValues({}, field), { label, renderValue: (selected) => { if (!selected) { return ""; } const selectedUser = assignableUsers.find( (user) => user.id === selected ); if (!selectedUser) return null; return /* @__PURE__ */ import_react4.default.createElement( import_material3.Box, { display: "flex", alignItems: "center", gap: 1, sx: { width: "100%" } }, /* @__PURE__ */ import_react4.default.createElement(UserAvatar, { user: selectedUser, size: 24 }), /* @__PURE__ */ import_react4.default.createElement(import_material3.Box, { sx: { flexGrow: 1 } }, /* @__PURE__ */ import_react4.default.createElement(import_material3.Typography, { variant: "body2" }, selectedUser.name), /* @__PURE__ */ import_react4.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary" }, selectedUser.role)), /* @__PURE__ */ import_react4.default.createElement( import_material3.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__ */ import_react4.default.createElement(import_icons_material.Close, { fontSize: "small" }) ) ); } }), /* @__PURE__ */ import_react4.default.createElement(import_material3.MenuItem, { value: "" }, /* @__PURE__ */ import_react4.default.createElement(import_material3.Typography, { color: "text.secondary" }, placeholder)), assignableUsers.map((user) => /* @__PURE__ */ import_react4.default.createElement(import_material3.MenuItem, { key: user.id, value: user.id }, /* @__PURE__ */ import_react4.default.createElement(import_material3.Box, { display: "flex", alignItems: "center", gap: 1 }, /* @__PURE__ */ import_react4.default.createElement(UserAvatar, { user, size: 24 }), /* @__PURE__ */ import_react4.default.createElement(import_material3.Box, null, /* @__PURE__ */ import_react4.default.createElement(import_material3.Typography, { variant: "body2" }, user.name), /* @__PURE__ */ import_react4.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary" }, user.role, " \u2022 ", user.email))))) ), helperText && /* @__PURE__ */ import_react4.default.createElement(import_material3.Typography, { 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__ */ import_react5.default.createElement( UserSelect, { name: "assignedTo", control, users, label: "Assigner \xE0", placeholder: "S\xE9lectionner un agent ou admin..." } ); }; // src/components/ticket-form/create/TicketBasicFields.tsx var import_react_hook_form2 = require("react-hook-form"); var import_react6 = __toESM(require("react")); var import_material4 = require("@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__ */ import_react6.default.createElement(import_react6.default.Fragment, null, /* @__PURE__ */ import_react6.default.createElement( import_react_hook_form2.Controller, { name: "title", control, render: ({ field }) => { var _a; return /* @__PURE__ */ import_react6.default.createElement( import_material4.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__ */ import_react6.default.createElement( import_react_hook_form2.Controller, { name: "description", control, render: ({ field }) => { var _a; return /* @__PURE__ */ import_react6.default.createElement( import_material4.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 var import_react_hook_form3 = require("react-hook-form"); var import_material5 = require("@mui/material"); var import_react7 = __toESM(require("react")); var TicketCategoryField = ({ control, errors }) => { const { config } = useHelpdesk(); return /* @__PURE__ */ import_react7.default.createElement( import_react_hook_form3.Controller, { name: "category", control, render: ({ field }) => /* @__PURE__ */ import_react7.default.createElement(import_material5.FormControl, { fullWidth: true, error: !!errors.category }, /* @__PURE__ */ import_react7.default.createElement(import_material5.InputLabel, null, "Cat\xE9gorie"), /* @__PURE__ */ import_react7.default.createElement(import_material5.Select, __spreadProps(__spreadValues({}, field), { label: "Cat\xE9gorie" }), config.categories.map((category) => /* @__PURE__ */ import_react7.default.createElement(import_material5.MenuItem, { key: category.value, value: category.value }, category.label))), errors.category && /* @__PURE__ */ import_react7.default.createElement(import_material5.Typography, { variant: "caption", color: "error" }, errors.category.message)) } ); }; // src/components/ticket-form/common/TicketFileUpload.tsx var import_material7 = require("@mui/material"); var import_react_hook_form4 = require("react-hook-form"); var import_react9 = __toESM(require("react")); // src/components/ticket-form/common/FilePreview.tsx var import_material6 = require("@mui/material"); var import_Delete = __toESM(require("@mui/icons-material/Delete")); var import_Description = __toESM(require("@mui/icons-material/Description")); var import_InsertDriveFile = __toESM(require("@mui/icons-material/InsertDriveFile")); var import_PictureAsPdf = __toESM(require("@mui/icons-material/PictureAsPdf")); var import_react8 = __toESM(require("react")); var import_TableChart = __toESM(require("@mui/icons-material/TableChart")); var FilePreview = ({ file, onPreview, onRemove }) => { const [previewUrl, setPreviewUrl] = import_react8.default.useState(null); import_react8.default.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__ */ import_react8.default.createElement(import_PictureAsPdf.default, { color: "error" }); } else if (file.type.includes("spreadsheet") || file.name.endsWith(".xlsx") || file.name.endsWith(".xls")) { return /* @__PURE__ */ import_react8.default.createElement(import_TableChart.default, { color: "success" }); } else if (file.type.includes("document") || file.name.endsWith(".docx") || file.name.endsWith(".doc")) { return /* @__PURE__ */ import_react8.default.createElement(import_Description.default, { color: "primary" }); } else { return /* @__PURE__ */ import_react8.default.createElement(import_InsertDriveFile.default, { color: "action" }); } }; const canPreview = file.type.startsWith("image/") || file.type === "application/pdf"; return /* @__PURE__ */ import_react8.default.createElement( import_material6.Box, { display: "flex", alignItems: "center", gap: 1, sx: { position: "relative" } }, previewUrl && file.type.startsWith("image/") ? /* @__PURE__ */ import_react8.default.createElement( import_material6.Avatar, { src: previewUrl, alt: file.name, variant: "rounded", sx: { width: 40, height: 40, cursor: onPreview ? "pointer" : void 0 }, onClick: () => onPreview && previewUrl && onPreview(file, previewUrl) } ) : /* @__PURE__ */ import_react8.default.createElement( import_material6.Avatar, { 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__ */ import_react8.default.createElement(import_material6.Box, { flex: 1 }, /* @__PURE__ */ import_react8.default.createElement(import_material6.Typography, { variant: "body2", noWrap: true, maxWidth: 120, title: file.name }, file.name), /* @__PURE__ */ import_react8.default.createElement(import_material6.Typography, { variant: "caption", color: "text.secondary" }, (file.size / 1024).toFixed(1), " Ko")), onRemove && /* @__PURE__ */ import_react8.default.createElement( import_material6.IconButton, { size: "small", onClick: () => onRemove(file), sx: { color: "error.main", "&:hover": { backgroundColor: "error.light" } } }, /* @__PURE__ */ import_react8.default.createElement(import_Delete.default, { fontSize: "small" }) ) ); }; // src/components/ticket-form/common/TicketFileUpload.tsx var TicketFileUpload = ({ control, errors, loading = false, onPreview, onRemove }) => { var _a; const { config } = useHelpdesk(); const fileInputRef = (0, import_react9.useRef)(null); const selectedFiles = (0, import_react_hook_form4.useWatch)({ control, name: "files" }) || []; const { setValue } = (0, import_react_hook_form4.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__ */ import_react9.default.createElement(import_material7.Box, null, /* @__PURE__ */ import_react9.default.createElement( "input", { ref: fileInputRef, type: "file", multiple: true, style: { display: "none" }, onChange: handleFilesChange, accept: (_a = config.allowedFileTypes) == null ? void 0 : _a.join(",") } ), /* @__PURE__ */ import_react9.default.createElement( import_material7.Button, { variant: "outlined", onClick: () => { var _a2; return (_a2 = fileInputRef.current) == null ? void 0 : _a2.click(); }, disabled: loading }, "Ajouter des fichiers" ), selectedFiles.length > 0 && /* @__PURE__ */ import_react9.default.createElement(import_material7.Box, { mt: 1 }, /* @__PURE__ */ import_react9.default.createElement(import_material7.Typography, { variant: "caption", color: "text.secondary" }, "Fichiers s\xE9lectionn\xE9s :"), /* @__PURE__ */ import_react9.default.createElement(import_material7.Box, { display: "flex", flexDirection: "column", gap: 1, mt: 1 }, selectedFiles.map((file, idx) => /* @__PURE__ */ import_react9.default.createElement( FilePreview, { key: idx, file, onPreview, onRemove: handleRemoveFile } )))), errors.files && /* @__PURE__ */ import_react9.default.createElement(import_material7.Typography, { variant: "caption", color: "error" }, errors.files.message)); }; // src/components/ticket-form/create/TicketPriorityField.tsx var import_react_hook_form5 = require("react-hook-form"); var import_material8 = require("@mui/material"); var import_react10 = __toESM(require("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__ */ import_react10.default.createElement( import_react_hook_form5.Controller, { name: "priority", control, render: ({ field }) => /* @__PURE__ */ import_react10.default.createElement(import_material8.FormControl, { fullWidth: true, error: !!errors.priority }, /* @__PURE__ */ import_react10.default.createElement(import_material8.InputLabel, null, "Priorit\xE9"), /* @__PURE__ */ import_react10.default.createElement(import_material8.Select, __spreadProps(__spreadValues({}, field), { label: "Priorit\xE9" }), config.priorities.map((priority) => /* @__PURE__ */ import_react10.default.createElement(import_material8.MenuItem, { key: priority.value, value: priority.value }, priority.label))), errors.priority && /* @__PURE__ */ import_react10.default.createElement(import_material8.Typography, { variant: "caption", color: "error" }, errors.priority.message)) } ); } return /* @__PURE__ */ import_react10.default.createElement( import_material8.TextField, { 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 var import_material10 = require("@mui/material"); var import_react12 = __toESM(require("react")); // src/components/common/TagChip.tsx var import_material9 = require("@mui/material"); var import_react11 = __toESM(require("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__ */ import_react11.default.createElement( import_material9.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__ */ import_react12.default.createElement( import_material10.Box, { sx: { p: 2, bgcolor: "grey.50", borderRadius: 1, border: 1, borderColor: "grey.200" } }, /* @__PURE__ */ import_react12.default.createElement(import_material10.Typography, { variant: "body2", color: "text.secondary" }, "\u{1F4CB} ", /* @__PURE__ */ import_react12.default.createElement("strong", null, "Tags disponibles :"), " Aucun tag d\xE9fini pour cette cat\xE9gorie.") ); } return /* @__PURE__ */ import_react12.default.createElement( import_material10.Box, { sx: { p: 2, bgcolor: "grey.50", borderRadius: 1, border: 1, borderColor: "grey.200" } }, /* @__PURE__ */ import_react12.default.createElement(import_material10.Typography, { variant: "body2", color: "text.secondary", sx: { mb: 1 } }, "\u{1F4CB} ", /* @__PURE__ */ import_react12.default.createElement("strong", null, "Tags disponibles pour cette cat\xE9gorie :")), /* @__PURE__ */ import_react12.default.createElement(import_material10.Box, { sx: { display: "flex", gap: 1, flexWrap: "wrap" } }, availableTags.map((tag) => /* @__PURE__ */ import_react12.default.createElement( TagChip, { key: tag.id, tag, size: "small", category, deletable: true, globalDelete: true } ))) ); }; // src/components/ticket-form/create/TicketTagsField.tsx var import_react_hook_form6 = require("react-hook-form"); var import_react14 = __toESM(require("react")); // src/components/common/TagSelect.tsx var import_material11 = require("@mui/material"); var import_icons_material2 = require("@mui/icons-material"); var import_react13 = __toESM(require("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] = (0, import_react13.useState)(""); const [showColorDialog, setShowColorDialog] = (0, import_react13.useState)(false); const [newTagData, setNewTagData] = (0, import_react13.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__ */ import_react13.default.createElement(import_react13.default.Fragment, null, /* @__PURE__ */ import_react13.default.createElement( import_material11.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__ */ import_react13.default.createElement( import_material11.TextField, __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__ */ import_react13.default.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__ */ import_react13.default.createElement( import_material11.Box, __spreadProps(__spreadValues({ component: "li" }, props), { sx: { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" } }), /* @__PURE__ */ import_react13.default.createElement(import_material11.Box, { sx: { display: "flex", alignItems: "center", flex: 1 } }, /* @__PURE__ */ import_react13.default.createElement( import_material11.Chip, { 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__ */ import_react13.default.createElement(import_material11.Typography, { variant: "body2" }, option.label)), isAdmin && /* @__PURE__ */ import_react13.default.createElement( import_material11.IconButton, { size: "small", onClick: (e) => { e.stopPropagation(); removeTagFromCategory(category, option.id); }, sx: { ml: 1 } }, /* @__PURE__ */ import_react13.default.createElement(import_icons_material2.Delete, { fontSize: "small" }) ) ); }, disabled } ), /* @__PURE__ */ import_react13.default.createElement( import_material11.Dialog, { open: showColorDialog, onClose: () => setShowColorDialog(false), maxWidth: "sm", fullWidth: true }, /* @__PURE__ */ import_react13.default.createElement(import_material11.DialogTitle, null, "Cr\xE9er un nouveau tag"), /* @__PURE__ */ import_react13.default.createElement(import_material11.DialogContent, null, /* @__PURE__ */ import_react13.default.createElement(import_material11.Box, { sx: { mt: 2 } }, /* @__PURE__ */ import_react13.default.createElement( import_material11.TextField, { 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__ */ import_react13.default.createElement(import_material11.Typography, { variant: "subtitle2", gutterBottom: true }, "Couleur du tag"), /* @__PURE__ */ import_react13.default.createElement(import_material11.Box, { sx: { display: "flex", gap: 2, flexWrap: "wrap", mb: 2 } }, [ "#1976d2", "#dc004e", "#2e7d32", "#ed6c02", "#9c27b0", "#0288d1", "#d32f2f", "#388e3c" ].map((color) => /* @__PURE__ */ import_react13.default.createElement( import_material11.Box, { 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__ */ import_react13.default.createElement(import_material11.Box, { sx: { display: "flex", alignItems: "center", gap: 2 } }, /* @__PURE__ */ import_react13.default.createElement(import_material11.Typography, { variant: "body2" }, "Couleur personnalis\xE9e :"), /* @__PURE__ */ import_react13.default.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__ */ import_react13.default.createElement(import_material11.Typography, { variant: "body2", sx: { fontFamily: "monospace" } }, newTagData == null ? void 0 : newTagData.color)), newTagData && /* @__PURE__ */ import_react13.default.createElement(import_material11.Box, { sx: { mt: 3, p: 2, bgcolor: "grey.50", borderRadius: 1 } }, /* @__PURE__ */ import_react13.default.createElement(import_material11.Typography, { variant: "subtitle2", gutterBottom: true }, "Aper\xE7u :"), /* @__PURE__ */ import_react13.default.createElement( import_material11.Chip, { label: newTagData.label, sx: { bgcolor: newTagData.color, color: "white" } } )))), /* @__PURE__ */ import_react13.default.createElement(import_material11.DialogActions, null, /* @__PURE__ */ import_react13.default.createElement(import_material11.Button, { onClick: () => setShowColorDialog(false) }, "Annuler"), /* @__PURE__ */ import_react13.default.createElement( import_material11.But