import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { useAuthUnified } from "@/hooks/useAuthUnified"; import { useToast } from "@/hooks/use-toast"; import { apiRequest } from "@/lib/queryClient"; import { Plus, Search, Filter, Wrench, Calendar, Eye, Edit, Trash2, Building, User, MessageCircle, AlertTriangle, CheckCircle, Clock, Ban } from "lucide-react"; import { safeFormat } from "@/lib/dateUtils"; import type { SavTicketWithRelations, Supplier, Group, InsertSavTicket } from "@shared/schema"; const statusConfig = { nouveau: { label: "Nouveau", color: "bg-blue-100 text-blue-800", icon: AlertTriangle }, en_cours: { label: "En cours", color: "bg-yellow-100 text-yellow-800", icon: Clock }, attente_pieces: { label: "Attente pièces", color: "bg-orange-100 text-orange-800", icon: Clock }, attente_echange: { label: "Attente échange", color: "bg-purple-100 text-purple-800", icon: Clock }, resolu: { label: "Résolu", color: "bg-green-100 text-green-800", icon: CheckCircle }, ferme: { label: "Fermé", color: "bg-gray-100 text-gray-800", icon: Ban } }; const priorityConfig = { faible: { label: "Faible", color: "bg-gray-100 text-gray-800" }, normale: { label: "Normale", color: "bg-blue-100 text-blue-800" }, haute: { label: "Haute", color: "bg-orange-100 text-orange-800" }, critique: { label: "Critique", color: "bg-red-100 text-red-800" } }; export default function SavTickets() { const { user } = useAuthUnified(); const { toast } = useToast(); const queryClient = useQueryClient(); const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [priorityFilter, setPriorityFilter] = useState("all"); const [supplierFilter, setSupplierFilter] = useState("all"); const [showCreateModal, setShowCreateModal] = useState(false); const [showDetailModal, setShowDetailModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [selectedTicket, setSelectedTicket] = useState(null); const [newComment, setNewComment] = useState(""); const [tempPriority, setTempPriority] = useState(""); const [tempStatus, setTempStatus] = useState(""); const [formData, setFormData] = useState({ supplierId: "", groupId: "", productGencode: "", productReference: "", productDesignation: "", problemType: "defectueux", problemDescription: "", priority: "normale", clientName: "", clientPhone: "" }); // Build query parameters const queryParams = new URLSearchParams(); if (statusFilter !== 'all') queryParams.append('status', statusFilter); if (priorityFilter !== 'all') queryParams.append('priority', priorityFilter); if (supplierFilter !== 'all') queryParams.append('supplierId', supplierFilter); const ticketsUrl = `/api/sav/tickets${queryParams.toString() ? '?' + queryParams.toString() : ''}`; const { data: ticketsData = [], isLoading } = useQuery({ queryKey: [ticketsUrl], staleTime: 1000 * 60 * 5, // 5 minutes }); const { data: suppliersData = [] } = useQuery({ queryKey: ['/api/suppliers'], staleTime: 1000 * 60 * 10, // 10 minutes }); const { data: groupsData = [] } = useQuery({ queryKey: ['/api/groups'], staleTime: 1000 * 60 * 10, // 10 minutes }); const { data: statsData } = useQuery<{ totalTickets: number; newTickets: number; inProgressTickets: number; resolvedTickets: number; criticalTickets: number; }>({ queryKey: ['/api/sav/stats'], staleTime: 1000 * 60 * 2, // 2 minutes }); // Create ticket mutation const createTicketMutation = useMutation({ mutationFn: async (data: InsertSavTicket) => { const response = await fetch('/api/sav/tickets', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || 'Erreur lors de la création du ticket'); } return response.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/sav/tickets'] }); queryClient.invalidateQueries({ queryKey: ['/api/sav/stats'] }); setShowCreateModal(false); setFormData({ supplierId: "", groupId: "", productGencode: "", productReference: "", productDesignation: "", problemType: "defectueux", problemDescription: "", priority: "normale", clientName: "", clientPhone: "" }); toast({ title: "Ticket créé", description: "Le ticket SAV a été créé avec succès.", }); }, onError: (error: any) => { console.error('Error creating ticket:', error); toast({ title: "Erreur", description: error?.message || "Impossible de créer le ticket.", variant: "destructive", }); }, }); // Add comment mutation const addCommentMutation = useMutation({ mutationFn: async (data: { ticketId: number; comment: string }) => { const response = await apiRequest(`/api/sav/tickets/${data.ticketId}/history`, "POST", { description: data.comment // ✅ Utilise 'description' selon le schéma API }); return response; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/sav/tickets'] }); // ✅ Invalider aussi les détails du ticket pour rafraîchir l'historique if (selectedTicket) { queryClient.invalidateQueries({ queryKey: [`/api/sav/tickets/${selectedTicket.id}`] }); } setNewComment(""); toast({ title: "Commentaire ajouté", description: "Le commentaire a été ajouté avec succès.", }); }, onError: (error: any) => { toast({ title: "Erreur", description: error?.message || "Impossible d'ajouter le commentaire.", variant: "destructive", }); }, }); // Delete ticket mutation const deleteTicketMutation = useMutation({ mutationFn: async (ticketId: number) => { const response = await apiRequest(`/api/sav/tickets/${ticketId}`, "DELETE"); return response; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/sav/tickets'] }); queryClient.invalidateQueries({ queryKey: ['/api/sav/stats'] }); setShowDeleteModal(false); toast({ title: "Ticket supprimé", description: "Le ticket a été supprimé avec succès.", }); }, onError: (error: any) => { toast({ title: "Erreur", description: error?.message || "Impossible de supprimer le ticket.", variant: "destructive", }); }, }); // Check permissions AFTER all hooks are called const canView = ['admin', 'directeur', 'manager', 'employee'].includes(user?.role || ''); const canModify = ['admin', 'directeur', 'manager'].includes(user?.role || ''); const canDelete = ['admin', 'directeur'].includes(user?.role || ''); // Get user's available groups for auto-assignment const getUserGroups = () => { if (!user || user.role === 'admin') { return groupsData; // Admin can access all groups } // For other users, get their assigned groups const userGroups = (user as any).userGroups || []; return groupsData.filter(group => userGroups.some((ug: any) => ug.groupId === group.id) ); }; const availableGroups = getUserGroups(); // Handle form submission const handleCreateTicket = () => { if (!formData.supplierId || !formData.productGencode || !formData.productDesignation || !formData.problemDescription) { toast({ title: "Erreur de validation", description: "Veuillez remplir tous les champs obligatoires.", variant: "destructive", }); return; } // Auto-assign group if user has only one group or if admin didn't select one let selectedGroupId = formData.groupId ? parseInt(formData.groupId) : null; if (!selectedGroupId && availableGroups.length > 0) { // Auto-assign first available group selectedGroupId = availableGroups[0].id; } const ticketData: InsertSavTicket = { supplierId: parseInt(formData.supplierId), groupId: selectedGroupId || undefined, productGencode: formData.productGencode, productReference: formData.productReference || undefined, productDesignation: formData.productDesignation, problemType: formData.problemType as "defectueux" | "pieces_manquantes" | "non_conforme" | "autre", problemDescription: formData.problemDescription, priority: formData.priority as "faible" | "normale" | "haute" | "critique", clientName: formData.clientName || undefined, clientPhone: formData.clientPhone || undefined, }; console.log('Creating ticket with data:', ticketData); createTicketMutation.mutate(ticketData); }; // Handle viewing ticket details const handleViewTicket = (ticket: SavTicketWithRelations) => { setSelectedTicket(ticket); setShowDetailModal(true); }; // Query pour récupérer les détails complets du ticket avec historique const { data: ticketDetails } = useQuery({ queryKey: [`/api/sav/tickets/${selectedTicket?.id}`], enabled: !!selectedTicket && showDetailModal, staleTime: 0, // Toujours récupérer les derniers commentaires }); // Handle editing ticket const handleEditTicket = (ticket: SavTicketWithRelations) => { setSelectedTicket(ticket); // Pre-fill form with ticket data setFormData({ supplierId: ticket.supplierId.toString(), groupId: ticket.groupId.toString(), productGencode: ticket.productGencode, productReference: ticket.productReference || "", productDesignation: ticket.productDesignation, problemType: ticket.problemType, problemDescription: ticket.problemDescription, priority: ticket.priority, clientName: ticket.clientName || "", clientPhone: ticket.clientPhone || "" }); setShowEditModal(true); }; // Handle deleting ticket const handleDeleteTicket = (ticket: SavTicketWithRelations) => { setSelectedTicket(ticket); setShowDeleteModal(true); }; // Handle permission check without early return if (!canView) { return (

Accès restreint
Vous n'avez pas les permissions nécessaires pour accéder au module SAV.

); } // Filter tickets by search term const filteredTickets = ticketsData.filter(ticket => { if (!searchTerm) return true; const searchLower = searchTerm.toLowerCase(); return ( ticket.ticketNumber.toLowerCase().includes(searchLower) || ticket.clientName?.toLowerCase().includes(searchLower) || ticket.problemDescription?.toLowerCase().includes(searchLower) || ticket.productDesignation?.toLowerCase().includes(searchLower) || ticket.supplier?.name.toLowerCase().includes(searchLower) ); }); return (
{/* Header */}

Service Après-Vente

Gestion des tickets SAV et suivi des réparations

{canModify && ( Créer un nouveau ticket SAV Remplissez les informations pour créer un nouveau ticket de service après-vente.
{/* Informations produit */}

Informations produit

setFormData({...formData, productGencode: e.target.value})} placeholder="Code-barres du produit" />
setFormData({...formData, productReference: e.target.value})} placeholder="Référence du produit" />
setFormData({...formData, productDesignation: e.target.value})} placeholder="Nom/désignation du produit" />
{availableGroups.length > 1 && (
)} {availableGroups.length === 1 && (
{availableGroups[0].name}
)} {availableGroups.length === 0 && (
Aucun magasin assigné. Contactez un administrateur.
)}
{/* Informations problème et client */}

Problème et client