diff --git a/app/(dashboard)/admin/tickets/page.tsx b/app/(dashboard)/admin/tickets/page.tsx deleted file mode 100644 index 7453b264..00000000 --- a/app/(dashboard)/admin/tickets/page.tsx +++ /dev/null @@ -1,477 +0,0 @@ -"use client"; - -import { AuthGuard } from "@/components/auth/AuthGuard"; -import { AdminStatistics } from "@/components/ticket/admin-statistics"; -import { AdminTicketCard } from "@/components/ticket/admin-ticket-card"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { useIsAdmin, useRolePermissions, useSession, useTicket } from "@/hooks"; -import { - AdminGetTicketsParams, - DEFAULT_PAGE_LIMIT, - Ticket, - TicketPriority, - TicketStatistics, - TicketStatus, -} from "@/lib/types/ticket"; -import { useTicketService } from "@/services"; -import { - AlertTriangle, - Search, - Settings, - Shield, - Sparkles, - XCircle, -} from "lucide-react"; -import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; - -// Configuration constants -const LOADING_SKELETON_COUNT = 3; - -export default function AdminTicketsPage() { - const router = useRouter(); - const session = useSession(); - const isAdmin = useIsAdmin(); - const permissions = useRolePermissions(); - const ticketService = useTicketService(); - const { closeTicket, bulkUpdateTickets: _bulkUpdateTickets } = - useTicket(ticketService); - - // State management - const [tickets, setTickets] = useState([]); - const [statistics, setStatistics] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [isUpdating, setIsUpdating] = useState>({}); - const [searchTerm, setSearchTerm] = useState(""); - const [statusFilter, setStatusFilter] = useState("all"); - const [priorityFilter, setPriorityFilter] = useState("all"); - const [page, setPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - - // Redirect non-admin users - useEffect(() => { - // Redirect if not authenticated or not admin - if (!session.loading && (!session.user || !isAdmin)) { - router.push("/tickets"); - return; - } - }, [session.loading, session.user, isAdmin, router]); - - // Load tickets - const loadTickets = async (params?: AdminGetTicketsParams) => { - if (!permissions.canAccessAdminEndpoints) { - return; - } - - setIsLoading(true); - try { - const response = await ticketService.adminGetAllTickets({ - page: page, - limit: DEFAULT_PAGE_LIMIT, - search: searchTerm || undefined, - status: - statusFilter !== "all" - ? (statusFilter as TicketStatus) - : undefined, - priority: - priorityFilter !== "all" - ? (priorityFilter as TicketPriority) - : undefined, - ...params, - }); - - if (response && response.data) { - setTickets(response.data.tickets || []); - setStatistics(response.data.statistics); - setTotalPages(response.data.pagination?.pages || 1); - } - } catch (error) { - console.error("Failed to load admin tickets:", error); - } finally { - setIsLoading(false); - } - }; - - // Load tickets on mount and filter changes - useEffect(() => { - // S'assurer que la session est chargée ET que l'utilisateur est défini ET que les permissions sont OK - if ( - !session.loading && - session.user && - permissions.canAccessAdminEndpoints && - isAdmin - ) { - loadTickets(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - searchTerm, - statusFilter, - priorityFilter, - page, - session.loading, - session.user, - permissions.canAccessAdminEndpoints, - isAdmin, - ]); - - // Handle ticket status change - const handleStatusChange = async ( - ticketId: string, - status: TicketStatus - ) => { - setIsUpdating(prev => ({ ...prev, [ticketId]: true })); - try { - const response = await ticketService.updateTicket(ticketId, { - status, - }); - if (response) { - // Update ticket in local state - setTickets(prev => - prev.map(ticket => - ticket._id === ticketId - ? { - ...ticket, - status, - updatedAt: new Date().toISOString(), - } - : ticket - ) - ); - // Reload statistics - loadTickets(); - } - } catch (error) { - console.error("Failed to update ticket status:", error); - } finally { - setIsUpdating(prev => ({ ...prev, [ticketId]: false })); - } - }; - - // Handle ticket priority change - const handlePriorityChange = async ( - ticketId: string, - priority: TicketPriority - ) => { - setIsUpdating(prev => ({ ...prev, [ticketId]: true })); - try { - const response = await ticketService.updateTicket(ticketId, { - priority, - }); - if (response) { - // Update ticket in local state - setTickets(prev => - prev.map(ticket => - ticket._id === ticketId - ? { - ...ticket, - priority, - updatedAt: new Date().toISOString(), - } - : ticket - ) - ); - } - } catch (error) { - console.error("Failed to update ticket priority:", error); - } finally { - setIsUpdating(prev => ({ ...prev, [ticketId]: false })); - } - }; - - // Handle ticket assignment (placeholder) - const handleAssign = async (_ticketId: string) => { - // TODO: Implement assignment dialog - }; - - // Handle ticket closure - const handleClose = async (ticketId: string) => { - setIsUpdating(prev => ({ ...prev, [ticketId]: true })); - try { - const result = await closeTicket(ticketId); - if (result) { - // Update local state - setTickets(prev => - prev.map(ticket => - ticket._id === ticketId - ? { - ...ticket, - status: TicketStatus.CLOSED, - updatedAt: new Date().toISOString(), - } - : ticket - ) - ); - // Reload statistics - loadTickets(); - } - } catch (error) { - console.error("Failed to close ticket:", error); - } finally { - setIsUpdating(prev => ({ ...prev, [ticketId]: false })); - } - }; - - // Get empty state message based on active filters - const getEmptyStateMessage = () => { - const hasFilters = - searchTerm || statusFilter !== "all" || priorityFilter !== "all"; - if (hasFilters) { - return "Aucun ticket ne correspond à vos critères de recherche."; - } - return "Aucun ticket n'a été trouvé dans le système."; - }; - - // Check if filters are active - const hasActiveFilters = - searchTerm || statusFilter !== "all" || priorityFilter !== "all"; - - // Don't render anything if still loading - if (session.loading) { - return ( -
-
-
- ); - } - - // Don't render anything if user is loaded but not admin - if (session.user && !isAdmin) { - return null; - } - - // If no user loaded (shouldn't happen with AuthGuard, but safety check) - if (!session.user) { - return null; - } - - return ( - -
- {/* Admin Header */} -
-
-
-
-
-
-
- -
-
- Administration -
-
-

- Gestion des tickets -

-

- Vue d'ensemble de tous les tickets du - système avec contrôles administrateur -

-
-
- -
-
-
-
-
-
- - {/* Admin Notice */} - - -
- -
-

- Mode Administrateur Activé -

-

- Vous avez accès à tous les tickets et pouvez - effectuer des modifications administrateur. -

-
-
-
-
- - {/* Statistics */} - {statistics && ( - - )} - - {/* Filters */} - - -
-
- - - setSearchTerm(e.target.value) - } - className="pl-10 bg-gray-50 border-gray-200 focus:bg-white transition-colors" - /> -
- - -
-
-
- - {/* Tickets List */} -
- {isLoading ? ( - // Loading skeleton - Array.from( - { length: LOADING_SKELETON_COUNT }, - (_, i) => ( - - -
-
-
- ) - ) - ) : tickets.length === 0 ? ( - // Empty state - - -
- -
-

- Aucun ticket trouvé -

-

- {getEmptyStateMessage()} -

- {hasActiveFilters && ( - - )} -
-
- ) : ( - // Tickets list - tickets.map(ticket => ( - - )) - )} -
- - {/* Pagination */} - {totalPages > 1 && ( -
- {[...Array(totalPages)].map((_, i) => ( - - ))} -
- )} -
-
- ); -} diff --git a/app/(dashboard)/support/layout.tsx b/app/(dashboard)/support/layout.tsx deleted file mode 100644 index 33d04954..00000000 --- a/app/(dashboard)/support/layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { ReactNode } from "react"; -import { Metadata } from "next"; -import { constructMetadata } from "@/lib/seo"; - -export const metadata: Metadata = constructMetadata({ - title: "Support et Aide - Edukai", - description: - "Besoin d'aide ? Consultez notre documentation, FAQ et contactez notre équipe support pour toute question sur Edukai.", -}); - -export default function SupportLayout({ children }: { children: ReactNode }) { - return <>{children}; -} diff --git a/app/(dashboard)/support/page.tsx b/app/(dashboard)/support/page.tsx deleted file mode 100644 index 4e5a5ee8..00000000 --- a/app/(dashboard)/support/page.tsx +++ /dev/null @@ -1,720 +0,0 @@ -"use client"; - -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; -import { - BookOpen, - CheckCircle2, - ChevronDown, - Clock, - CreditCard, - ExternalLink, - FileText, - HelpCircle, - LifeBuoy, - Mail, - MessageCircle, - Search, - Send, - Settings, - Shield, - Smartphone, - User, - Zap, -} from "lucide-react"; -import { useState } from "react"; -import { toast } from "sonner"; - -type HelpCategory = { - id: string; - title: string; - description: string; - icon: React.ComponentType<{ className?: string }>; - articles: number; - color: string; -}; - -type FAQ = { - id: string; - question: string; - answer: string; - category: string; - helpful: number; -}; - -type ContactReason = { - id: string; - title: string; - description: string; - icon: React.ComponentType<{ className?: string }>; -}; - -const helpCategories: HelpCategory[] = [ - { - id: "getting-started", - title: "Premiers pas", - description: "Découvre comment utiliser Edukai efficacement", - icon: BookOpen, - articles: 8, - color: "bg-blue-50 border-blue-200 text-blue-700", - }, - { - id: "courses", - title: "Cours et Quiz", - description: "Tout sur la création et gestion de tes cours", - icon: FileText, - articles: 12, - color: "bg-green-50 border-green-200 text-green-700", - }, - { - id: "account", - title: "Mon compte", - description: "Paramètres, profil et sécurité", - icon: User, - articles: 6, - color: "bg-purple-50 border-purple-200 text-purple-700", - }, - { - id: "subscription", - title: "Abonnement", - description: "Facturation, plans et fonctionnalités Premium", - icon: CreditCard, - articles: 5, - color: "bg-yellow-50 border-yellow-200 text-yellow-700", - }, - { - id: "technical", - title: "Problèmes techniques", - description: "Bugs, problèmes de connexion et performance", - icon: Settings, - articles: 9, - color: "bg-red-50 border-red-200 text-red-700", - }, - { - id: "mobile", - title: "Application mobile", - description: "Utilisation sur smartphone et tablette", - icon: Smartphone, - articles: 4, - color: "bg-indigo-50 border-indigo-200 text-indigo-700", - }, -]; - -const faqs: FAQ[] = [ - { - id: "1", - question: "Comment créer mon premier cours ?", - answer: "Pour créer ton premier cours, va dans la section 'Générer' et télécharge tes documents (PDF, images, etc.). Notre IA analysera automatiquement le contenu et créera un cours personnalisé avec des quiz adaptés. Le processus prend généralement 2-3 minutes.", - category: "getting-started", - helpful: 42, - }, - { - id: "2", - question: "Puis-je modifier mes cours après création ?", - answer: "Oui ! Tu peux modifier le contenu de tes cours, ajouter des notes personnelles, créer des examens personnalisés, et organiser tes fiches de révision. Va dans ta bibliothèque et clique sur le cours que tu veux modifier.", - category: "courses", - helpful: 38, - }, - { - id: "3", - question: - "Quelle est la différence entre la version gratuite et Premium ?", - answer: "La version gratuite te permet de créer 3 cours par mois avec des fonctionnalités de base. Premium débloque la création illimitée, l'IA avancée, les statistiques détaillées, le partage de cours, et l'accès prioritaire aux nouvelles fonctionnalités.", - category: "subscription", - helpful: 56, - }, - { - id: "4", - question: "Mes données sont-elles sécurisées ?", - answer: "Absolument ! Nous utilisons un chiffrement de niveau bancaire (AES-256) pour protéger tes données. Tes documents et cours sont stockés de manière sécurisée et ne sont jamais partagés avec des tiers. Tu peux consulter notre politique de confidentialité pour plus de détails.", - category: "account", - helpful: 29, - }, - { - id: "5", - question: "L'application fonctionne-t-elle hors ligne ?", - answer: "Tu peux consulter tes cours téléchargés hors ligne sur l'application mobile. Cependant, la création de nouveaux cours et la synchronisation nécessitent une connexion internet.", - category: "mobile", - helpful: 33, - }, - { - id: "6", - question: "Comment puis-je annuler mon abonnement ?", - answer: "Tu peux annuler ton abonnement à tout moment dans tes paramètres de compte. Va dans 'Paramètres' > 'Abonnement' > 'Gérer l'abonnement'. L'annulation prend effet à la fin de la période de facturation en cours.", - category: "subscription", - helpful: 21, - }, -]; - -const contactReasons: ContactReason[] = [ - { - id: "bug", - title: "Signaler un bug", - description: "Quelque chose ne fonctionne pas comme prévu", - icon: Zap, - }, - { - id: "feature", - title: "Demande de fonctionnalité", - description: "Suggérer une amélioration ou nouvelle fonctionnalité", - icon: HelpCircle, - }, - { - id: "billing", - title: "Question de facturation", - description: "Problème avec ton abonnement ou paiement", - icon: CreditCard, - }, - { - id: "content", - title: "Problème de contenu", - description: "Cours généré incorrectement ou contenu inapproprié", - icon: FileText, - }, - { - id: "account", - title: "Compte utilisateur", - description: "Problème de connexion, mot de passe ou profil", - icon: User, - }, - { - id: "other", - title: "Autre", - description: "Une question qui ne rentre dans aucune catégorie", - icon: MessageCircle, - }, -]; - -export default function SupportPage() { - const [searchQuery, setSearchQuery] = useState(""); - const [selectedCategory, setSelectedCategory] = useState( - null - ); - const [openFAQ, setOpenFAQ] = useState(null); - const [contactForm, setContactForm] = useState({ - name: "", - email: "", - reason: "", - subject: "", - message: "", - }); - - const filteredFAQs = faqs.filter(faq => { - const matchesSearch = - faq.question.toLowerCase().includes(searchQuery.toLowerCase()) || - faq.answer.toLowerCase().includes(searchQuery.toLowerCase()); - const matchesCategory = - !selectedCategory || faq.category === selectedCategory; - return matchesSearch && matchesCategory; - }); - - const handleContactSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - if ( - !contactForm.name || - !contactForm.email || - !contactForm.subject || - !contactForm.message - ) { - toast.error("Veuillez remplir tous les champs obligatoires"); - return; - } - - // Simulate form submission - toast.success( - "Votre message a été envoyé ! Nous vous répondrons sous 24h." - ); - setContactForm({ - name: "", - email: "", - reason: "", - subject: "", - message: "", - }); - }; - - return ( -
- {/* Header */} -
-
-
-
-
- -
- - Support 24/7 - -
-

- Centre d'assistance -

-

- Trouve des réponses rapides ou contacte notre équipe - pour une aide personnalisée. -

-
- {/* Decorative elements */} -
-
-
- - {/* Quick Actions */} -
- - -
- -
-

- Chat en direct -

-

- Discute avec notre équipe en temps réel -

- -
-
- - - -
- -
-

- Email -

-

- Envoie-nous un email détaillé -

- -
-
- - - -
- -
-

- Horaires -

-

- Lun-Ven: 9h-18h -

-

- Sam-Dim: 10h-16h -

-
-
-
- - {/* Search and Categories */} - - -
- {/* Search */} -
- - setSearchQuery(e.target.value)} - className="pl-10 bg-white/50 border-gray-200 text-sm lg:text-base" - /> -
- - {/* Categories */} -
- {helpCategories.map(category => ( - - ))} -
-
-
-
- - {/* FAQ Section */} - - - -
- - Questions fréquentes -
- {selectedCategory && ( - - { - helpCategories.find( - c => c.id === selectedCategory - )?.title - } - - )} -
-
- - {filteredFAQs.length === 0 ? ( -
- -

- Aucune question trouvée -

-

- Essaie avec d'autres mots-clés ou contacte notre - équipe. -

-
- ) : ( -
- {filteredFAQs.map(faq => ( - - setOpenFAQ(open ? faq.id : null) - } - > - - - - - {faq.answer} -
- - Cette réponse était-elle utile ? - -
- - -
-
-
-
- ))} -
- )} -
-
- - {/* Contact Form */} - - - - - Contacter l'équipe support - - - -
-
-
- - - setContactForm(prev => ({ - ...prev, - name: e.target.value, - })) - } - className="bg-white/50 border-gray-200 text-sm lg:text-base" - placeholder="Ton nom" - /> -
-
- - - setContactForm(prev => ({ - ...prev, - email: e.target.value, - })) - } - className="bg-white/50 border-gray-200 text-sm lg:text-base" - placeholder="ton@email.com" - /> -
-
- -
- -
- {contactReasons.map(reason => ( - - ))} -
-
- -
- - - setContactForm(prev => ({ - ...prev, - subject: e.target.value, - })) - } - className="bg-white/50 border-gray-200 text-sm lg:text-base" - placeholder="Décris brièvement ton problème" - /> -
- -
- -