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
+314 -200
+314 -200
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,160 +416,200 @@ 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">
<div className="space-y-2"> <Building className="h-5 w-5 mr-2" />
<Label htmlFor="productGencode">Code-barres produit *</Label> Informations Produit
<Input </CardTitle>
id="productGencode" <CardDescription>
value={formData.productGencode} Détails du produit concerné par le SAV
onChange={(e) => setFormData({...formData, productGencode: e.target.value})} </CardDescription>
placeholder="Code-barres du produit" </CardHeader>
/> <CardContent className="space-y-4">
</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="productGencode">Code-barres produit *</Label>
<Label htmlFor="productReference">Référence produit</Label> <Input
<Input id="productGencode"
id="productReference" value={formData.productGencode}
value={formData.productReference} onChange={(e) => setFormData({...formData, productGencode: e.target.value})}
onChange={(e) => setFormData({...formData, productReference: e.target.value})} placeholder="Code-barres du produit"
placeholder="Référence du produit" />
/> </div>
</div>
<div className="space-y-2">
<div className="space-y-2"> <Label htmlFor="productReference">Référence produit</Label>
<Label htmlFor="productDesignation">Désignation produit *</Label> <Input
<Input id="productReference"
id="productDesignation" value={formData.productReference}
value={formData.productDesignation} onChange={(e) => setFormData({...formData, productReference: e.target.value})}
onChange={(e) => setFormData({...formData, productDesignation: e.target.value})} placeholder="Référence du produit"
placeholder="Nom/désignation du produit" />
/>
</div>
<div className="space-y-2">
<Label htmlFor="supplierId">Fournisseur *</Label>
<Select value={formData.supplierId} onValueChange={(value) => setFormData({...formData, supplierId: value})}>
<SelectTrigger>
<SelectValue placeholder="Sélectionner un fournisseur" />
</SelectTrigger>
<SelectContent>
{suppliersData.map((supplier) => (
<SelectItem key={supplier.id} value={supplier.id.toString()}>
{supplier.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{availableGroups.length > 1 && (
<div className="space-y-2">
<Label htmlFor="groupId">Magasin</Label>
<Select value={formData.groupId} onValueChange={(value) => setFormData({...formData, groupId: value})}>
<SelectTrigger>
<SelectValue placeholder="Sélectionner un magasin" />
</SelectTrigger>
<SelectContent>
{availableGroups.map((group) => (
<SelectItem key={group.id} value={group.id.toString()}>
{group.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{availableGroups.length === 1 && (
<div className="space-y-2">
<Label>Magasin assigné</Label>
<div className="flex items-center space-x-2 p-2 bg-gray-50 rounded">
<Building className="h-4 w-4 text-gray-500" />
<span className="text-sm text-gray-700">{availableGroups[0].name}</span>
</div> </div>
</div> </div>
)}
{availableGroups.length === 0 && (
<div className="space-y-2"> <div className="space-y-2">
<Label>Magasin</Label> <Label htmlFor="productDesignation">Désignation produit *</Label>
<div className="p-2 bg-orange-50 border border-orange-200 rounded text-sm text-orange-700"> <Input
Aucun magasin assigné. Contactez un administrateur. id="productDesignation"
</div> value={formData.productDesignation}
onChange={(e) => setFormData({...formData, productDesignation: e.target.value})}
placeholder="Nom/désignation du produit"
/>
</div> </div>
)}
</div> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="supplierId">Fournisseur *</Label>
<Select value={formData.supplierId} onValueChange={(value) => setFormData({...formData, supplierId: value})}>
<SelectTrigger>
<SelectValue placeholder="Sélectionner un fournisseur" />
</SelectTrigger>
<SelectContent>
{suppliersData.map((supplier) => (
<SelectItem key={supplier.id} value={supplier.id.toString()}>
{supplier.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{availableGroups.length > 1 && (
<div className="space-y-2">
<Label htmlFor="groupId">Magasin</Label>
<Select value={formData.groupId} onValueChange={(value) => setFormData({...formData, groupId: value})}>
<SelectTrigger>
<SelectValue placeholder="Sélectionner un magasin" />
</SelectTrigger>
<SelectContent>
{availableGroups.map((group) => (
<SelectItem key={group.id} value={group.id.toString()}>
{group.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
{availableGroups.length === 1 && (
<div className="space-y-2">
<Label>Magasin assigné</Label>
<div className="flex items-center space-x-2 p-2 bg-gray-50 rounded">
<Building className="h-4 w-4 text-gray-500" />
<span className="text-sm text-gray-700">{availableGroups[0].name}</span>
</div>
</div>
)}
{availableGroups.length === 0 && (
<div className="space-y-2">
<Label>Magasin</Label>
<div className="p-2 bg-orange-50 border border-orange-200 rounded text-sm text-orange-700">
Aucun magasin assigné. Contactez un administrateur.
</div>
</div>
)}
</CardContent>
</Card>
{/* Informations problème et client */} {/* Section Informations Client */}
<div className="space-y-4"> <Card>
<h3 className="text-lg font-medium">Problème et client</h3> <CardHeader>
<CardTitle className="text-lg flex items-center">
<div className="space-y-2"> <User className="h-5 w-5 mr-2" />
<Label htmlFor="problemType">Type de problème</Label> Informations Client
<Select value={formData.problemType} onValueChange={(value) => setFormData({...formData, problemType: value})}> </CardTitle>
<SelectTrigger> <CardDescription>
<SelectValue /> Informations de contact du client (optionnel)
</SelectTrigger> </CardDescription>
<SelectContent> </CardHeader>
<SelectItem value="defectueux">Défectueux</SelectItem> <CardContent className="space-y-4">
<SelectItem value="pieces_manquantes">Pièces manquantes</SelectItem> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<SelectItem value="non_conforme">Non conforme</SelectItem> <div className="space-y-2">
<SelectItem value="autre">Autre</SelectItem> <Label htmlFor="clientName">Nom du client</Label>
</SelectContent> <Input
</Select> id="clientName"
</div> value={formData.clientName}
onChange={(e) => setFormData({...formData, clientName: e.target.value})}
<div className="space-y-2"> placeholder="Nom du client"
<Label htmlFor="priority">Priorité</Label> />
<Select value={formData.priority} onValueChange={(value) => setFormData({...formData, priority: value})}> </div>
<SelectTrigger>
<SelectValue /> <div className="space-y-2">
</SelectTrigger> <Label htmlFor="clientPhone">Téléphone du client</Label>
<SelectContent> <Input
<SelectItem value="faible">Faible</SelectItem> id="clientPhone"
<SelectItem value="normale">Normale</SelectItem> value={formData.clientPhone}
<SelectItem value="haute">Haute</SelectItem> onChange={(e) => setFormData({...formData, clientPhone: e.target.value})}
<SelectItem value="critique">Critique</SelectItem> placeholder="Numéro de téléphone"
</SelectContent> />
</Select> </div>
</div> </div>
</CardContent>
<div className="space-y-2"> </Card>
<Label htmlFor="problemDescription">Description du problème *</Label>
<Textarea {/* Section Problème et Priorité */}
id="problemDescription" <Card>
value={formData.problemDescription} <CardHeader>
onChange={(e) => setFormData({...formData, problemDescription: e.target.value})} <CardTitle className="text-lg flex items-center">
placeholder="Décrivez le problème en détail..." <AlertTriangle className="h-5 w-5 mr-2" />
rows={4} Problème et Priorité
/> </CardTitle>
</div> <CardDescription>
Description du problème rencontré
<div className="space-y-2"> </CardDescription>
<Label htmlFor="clientName">Nom du client</Label> </CardHeader>
<Input <CardContent className="space-y-4">
id="clientName" <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
value={formData.clientName} <div className="space-y-2">
onChange={(e) => setFormData({...formData, clientName: e.target.value})} <Label htmlFor="problemType">Type de problème</Label>
placeholder="Nom du client" <Select value={formData.problemType} onValueChange={(value) => setFormData({...formData, problemType: value})}>
/> <SelectTrigger>
</div> <SelectValue />
</SelectTrigger>
<div className="space-y-2"> <SelectContent>
<Label htmlFor="clientPhone">Téléphone client</Label> <SelectItem value="defectueux">Défectueux</SelectItem>
<Input <SelectItem value="pieces_manquantes">Pièces manquantes</SelectItem>
id="clientPhone" <SelectItem value="non_conforme">Non conforme</SelectItem>
value={formData.clientPhone} <SelectItem value="autre">Autre</SelectItem>
onChange={(e) => setFormData({...formData, clientPhone: e.target.value})} </SelectContent>
placeholder="Numéro de téléphone" </Select>
/> </div>
</div>
</div> <div className="space-y-2">
<Label htmlFor="priority">Priorité</Label>
<Select value={formData.priority} onValueChange={(value) => setFormData({...formData, priority: value})}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="faible">Faible</SelectItem>
<SelectItem value="normale">Normale</SelectItem>
<SelectItem value="haute">Haute</SelectItem>
<SelectItem value="critique">Critique</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="problemDescription">Description du problème *</Label>
<Textarea
id="problemDescription"
value={formData.problemDescription}
onChange={(e) => setFormData({...formData, problemDescription: e.target.value})}
placeholder="Décrivez le problème en détail..."
rows={4}
/>
</div>
</CardContent>
</Card>
</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,35 +935,19 @@ 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">
<h3 className="text-lg font-medium">Problème</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Type:</span>
<p>{selectedTicket.problemType}</p>
</div>
<div>
<span className="font-medium">Priorité:</span>
<Badge className={priorityConfig[selectedTicket.priority as keyof typeof priorityConfig]?.color}>
{priorityConfig[selectedTicket.priority as keyof typeof priorityConfig]?.label}
</Badge>
</div>
<div className="col-span-2">
<span className="font-medium">Description:</span>
<p className="mt-1 p-2 bg-gray-50 rounded">{selectedTicket.problemDescription}</p>
</div>
</div>
</div>
{/* Client Info */}
{(selectedTicket.clientName || selectedTicket.clientPhone) && ( {(selectedTicket.clientName || selectedTicket.clientPhone) && (
<div className="space-y-4"> <Card>
<h3 className="text-lg font-medium">Client</h3> <CardHeader>
<div className="grid grid-cols-2 gap-4 text-sm"> <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> <div>
<span className="font-medium">Nom:</span> <span className="font-medium">Nom:</span>
<p>{selectedTicket.clientName || "Non renseigné"}</p> <p>{selectedTicket.clientName || "Non renseigné"}</p>
@@ -875,14 +956,47 @@ export default function SavTickets() {
<span className="font-medium">Téléphone:</span> <span className="font-medium">Téléphone:</span>
<p>{selectedTicket.clientPhone || "Non renseigné"}</p> <p>{selectedTicket.clientPhone || "Non renseigné"}</p>
</div> </div>
</div> </CardContent>
</div> </Card>
)} )}
{/* Ticket Info */} {/* Section Problème */}
<div className="space-y-4"> <Card>
<h3 className="text-lg font-medium">Informations ticket</h3> <CardHeader>
<div className="grid grid-cols-2 gap-4 text-sm"> <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>
<span className="font-medium">Type:</span>
<p>{selectedTicket.problemType}</p>
</div>
<div>
<span className="font-medium">Priorité:</span>
<Badge className={priorityConfig[selectedTicket.priority as keyof typeof priorityConfig]?.color}>
{priorityConfig[selectedTicket.priority as keyof typeof priorityConfig]?.label}
</Badge>
</div>
</div>
<div>
<span className="font-medium">Description:</span>
<p className="mt-1 p-2 bg-gray-50 rounded">{selectedTicket.problemDescription}</p>
</div>
</CardContent>
</Card>
{/* Section Informations Ticket */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center">
<Wrench className="h-5 w-5 mr-2" />
Informations Ticket
</CardTitle>
</CardHeader>
<CardContent 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>