Merge pull request #444 from R0m1k3/1.1

1.1
This commit is contained in:
LogiFlow authored and GitHub committed 2025-09-11 13:55:11 +02:00
commit 2b2912978b
2 files changed
+129 -133

No files matched your search

+4
View File
@@ -15,6 +15,10 @@ run = ["npm", "run", "start"]
localPort = 5000 localPort = 5000
externalPort = 80 externalPort = 80
[[ports]]
localPort = 34895
externalPort = 3000
[[ports]] [[ports]]
localPort = 38451 localPort = 38451
externalPort = 3002 externalPort = 3002
+125 -133
View File
@@ -455,22 +455,29 @@ export default function SavTickets() {
}); });
return ( return (
<div className="p-4 sm:p-6 space-y-6"> <div className="p-3 sm:p-6 space-y-4 sm:space-y-6">
{/* Header */} {/* Header */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4"> <div className="bg-white border-b border-gray-200 p-4 sm:p-6 shadow-sm -m-3 sm:-m-6 mb-4 sm:mb-6">
<div> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900">Service Après-Vente</h1> <div>
<p className="text-gray-600 mt-1">Gestion des tickets SAV et suivi des réparations</p> <h1 className="text-xl sm:text-2xl font-semibold text-gray-900 flex items-center">
</div> <Wrench className="w-5 h-5 sm:w-6 sm:h-6 mr-2 sm:mr-3 text-blue-600" />
{canCreate && ( Service Après-Vente
<Dialog open={showCreateModal} onOpenChange={setShowCreateModal}> </h1>
<DialogTrigger asChild> <p className="text-gray-600 mt-1 text-sm sm:text-base">
<Button className="w-full sm:w-auto"> Gestion des tickets SAV et suivi des réparations
<Plus className="h-4 w-4 mr-2" /> </p>
Nouveau Ticket </div>
</Button> <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3">
</DialogTrigger> {canCreate && (
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto"> <Dialog open={showCreateModal} onOpenChange={setShowCreateModal}>
<DialogTrigger asChild>
<Button className="w-full sm:w-auto">
<Plus className="h-4 w-4 mr-2" />
Nouveau Ticket
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Créer un nouveau ticket SAV</DialogTitle> <DialogTitle>Créer un nouveau ticket SAV</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -689,6 +696,8 @@ export default function SavTickets() {
</Dialog> </Dialog>
)} )}
</div> </div>
</div>
</div>
{/* Stats Cards */} {/* Stats Cards */}
{statsData && ( {statsData && (
@@ -826,143 +835,126 @@ export default function SavTickets() {
<p className="text-gray-500">Aucun ticket trouvé</p> <p className="text-gray-500">Aucun ticket trouvé</p>
</div> </div>
) : ( ) : (
<div> <div className="p-6">
<table className="w-full"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<thead className="bg-gray-50 border-b"> {filteredTickets.map((ticket) => {
<tr> const StatusIcon = statusConfig[ticket.status as keyof typeof statusConfig]?.icon || Clock;
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> const priorityColor = priorityConfig[ticket.priority as keyof typeof priorityConfig]?.color || "bg-gray-100 text-gray-800";
Ticket const statusColor = statusConfig[ticket.status as keyof typeof statusConfig]?.color || "bg-gray-100 text-gray-800";
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> return (
Client <Card key={ticket.id} className={`hover:shadow-md transition-shadow cursor-pointer ${ticket.status === 'ferme' ? 'opacity-60' : ''}`}>
</th> <CardContent className="p-4">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> {/* Header avec numéro de ticket et statut */}
Produit <div className="flex items-start justify-between mb-3">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Problème
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Statut
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Priorité
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Fournisseur
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredTickets.map((ticket) => {
const StatusIcon = statusConfig[ticket.status as keyof typeof statusConfig]?.icon || Clock;
return (
<tr key={ticket.id} className={`hover:bg-gray-50 ${ticket.status === 'ferme' ? 'bg-gray-100 opacity-60' : ''}`}>
<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 mr-2"> <h5 className="font-medium text-gray-900 text-sm">{ticket.ticketNumber}</h5>
{ticket.ticketNumber}
</span>
{hasRecentComments(ticket) && ( {hasRecentComments(ticket) && (
<Badge className="bg-orange-100 text-orange-800 text-xs"> <Badge className="bg-orange-100 text-orange-800 text-xs ml-2">
<MessageCircle className="h-3 w-3 mr-1" /> <MessageCircle className="h-3 w-3 mr-1" />
Nouveau Nouveau
</Badge> </Badge>
)} )}
</div> </div>
</td>
<td className="px-6 py-4">
<div>
<div className="text-sm font-medium text-gray-900">
{ticket.clientName || 'N/A'}
</div>
{ticket.clientPhone && (
<div className="text-sm text-gray-500">
{ticket.clientPhone}
</div>
)}
</div>
</td>
<td className="px-6 py-4">
<div>
{ticket.productDesignation && (
<div className="text-sm font-medium text-gray-900">
{ticket.productDesignation}
</div>
)}
{ticket.productGencode && (
<div className="text-sm text-gray-500">
{ticket.productGencode}
</div>
)}
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900 max-w-xs truncate">
{ticket.problemDescription}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center"> <div className="flex items-center">
<StatusIcon className="h-4 w-4 mr-2" /> <StatusIcon className="h-3 w-3 mr-1" />
<Badge className={statusConfig[ticket.status as keyof typeof statusConfig]?.color}> <Badge className={`${statusColor} text-xs`}>
{statusConfig[ticket.status as keyof typeof statusConfig]?.label} {statusConfig[ticket.status as keyof typeof statusConfig]?.label}
</Badge> </Badge>
</div> </div>
</td> </div>
<td className="px-6 py-4 whitespace-nowrap">
<Badge className={priorityConfig[ticket.priority as keyof typeof priorityConfig]?.color}> {/* Client */}
<div className="mb-2">
<div className="text-sm font-medium text-gray-900">
{ticket.clientName || 'N/A'}
</div>
{ticket.clientPhone && (
<div className="text-xs text-gray-500">
📞 {ticket.clientPhone}
</div>
)}
</div>
{/* Produit */}
<div className="mb-2">
{ticket.productDesignation && (
<div className="text-xs text-gray-600 font-medium">
{ticket.productDesignation}
</div>
)}
{ticket.productGencode && (
<div className="text-xs text-gray-500">
📦 {ticket.productGencode}
</div>
)}
</div>
{/* Problème */}
{ticket.problemDescription && (
<div className="mb-3">
<p className="text-xs text-gray-600 line-clamp-2">
{ticket.problemDescription}
</p>
</div>
)}
{/* Priorité et fournisseur */}
<div className="flex items-center justify-between mb-3">
<Badge className={`${priorityColor} text-xs`}>
{priorityConfig[ticket.priority as keyof typeof priorityConfig]?.label} {priorityConfig[ticket.priority as keyof typeof priorityConfig]?.label}
</Badge> </Badge>
</td>
<td className="px-6 py-4 whitespace-nowrap"> <div className="flex items-center text-xs text-gray-500">
<div className="flex items-center"> <Building className="h-3 w-3 mr-1" />
<Building className="h-4 w-4 text-gray-400 mr-2" /> {ticket.supplier?.name}
<span className="text-sm text-gray-900">
{ticket.supplier?.name}
</span>
</div> </div>
</td> </div>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<div className="flex items-center"> {/* Date */}
<Calendar className="h-4 w-4 mr-1" /> <div className="text-xs text-gray-500 mb-3 flex items-center">
{safeFormat(ticket.createdAt, 'dd/MM/yyyy')} <Calendar className="h-3 w-3 mr-1" />
</div> {safeFormat(ticket.createdAt, 'dd/MM/yyyy')}
</td> </div>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex justify-end space-x-2"> {/* Actions */}
<div className="flex items-center gap-1">
<Button
variant="outline"
size="sm"
onClick={() => handleViewTicket(ticket)}
title="Suivi du ticket"
className="text-xs h-7"
>
<ClipboardList className="h-3 w-3" />
</Button>
{canModify && (
<Button <Button
variant="ghost" variant="outline"
size="sm" size="sm"
onClick={() => handleViewTicket(ticket)} onClick={() => handleEditTicket(ticket)}
title="Suivi du ticket" className="text-xs h-7"
> >
<ClipboardList className="h-4 w-4" /> <Edit className="h-3 w-3" />
</Button> </Button>
{canModify && ( )}
<Button variant="ghost" size="sm" onClick={() => handleEditTicket(ticket)}> {canDelete && (
<Edit className="h-4 w-4" /> <Button
</Button> variant="outline"
)} size="sm"
{canDelete && ( className="text-red-600 hover:text-red-700 text-xs h-7"
<Button variant="ghost" size="sm" className="text-red-600 hover:text-red-900" onClick={() => handleDeleteTicket(ticket)}> onClick={() => handleDeleteTicket(ticket)}
<Trash2 className="h-4 w-4" /> >
</Button> <Trash2 className="h-3 w-3" />
)} </Button>
</div> )}
</td> </div>
</tr> </CardContent>
); </Card>
})} );
</tbody> })}
</table> </div>
</div> </div>
)} )}
</CardContent> </CardContent>