@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
JavaScript
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