Merge pull request #201 from R0m1k3/1.1

1.1
This commit is contained in:
LogiFlow authored and GitHub committed 2025-08-18 09:35:57 +02:00
commit 8891b8960f
2 files changed
+3140 -200

No files matched your search

File diff suppressed because it is too large. Load diff
+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>