From 74103f4e5c661d5dbb6704327ed838e2d17ddeee Mon Sep 17 00:00:00 2001 From: maximus Date: Wed, 24 Jun 2026 21:39:05 +0200 Subject: [PATCH] add ticketing system --- src/pages/app/Support.jsx | 374 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 src/pages/app/Support.jsx diff --git a/src/pages/app/Support.jsx b/src/pages/app/Support.jsx new file mode 100644 index 0000000..0942a91 --- /dev/null +++ b/src/pages/app/Support.jsx @@ -0,0 +1,374 @@ +import { useState, useEffect, useCallback } from 'react'; +import { getClientTickets, createTicket, getHelpdesks, getTicketDetails, replyTicket } from '../../services/api'; // Ajout des routes de détails et réponses +import { LifeBuoy, Plus, MessageSquare, Clock, CheckCircle2, Send, AlertCircle, Loader, Lock } from 'lucide-react'; + +// ============================================================================ +// COMPOSANT 0 : MODAL DE NOTIFICATION (Design System) +// ============================================================================ +const NotificationModal = ({ notification, onClose }) => { + if (!notification) return null; + const isError = notification.type === 'error'; + return ( +
+
+

+ {notification.title.toUpperCase()} +

+

{notification.message}

+ +
+
+ ); +}; + +// ============================================================================ +// COMPOSANT PRINCIPAL : SUPPORT TICKETS +// ============================================================================ +export default function Support() { + const [tickets, setTickets] = useState([]); + const [helpdesks, setHelpdesks] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isLoadingConversation, setIsLoadingConversation] = useState(false); // Loader spécifique pour le fil de discussion + const [error, setError] = useState(null); + + // États pour les formulaires + const [subject, setSubject] = useState(''); + const [message, setMessage] = useState(''); + const [replyMessage, setReplyMessage] = useState(''); // Stocke le texte de la réponse + const [selectedHelpdesk, setSelectedHelpdesk] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + // États pour les modaux + const [customAlert, setCustomAlert] = useState(null); + const [isCreatingTicket, setIsCreatingTicket] = useState(false); + const [activeTicket, setActiveTicket] = useState(null); + + const triggerAlert = (title, message, type = "info") => { + setCustomAlert({ title, message, type }); + }; + + // Chargement synchrone des tickets et des helpdesks + const loadSupportData = useCallback(async () => { + try { + setIsLoading(true); + const ticketsData = await getClientTickets(); + if (ticketsData && ticketsData.list) { + setTickets(ticketsData.list.map(tkt => ({ + id: `TKT-${tkt.id}`, + db_id: tkt.id, + subject: tkt.subject, + department: tkt.helpdesk?.name || 'Support Technique', + status: (tkt.status === 'open' || tkt.status === 'closed') ? tkt.status : 'pending', + lastUpdate: tkt.updated_at || tkt.created_at || 'Récemment', + messages: [] + }))); + } + + const hdeskPairs = await getHelpdesks(); + if (hdeskPairs) { + const formattedDesks = Object.entries(hdeskPairs).map(([id, name]) => ({ id, name })); + setHelpdesks(formattedDesks); + const nexusDesk = formattedDesks.find(hd => hd.name.toLowerCase().includes('nexus')); + if (nexusDesk) { + setSelectedHelpdesk(nexusDesk.id); + } else if (formattedDesks.length > 0) { + setSelectedHelpdesk(formattedDesks[0].id); + } + } + } catch (err) { + setError(err.message || "Impossible de synchroniser le centre de support."); + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + loadSupportData(); + }, [loadSupportData]); + + // ACTION CLIC : Charger les messages du ticket depuis FOSSBilling + const handleOpenTicket = async (ticket) => { + try { + setIsLoadingConversation(true); + // On ouvre immédiatement le modal avec une liste de messages vide pour la fluidité + setActiveTicket({ ...ticket, messages: [] }); + + const details = await getTicketDetails(ticket.db_id); + + if (details && details.messages) { + // FOSSBilling renvoie l'auteur dans msg.author.role ('client', 'staff', 'admin') + const formattedMessages = details.messages.map(msg => ({ + sender: msg.author?.role === 'client' ? 'client' : 'staff', + text: msg.content, + date: msg.created_at || 'Récemment' + })); + + // Injection dynamique des messages dans le modal actif + setActiveTicket(prev => prev ? { ...prev, messages: formattedMessages } : null); + } + } catch (err) { + triggerAlert("Erreur Réseau", "Impossible de récupérer l'historique : " + err.message, "error"); + } finally { + setIsLoadingConversation(false); + } + }; + + // ACTION RÉPONSE : Envoyer un message dans le thread actuel + const handleReplySubmit = async (e) => { + e.preventDefault(); + if (!replyMessage.trim() || !activeTicket) return; + + try { + await replyTicket(activeTicket.db_id, replyMessage); + setReplyMessage(''); // Nettoyer l'input + + // Rechargement instantané du fil de discussion pour afficher le message soumis + const details = await getTicketDetails(activeTicket.db_id); + if (details && details.messages) { + const formattedMessages = details.messages.map(msg => ({ + sender: msg.author?.role === 'client' ? 'client' : 'staff', + text: msg.content, + date: msg.created_at || 'Récemment' + })); + setActiveTicket(prev => prev ? { ...prev, messages: formattedMessages } : null); + } + } catch (err) { + triggerAlert("Échec d'envoi", "Votre réponse n'a pas pu être transmise : " + err.message, "error"); + } + }; + + // Soumission du nouveau ticket + const handleCreateTicket = async (e) => { + e.preventDefault(); + if (!subject.trim() || !message.trim() || !selectedHelpdesk) return; + + setIsSubmitting(true); + try { + await createTicket(subject, message, selectedHelpdesk); + setIsCreatingTicket(false); + setSubject(''); + setMessage(''); + loadSupportData(); + triggerAlert("Ticket Ouvert", "Votre demande a bien été enregistrée sur le Service Desk.", "success"); + } catch (err) { + triggerAlert("Échec", "Erreur lors de la création du ticket : " + err.message, "error"); + } finally { + setIsSubmitting(false); + } + }; + + const getStatusConfig = (status) => { + switch (status) { + case 'open': return { color: 'text-cyan-400', bg: 'bg-cyan-500/10', border: 'border-cyan-500/20', label: 'SUPPORT', icon: }; + case 'pending': return { color: 'text-orange-400', bg: 'bg-orange-500/10', border: 'border-orange-500/20', label: 'EN ATTENTE', icon: }; + case 'closed': return { color: 'text-gray-400', bg: 'bg-gray-500/10', border: 'border-gray-500/20', label: 'RÉSOLU', icon: }; + default: return { color: 'text-gray-400', bg: 'bg-gray-800', border: 'border-gray-700', label: status.toUpperCase() }; + } + }; + + return ( +
+ + {/* HEADER */} +
+
+
+

+ + CENTRE DE SUPPORT +

+

Canal de communication sécurisé avec les ingénieurs GISE.

+
+
+ + +
+ + {/* CHARGEMENT & ERREURS GLOBALES */} + {isLoading && ( +
+ + Déchiffrement de la matrice de support... +
+ )} + + {error && ( +
+ + {error} +
+ )} + + {/* GRILLE DES TICKETS */} + {!isLoading && !error && ( +
+ {tickets.length === 0 ? ( +
+ Aucun ticket de support actif. L'infrastructure est nominale. +
+ ) : ( + tickets.map(ticket => { + const conf = getStatusConfig(ticket.status); + return ( +
handleOpenTicket(ticket)} // Modification ici : Appel de la fonction de chargement au lieu du setter direct + className="bg-gray-900 border border-gray-800 rounded-xl p-5 hover:border-cyan-400/50 hover:bg-gray-800/50 transition-all cursor-pointer group flex flex-col justify-between min-h-[160px]" + > +
+
+ {ticket.id} + + {conf.icon} {conf.label} + +
+

+ {ticket.subject} +

+
+
+ Département: {ticket.department} + MàJ: {ticket.lastUpdate} +
+
+ ) + }) + )} +
+ )} + + {/* MODAL 1 : CRÉATION DE TICKET */} + {isCreatingTicket && ( +
+
+
+

+ OUVRIR UNE REQUÊTE +

+ +
+ +
+
+ + setSubject(e.target.value)} required placeholder="Ex: Problème d'accès sur l'API" className="w-full bg-black border border-gray-800 text-white p-3 rounded text-sm focus:border-cyan-500 outline-none transition" /> +
+
+ + {/* CHAMP VERROUILLÉ (Read-Only) */} +
+ + {helpdesks.find(h => h.id === selectedHelpdesk)?.name || 'Service Desk Nexus'} + + +
+

+ Routage automatique vers les ingénieurs d'infrastructure. +

+
+
+ + +
+ +
+
+
+ )} + + {/* MODAL 2 : FIL DE DISCUSSION INTERACTIF (THREAD NATIVE) */} + {activeTicket && ( +
+
+ + {/* Thread Header */} +
+
+
+ {activeTicket.id} + + {activeTicket.department} + +
+

{activeTicket.subject}

+
+ +
+ + {/* Thread Body (Messages avec état de chargement) */} +
+ {isLoadingConversation ? ( +
+ + Téléchargement des paquets de discussion sécurisés... +
+ ) : activeTicket.messages.length === 0 ? ( +
+ Aucun message trouvé dans ce fil. +
+ ) : ( + activeTicket.messages.map((msg, idx) => ( +
+
+ + {msg.sender === 'client' ? 'VOUS' : 'INGÉNIEUR GISE'} + + {msg.date} +
+
+ {msg.text} +
+
+ )) + )} +
+ + {/* Thread Footer (Formulaire de réponse branché) */} + {activeTicket.status !== 'closed' ? ( +
+
+ + +
+
+ ) : ( +
+ [ CE TICKET EST VERROUILLÉ ET ARCHIVÉ ] +
+ )} +
+
+ )} + + {/* VRAI MODAL DE NOTIFICATION */} + setCustomAlert(null)} /> +
+ ); +} \ No newline at end of file