Improve SAV ticket management by fixing status updates and adding new comment indicators

Implement ticket status update mutation, show new comment badges, and refactor SAV modal layout for better client and product information separation.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: f9197b9b-a5b3-4469-a27d-930a9e9ee736
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/f9197b9b-a5b3-4469-a27d-930a9e9ee736/GCVoWnM
This commit is contained in:
michaelschal committed 2025-08-18 07:32:44 +00:00
1 parent 5974b1a29e
commit 50a8807019
1 file changed
+190 -76
+190 -76
View File
@@ -190,6 +190,38 @@ export default function SavTickets() {
}, },
}); });
// Update ticket status/priority mutation
const updateTicketMutation = useMutation({
mutationFn: async (data: { ticketId: number; status?: string; priority?: string; resolutionDescription?: string }) => {
const response = await apiRequest(`/api/sav/tickets/${data.ticketId}`, "PATCH", {
status: data.status,
priority: data.priority,
resolutionDescription: data.resolutionDescription,
});
return response;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/sav/tickets'] });
queryClient.invalidateQueries({ queryKey: ['/api/sav/stats'] });
if (selectedTicket) {
queryClient.invalidateQueries({ queryKey: [`/api/sav/tickets/${selectedTicket.id}`] });
}
setTempStatus("");
setTempPriority("");
toast({
title: "Ticket mis à jour",
description: "Le statut du ticket a été modifié avec succès.",
});
},
onError: (error: any) => {
toast({
title: "Erreur",
description: error?.message || "Impossible de modifier le ticket.",
variant: "destructive",
});
},
});
// Delete ticket mutation // Delete ticket mutation
const deleteTicketMutation = useMutation({ const deleteTicketMutation = useMutation({
mutationFn: async (ticketId: number) => { mutationFn: async (ticketId: number) => {
@@ -263,7 +295,7 @@ export default function SavTickets() {
problemDescription: formData.problemDescription, problemDescription: formData.problemDescription,
priority: formData.priority as "faible" | "normale" | "haute" | "critique", priority: formData.priority as "faible" | "normale" | "haute" | "critique",
status: "nouveau" as const, status: "nouveau" as const,
createdBy: 1, // Will be set by the backend from session createdBy: "1", // Will be set by the backend from session
clientName: formData.clientName || undefined, clientName: formData.clientName || undefined,
clientPhone: formData.clientPhone || undefined, clientPhone: formData.clientPhone || undefined,
}; };
@@ -285,6 +317,20 @@ export default function SavTickets() {
staleTime: 0, // Toujours récupérer les derniers commentaires staleTime: 0, // Toujours récupérer les derniers commentaires
}); });
// Function to check if ticket has recent comments (last 24h)
const hasRecentComments = (ticket: SavTicketWithRelations) => {
if (!ticket.history || ticket.history.length === 0) return false;
const oneDayAgo = new Date();
oneDayAgo.setDate(oneDayAgo.getDate() - 1);
return ticket.history.some(entry =>
entry.action === 'comment' &&
entry.createdAt &&
new Date(entry.createdAt) > oneDayAgo
);
};
// Handle editing ticket // Handle editing ticket
const handleEditTicket = (ticket: SavTicketWithRelations) => { const handleEditTicket = (ticket: SavTicketWithRelations) => {
setSelectedTicket(ticket); setSelectedTicket(ticket);
@@ -370,11 +416,20 @@ export default function SavTickets() {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="space-y-6">
{/* Informations produit */} {/* Section Informations Produit */}
<div className="space-y-4"> <Card>
<h3 className="text-lg font-medium">Informations produit</h3> <CardHeader>
<CardTitle className="text-lg flex items-center">
<Building className="h-5 w-5 mr-2" />
Informations Produit
</CardTitle>
<CardDescription>
Détails du produit concerné par le SAV
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="productGencode">Code-barres produit *</Label> <Label htmlFor="productGencode">Code-barres produit *</Label>
<Input <Input
@@ -394,6 +449,7 @@ export default function SavTickets() {
placeholder="Référence du produit" placeholder="Référence du produit"
/> />
</div> </div>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="productDesignation">Désignation produit *</Label> <Label htmlFor="productDesignation">Désignation produit *</Label>
@@ -405,6 +461,7 @@ export default function SavTickets() {
/> />
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="supplierId">Fournisseur *</Label> <Label htmlFor="supplierId">Fournisseur *</Label>
<Select value={formData.supplierId} onValueChange={(value) => setFormData({...formData, supplierId: value})}> <Select value={formData.supplierId} onValueChange={(value) => setFormData({...formData, supplierId: value})}>
@@ -438,6 +495,7 @@ export default function SavTickets() {
</Select> </Select>
</div> </div>
)} )}
</div>
{availableGroups.length === 1 && ( {availableGroups.length === 1 && (
<div className="space-y-2"> <div className="space-y-2">
@@ -457,12 +515,58 @@ export default function SavTickets() {
</div> </div>
</div> </div>
)} )}
</CardContent>
</Card>
{/* Section Informations Client */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center">
<User className="h-5 w-5 mr-2" />
Informations Client
</CardTitle>
<CardDescription>
Informations de contact du client (optionnel)
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="clientName">Nom du client</Label>
<Input
id="clientName"
value={formData.clientName}
onChange={(e) => setFormData({...formData, clientName: e.target.value})}
placeholder="Nom du client"
/>
</div> </div>
{/* Informations problème et client */} <div className="space-y-2">
<div className="space-y-4"> <Label htmlFor="clientPhone">Téléphone du client</Label>
<h3 className="text-lg font-medium">Problème et client</h3> <Input
id="clientPhone"
value={formData.clientPhone}
onChange={(e) => setFormData({...formData, clientPhone: e.target.value})}
placeholder="Numéro de téléphone"
/>
</div>
</div>
</CardContent>
</Card>
{/* Section Problème et Priorité */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center">
<AlertTriangle className="h-5 w-5 mr-2" />
Problème et Priorité
</CardTitle>
<CardDescription>
Description du problème rencontré
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="problemType">Type de problème</Label> <Label htmlFor="problemType">Type de problème</Label>
<Select value={formData.problemType} onValueChange={(value) => setFormData({...formData, problemType: value})}> <Select value={formData.problemType} onValueChange={(value) => setFormData({...formData, problemType: value})}>
@@ -492,6 +596,7 @@ export default function SavTickets() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="problemDescription">Description du problème *</Label> <Label htmlFor="problemDescription">Description du problème *</Label>
@@ -503,27 +608,8 @@ export default function SavTickets() {
rows={4} rows={4}
/> />
</div> </div>
</CardContent>
<div className="space-y-2"> </Card>
<Label htmlFor="clientName">Nom du client</Label>
<Input
id="clientName"
value={formData.clientName}
onChange={(e) => setFormData({...formData, clientName: e.target.value})}
placeholder="Nom du client"
/>
</div>
<div className="space-y-2">
<Label htmlFor="clientPhone">Téléphone client</Label>
<Input
id="clientPhone"
value={formData.clientPhone}
onChange={(e) => setFormData({...formData, clientPhone: e.target.value})}
placeholder="Numéro de téléphone"
/>
</div>
</div>
</div> </div>
<div className="flex justify-end space-x-2 mt-6"> <div className="flex justify-end space-x-2 mt-6">
@@ -719,9 +805,15 @@ export default function SavTickets() {
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center"> <div className="flex items-center">
<Wrench className="h-4 w-4 text-gray-400 mr-2" /> <Wrench className="h-4 w-4 text-gray-400 mr-2" />
<span className="text-sm font-medium text-gray-900"> <span className="text-sm font-medium text-gray-900 mr-2">
{ticket.ticketNumber} {ticket.ticketNumber}
</span> </span>
{hasRecentComments(ticket) && (
<Badge className="bg-orange-100 text-orange-800 text-xs">
<MessageCircle className="h-3 w-3 mr-1" />
Nouveau
</Badge>
)}
</div> </div>
</td> </td>
<td className="px-6 py-4"> <td className="px-6 py-4">
@@ -822,10 +914,15 @@ export default function SavTickets() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 overflow-hidden"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 overflow-hidden">
{/* Colonne de gauche - Détails et Actions */} {/* Colonne de gauche - Détails et Actions */}
<div className="space-y-6 overflow-y-auto max-h-[70vh] pr-2"> <div className="space-y-6 overflow-y-auto max-h-[70vh] pr-2">
{/* Product Info */} {/* Section Informations Produit */}
<div className="space-y-4"> <Card>
<h3 className="text-lg font-medium">Informations produit</h3> <CardHeader>
<div className="grid grid-cols-2 gap-4 text-sm"> <CardTitle className="text-lg flex items-center">
<Building className="h-5 w-5 mr-2" />
Informations Produit
</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-2 gap-4 text-sm">
<div> <div>
<span className="font-medium">Code-barres:</span> <span className="font-medium">Code-barres:</span>
<p>{selectedTicket.productGencode}</p> <p>{selectedTicket.productGencode}</p>
@@ -838,12 +935,40 @@ export default function SavTickets() {
<span className="font-medium">Désignation:</span> <span className="font-medium">Désignation:</span>
<p>{selectedTicket.productDesignation}</p> <p>{selectedTicket.productDesignation}</p>
</div> </div>
</div> </CardContent>
</div> </Card>
{/* Problem Info */} {/* Section Informations Client */}
<div className="space-y-4"> {(selectedTicket.clientName || selectedTicket.clientPhone) && (
<h3 className="text-lg font-medium">Problème</h3> <Card>
<CardHeader>
<CardTitle className="text-lg flex items-center">
<User className="h-5 w-5 mr-2" />
Informations Client
</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Nom:</span>
<p>{selectedTicket.clientName || "Non renseigné"}</p>
</div>
<div>
<span className="font-medium">Téléphone:</span>
<p>{selectedTicket.clientPhone || "Non renseigné"}</p>
</div>
</CardContent>
</Card>
)}
{/* Section Problème */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center">
<AlertTriangle className="h-5 w-5 mr-2" />
Problème
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4 text-sm"> <div className="grid grid-cols-2 gap-4 text-sm">
<div> <div>
<span className="font-medium">Type:</span> <span className="font-medium">Type:</span>
@@ -855,34 +980,23 @@ export default function SavTickets() {
{priorityConfig[selectedTicket.priority as keyof typeof priorityConfig]?.label} {priorityConfig[selectedTicket.priority as keyof typeof priorityConfig]?.label}
</Badge> </Badge>
</div> </div>
<div className="col-span-2"> </div>
<div>
<span className="font-medium">Description:</span> <span className="font-medium">Description:</span>
<p className="mt-1 p-2 bg-gray-50 rounded">{selectedTicket.problemDescription}</p> <p className="mt-1 p-2 bg-gray-50 rounded">{selectedTicket.problemDescription}</p>
</div> </div>
</div> </CardContent>
</div> </Card>
{/* Client Info */} {/* Section Informations Ticket */}
{(selectedTicket.clientName || selectedTicket.clientPhone) && ( <Card>
<div className="space-y-4"> <CardHeader>
<h3 className="text-lg font-medium">Client</h3> <CardTitle className="text-lg flex items-center">
<div className="grid grid-cols-2 gap-4 text-sm"> <Wrench className="h-5 w-5 mr-2" />
<div> Informations Ticket
<span className="font-medium">Nom:</span> </CardTitle>
<p>{selectedTicket.clientName || "Non renseigné"}</p> </CardHeader>
</div> <CardContent className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Téléphone:</span>
<p>{selectedTicket.clientPhone || "Non renseigné"}</p>
</div>
</div>
</div>
)}
{/* Ticket Info */}
<div className="space-y-4">
<h3 className="text-lg font-medium">Informations ticket</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div> <div>
<span className="font-medium">Statut:</span> <span className="font-medium">Statut:</span>
<Badge className={statusConfig[selectedTicket.status as keyof typeof statusConfig]?.color}> <Badge className={statusConfig[selectedTicket.status as keyof typeof statusConfig]?.color}>
@@ -901,8 +1015,8 @@ export default function SavTickets() {
<span className="font-medium">Créé le:</span> <span className="font-medium">Créé le:</span>
<p>{safeFormat(selectedTicket.createdAt, 'dd/MM/yyyy HH:mm')}</p> <p>{safeFormat(selectedTicket.createdAt, 'dd/MM/yyyy HH:mm')}</p>
</div> </div>
</div> </CardContent>
</div> </Card>
{/* Actions rapides */} {/* Actions rapides */}
{canModify && ( {canModify && (
@@ -911,11 +1025,11 @@ export default function SavTickets() {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="quick-priority">Priorité</Label> <Label htmlFor="quick-priority">Priorité</Label>
<Select value={selectedTicket.priority} onValueChange={(value) => { <Select value={tempPriority || selectedTicket.priority} onValueChange={(value) => {
// TODO: Implement priority update setTempPriority(value);
toast({ updateTicketMutation.mutate({
title: "Fonction en développement", ticketId: selectedTicket.id,
description: "La modification de priorité sera bientôt disponible.", priority: value
}); });
}}> }}>
<SelectTrigger> <SelectTrigger>
@@ -932,11 +1046,11 @@ export default function SavTickets() {
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="quick-status">Statut</Label> <Label htmlFor="quick-status">Statut</Label>
<Select value={selectedTicket.status} onValueChange={(value) => { <Select value={tempStatus || selectedTicket.status} onValueChange={(value) => {
// TODO: Implement status update setTempStatus(value);
toast({ updateTicketMutation.mutate({
title: "Fonction en développement", ticketId: selectedTicket.id,
description: "La modification de statut sera bientôt disponible.", status: value
}); });
}}> }}>
<SelectTrigger> <SelectTrigger>