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}
+
+ COMPRIS
+
+
+
+ );
+};
+
+// ============================================================================
+// 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 */}
+
+
+
+
setIsCreatingTicket(true)}
+ className="bg-cyan-500 hover:bg-cyan-400 text-gray-900 px-5 py-2.5 rounded-lg font-bold text-sm transition flex items-center font-mono tracking-widest shadow-[0_0_15px_rgba(0,229,255,0.2)]"
+ >
+ NOUVEAU TICKET
+
+
+
+ {/* CHARGEMENT & ERREURS GLOBALES */}
+ {isLoading && (
+
+
+ Déchiffrement de la matrice de support...
+
+ )}
+
+ {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
+
+
setIsCreatingTicket(false)} className="text-gray-400 hover:text-white transition text-xl">✖
+
+
+
+
+
+ )}
+
+ {/* MODAL 2 : FIL DE DISCUSSION INTERACTIF (THREAD NATIVE) */}
+ {activeTicket && (
+
+
+
+ {/* Thread Header */}
+
+
+
+ {activeTicket.id}
+
+ {activeTicket.department}
+
+
+
{activeTicket.subject}
+
+
setActiveTicket(null)} className="text-gray-400 hover:text-white transition text-xl bg-black/50 p-2 rounded-lg">✖
+
+
+ {/* 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' ? (
+
+
+ setReplyMessage(e.target.value)}
+ required
+ placeholder="Taper une réponse sécurisée..."
+ className="flex-1 bg-black border border-gray-800 text-white p-3 rounded-lg text-sm focus:border-cyan-500 outline-none transition resize-none"
+ >
+
+
+ ENVOYER
+
+
+
+ ) : (
+
+ [ CE TICKET EST VERROUILLÉ ET ARCHIVÉ ]
+
+ )}
+
+
+ )}
+
+ {/* VRAI MODAL DE NOTIFICATION */}
+
setCustomAlert(null)} />
+
+ );
+}
\ No newline at end of file