mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-21 22:18:17 +02:00
- Routes en React.lazy (sauf connexion et accueil) avec un Suspense dans la zone de contenu ; recharts n'est plus préchargé. JS initial : 1,72 Mo -> 625 Ko (183 Ko gzip). Rechargement automatique unique si un ancien fichier JS a disparu après un déploiement, et ErrorBoundary par page pour garder le menu affiché en cas d'erreur. - Authentification unifiée sur le cache React Query ['/api/user'] : un seul GET /api/user pour toute l'application (au lieu de 4 à 5 par page), plus de rechargement complet après la connexion. - Pas de nouvel essai sur les erreurs 4xx ; fournisseurs et magasins en cache 5 minutes ; valeur du StoreContext mémoïsée et redimensionnement qui ne re-rend que lorsque le palier d'écran change. - Pages : requêtes inutilisées ou en double supprimées, appels lancés seulement une fois l'utilisateur connu, invalidations ciblées au lieu de rechargements complets, filtres et tris mémoïsés, anciennes données gardées (et estompées) pendant un changement de filtre ou de mois, recherche DLC mobile temporisée, vérifications de factures à concurrence bornée, navigation interne sans rechargement de la page. - index.html : script de bannière Replit retiré, lang="fr". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MsDJjQrAggcJwbbBtKhgyb
573 lines
23 KiB
TypeScript
573 lines
23 KiB
TypeScript
import { useMemo, 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 { Pagination, usePagination } from "@/components/ui/pagination";
|
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
|
import { useStore } from "@/contexts/StoreContext";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { apiRequest } from "@/lib/queryClient";
|
|
import { isUnauthorizedError } from "@/lib/authUtils";
|
|
import { usePermissions } from "@shared/permissions";
|
|
import {
|
|
Plus,
|
|
Search,
|
|
Filter,
|
|
Truck,
|
|
Calendar,
|
|
Eye,
|
|
Edit,
|
|
Trash2,
|
|
Building,
|
|
User,
|
|
Check,
|
|
Package,
|
|
AlertTriangle,
|
|
CheckCircle
|
|
} from "lucide-react";
|
|
import { safeFormat } from "@/lib/dateUtils";
|
|
import CreateDeliveryModal from "@/components/modals/CreateDeliveryModal";
|
|
import EditDeliveryModal from "@/components/modals/EditDeliveryModal";
|
|
import OrderDetailModal from "@/components/modals/OrderDetailModal";
|
|
import ValidateDeliveryModal from "@/components/modals/ValidateDeliveryModal";
|
|
import ConfirmDeleteModal from "@/components/modals/ConfirmDeleteModal";
|
|
import type { DeliveryWithRelations } from "@shared/schema";
|
|
|
|
// Référence stable pendant le chargement (pour la mémoïsation du filtrage)
|
|
const NO_DELIVERIES: DeliveryWithRelations[] = [];
|
|
|
|
export default function Deliveries() {
|
|
const { user } = useAuthUnified();
|
|
const { selectedStoreId } = useStore();
|
|
const { toast } = useToast();
|
|
const queryClient = useQueryClient();
|
|
const permissions = usePermissions(user?.role);
|
|
|
|
// Redirection pour les employés
|
|
if (user?.role === 'employee') {
|
|
return (
|
|
<div className="p-4 sm:p-6">
|
|
<div className="bg-orange-50 border-l-4 border-orange-400 p-4 rounded-r-lg">
|
|
<div className="flex">
|
|
<div className="flex-shrink-0">
|
|
<Truck className="h-5 w-5 text-orange-400" />
|
|
</div>
|
|
<div className="ml-3">
|
|
<p className="text-sm text-orange-700">
|
|
<strong>Accès restreint</strong><br />
|
|
Seuls les managers et administrateurs peuvent accéder à la page des livraisons.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [statusFilter, setStatusFilter] = useState("all");
|
|
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
const [showEditModal, setShowEditModal] = useState(false);
|
|
const [showDetailModal, setShowDetailModal] = useState(false);
|
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
const [showValidateModal, setShowValidateModal] = useState(false);
|
|
const [selectedDelivery, setSelectedDelivery] = useState<DeliveryWithRelations | null>(null);
|
|
const [deliveryToDelete, setDeliveryToDelete] = useState<DeliveryWithRelations | null>(null);
|
|
const [deliveryToValidate, setDeliveryToValidate] = useState<DeliveryWithRelations | null>(null);
|
|
|
|
// Construire l'URL pour l'historique complet sans filtrage par date
|
|
// CRITICAL FIX: Appliquer le filtrage par storeId pour TOUS les rôles, pas seulement admin
|
|
const deliveriesUrl = `/api/deliveries${selectedStoreId ? `?storeId=${selectedStoreId}` : ''}`;
|
|
|
|
const { data: deliveriesData = NO_DELIVERIES, isLoading } = useQuery<DeliveryWithRelations[]>({
|
|
queryKey: ['/api/deliveries', selectedStoreId, user?.role],
|
|
queryFn: async () => {
|
|
const response = await fetch(deliveriesUrl, { credentials: 'include' });
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch deliveries');
|
|
}
|
|
const data = await response.json();
|
|
return Array.isArray(data) ? data : [];
|
|
},
|
|
});
|
|
|
|
// Production Bug Fix: Ensure array safety for all data operations
|
|
const deliveries = Array.isArray(deliveriesData) ? deliveriesData : NO_DELIVERIES;
|
|
|
|
// Clé construite sur l'URL (tableau de bord admin avec magasin sélectionné).
|
|
// Sans magasin, elle vaut ['/api/deliveries'], déjà invalidée : la réinvalider
|
|
// relancerait une seconde fois le chargement complet des livraisons.
|
|
const invalidateDeliveriesUrlKey = () => {
|
|
if (selectedStoreId) {
|
|
queryClient.invalidateQueries({ queryKey: [deliveriesUrl] });
|
|
}
|
|
};
|
|
|
|
|
|
const markControlValidatedMutation = useMutation({
|
|
mutationFn: async (id: number) => {
|
|
await apiRequest(`/api/deliveries/${id}/control`, "PUT");
|
|
},
|
|
onSuccess: () => {
|
|
toast({
|
|
title: "Succès",
|
|
description: "Contrôle validé avec succès",
|
|
});
|
|
queryClient.invalidateQueries({ queryKey: ['/api/deliveries'] });
|
|
invalidateDeliveriesUrlKey();
|
|
},
|
|
onError: (error: any) => {
|
|
if (isUnauthorizedError(error)) {
|
|
return;
|
|
}
|
|
toast({
|
|
title: "Erreur",
|
|
description: "Impossible de valider le contrôle",
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: async (id: number) => {
|
|
await apiRequest(`/api/deliveries/${id}`, "DELETE");
|
|
},
|
|
onSuccess: () => {
|
|
toast({
|
|
title: "Succès",
|
|
description: "Livraison supprimée avec succès",
|
|
});
|
|
// Invalider tous les caches liés aux livraisons
|
|
queryClient.invalidateQueries({ queryKey: ['/api/deliveries'] });
|
|
invalidateDeliveriesUrlKey();
|
|
queryClient.invalidateQueries({ queryKey: ['/api/stats/monthly'] });
|
|
// L'échéancier (cache de 5 min) liste les livraisons facturées
|
|
queryClient.invalidateQueries({ queryKey: ['/api/payment-schedule'] });
|
|
},
|
|
onError: (error) => {
|
|
if (isUnauthorizedError(error)) {
|
|
toast({
|
|
title: "Non autorisé",
|
|
description: "Vous êtes déconnecté. Reconnexion...",
|
|
variant: "destructive",
|
|
});
|
|
setTimeout(() => {
|
|
window.location.href = "/api/login";
|
|
}, 500);
|
|
return;
|
|
}
|
|
toast({
|
|
title: "Erreur",
|
|
description: "Impossible de supprimer la livraison",
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
// Filtrage recalculé seulement quand la liste, la recherche ou le statut changent
|
|
const filteredDeliveries = useMemo(() => {
|
|
const search = searchTerm.toLowerCase();
|
|
return deliveries.filter(delivery => {
|
|
const matchesSearch = delivery.supplier?.name.toLowerCase().includes(search) ||
|
|
delivery.group?.name.toLowerCase().includes(search) ||
|
|
delivery.notes?.toLowerCase().includes(search);
|
|
|
|
const matchesStatus = statusFilter === "all" || delivery.status === statusFilter;
|
|
|
|
return matchesSearch && matchesStatus;
|
|
});
|
|
}, [deliveries, searchTerm, statusFilter]);
|
|
|
|
// Pagination
|
|
const {
|
|
currentPage,
|
|
setCurrentPage,
|
|
itemsPerPage,
|
|
setItemsPerPage,
|
|
totalPages,
|
|
paginatedData: paginatedDeliveries,
|
|
totalItems
|
|
} = usePagination(filteredDeliveries, 20);
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
switch (status) {
|
|
case 'pending':
|
|
return <Badge variant="secondary">En attente</Badge>;
|
|
case 'planned':
|
|
return <Badge className="bg-blue-100 text-blue-800">Planifié</Badge>;
|
|
case 'delivered':
|
|
return <Badge className="bg-green-100 text-green-800">Livré</Badge>;
|
|
default:
|
|
return <Badge variant="outline">{status}</Badge>;
|
|
}
|
|
};
|
|
|
|
const getControlBadge = (delivery: DeliveryWithRelations) => {
|
|
if (!delivery.supplier?.requiresControl) {
|
|
return null;
|
|
}
|
|
|
|
if (delivery.controlValidated) {
|
|
return (
|
|
<Badge className="bg-green-50 text-green-700 border-green-200 flex items-center gap-1">
|
|
<CheckCircle className="w-3 h-3" />
|
|
Contrôle validé
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Badge className="bg-orange-50 text-orange-700 border-orange-200 flex items-center gap-1">
|
|
<AlertTriangle className="w-3 h-3" />
|
|
Contrôle à faire
|
|
</Badge>
|
|
);
|
|
};
|
|
|
|
const formatQuantity = (quantity: number, unit: string) => {
|
|
return `${quantity} ${unit === 'palettes' ? 'P' : 'C'}`;
|
|
};
|
|
|
|
const handleViewDelivery = (delivery: DeliveryWithRelations) => {
|
|
setSelectedDelivery({ ...delivery, type: 'delivery' } as any);
|
|
setShowDetailModal(true);
|
|
};
|
|
|
|
const handleEditDelivery = (delivery: DeliveryWithRelations) => {
|
|
setSelectedDelivery(delivery);
|
|
setShowEditModal(true);
|
|
};
|
|
|
|
const handleValidateDelivery = (delivery: DeliveryWithRelations) => {
|
|
setDeliveryToValidate(delivery);
|
|
setShowValidateModal(true);
|
|
};
|
|
|
|
const handleValidateModalClose = () => {
|
|
setShowValidateModal(false);
|
|
setDeliveryToValidate(null);
|
|
};
|
|
|
|
const handleDeleteDelivery = (delivery: DeliveryWithRelations) => {
|
|
setDeliveryToDelete(delivery);
|
|
setShowDeleteModal(true);
|
|
};
|
|
|
|
const handleMarkControlValidated = (deliveryId: number) => {
|
|
markControlValidatedMutation.mutate(deliveryId);
|
|
};
|
|
|
|
const confirmDeleteDelivery = () => {
|
|
if (deliveryToDelete) {
|
|
deleteMutation.mutate(deliveryToDelete.id);
|
|
setShowDeleteModal(false);
|
|
setDeliveryToDelete(null);
|
|
}
|
|
};
|
|
|
|
const canCreate = permissions.canCreate('deliveries');
|
|
const canEdit = permissions.canEdit('deliveries');
|
|
const canDelete = permissions.canDelete('deliveries');
|
|
const canValidate = permissions.canValidate('deliveries');
|
|
|
|
return (
|
|
<div className="p-6 space-y-6">
|
|
{/* Header */}
|
|
<div className="bg-white border-b border-gray-200 p-6 shadow-sm -m-6 mb-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-2xl font-semibold text-gray-900 flex items-center">
|
|
<Truck className="w-6 h-6 mr-3 text-green-600" />
|
|
Gestion des Livraisons
|
|
</h2>
|
|
<p className="text-gray-600 mt-1">
|
|
{filteredDeliveries.length} livraison{filteredDeliveries.length !== 1 ? 's' : ''}
|
|
</p>
|
|
</div>
|
|
{canCreate && (
|
|
<Button
|
|
onClick={() => setShowCreateModal(true)}
|
|
className="bg-green-600 hover:bg-green-700 text-white shadow-md"
|
|
>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Nouvelle Livraison
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="bg-gray-50 border border-gray-200 p-4 rounded-lg">
|
|
<div className="flex flex-wrap items-center gap-4">
|
|
<div className="flex-1 min-w-64">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
|
<Input
|
|
placeholder="Rechercher par fournisseur, groupe ou commentaire..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10 border border-gray-300 shadow-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
<SelectTrigger className="w-48 border border-gray-300 shadow-sm">
|
|
<Filter className="w-4 h-4 mr-2" />
|
|
<SelectValue placeholder="Filtrer par statut" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Tous les statuts</SelectItem>
|
|
<SelectItem value="pending">En attente</SelectItem>
|
|
<SelectItem value="planned">Planifié</SelectItem>
|
|
<SelectItem value="delivered">Livré</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{/* Deliveries List */}
|
|
<div className="flex-1 overflow-hidden flex flex-col">
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-secondary"></div>
|
|
</div>
|
|
) : filteredDeliveries.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center h-64 text-gray-500">
|
|
<Truck className="w-16 h-16 mb-4 text-gray-300" />
|
|
<h3 className="text-lg font-medium mb-2">Aucune livraison trouvée</h3>
|
|
<p className="text-center max-w-md">
|
|
{searchTerm || statusFilter !== "all"
|
|
? "Aucune livraison ne correspond à vos critères de recherche."
|
|
: "Vous n'avez pas encore de livraisons. Créez votre première livraison pour commencer."}
|
|
</p>
|
|
{canCreate && !searchTerm && statusFilter === "all" && (
|
|
<Button
|
|
onClick={() => setShowCreateModal(true)}
|
|
className="mt-4 bg-secondary hover:bg-green-700 text-white"
|
|
>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Créer une livraison
|
|
</Button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="flex-1 overflow-y-auto p-6">
|
|
<div className="bg-white border border-gray-200 shadow-lg overflow-hidden">
|
|
<div>
|
|
<table className="w-full">
|
|
<thead className="bg-gray-50 border-b border-gray-200">
|
|
<tr>
|
|
<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">
|
|
Groupe
|
|
</th>
|
|
<th className="px-6 py-4 text-left text-sm font-black text-gray-800 uppercase tracking-wider">
|
|
Date prévue
|
|
</th>
|
|
<th className="px-6 py-4 text-left text-sm font-black text-gray-800 uppercase tracking-wider">
|
|
Quantité
|
|
</th>
|
|
<th className="px-6 py-4 text-left text-sm font-black text-gray-800 uppercase tracking-wider">
|
|
Statut
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Commande liée
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Créé par
|
|
</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">
|
|
{paginatedDeliveries.map((delivery) => (
|
|
<tr key={delivery.id} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center">
|
|
<Building className="w-5 h-5 text-gray-400 mr-2" />
|
|
<div>
|
|
<div className="text-sm font-medium text-gray-900">
|
|
{delivery.supplier?.name}
|
|
</div>
|
|
<div className="text-sm text-gray-500">
|
|
#{delivery.id}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center">
|
|
<div
|
|
className="w-3 h-3 rounded-full mr-2"
|
|
style={{ backgroundColor: delivery.group?.color }}
|
|
/>
|
|
<span className="text-sm text-gray-900">{delivery.group?.name}</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
<div className="flex items-center">
|
|
<Calendar className="w-4 h-4 text-gray-400 mr-2" />
|
|
{safeFormat(delivery.scheduledDate, 'dd MMM yyyy')}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<Badge variant="outline" className="text-sm">
|
|
{formatQuantity(delivery.quantity, delivery.unit)}
|
|
</Badge>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex flex-col gap-1">
|
|
{getStatusBadge(delivery.status)}
|
|
{getControlBadge(delivery)}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{delivery.order ? (
|
|
<div className="flex items-center">
|
|
<Package className="w-4 h-4 text-primary mr-2" />
|
|
<span>#{delivery.order.id}</span>
|
|
</div>
|
|
) : (
|
|
<span className="text-gray-400">Aucune</span>
|
|
)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
<div className="flex items-center">
|
|
<User className="w-4 h-4 text-gray-400 mr-2" />
|
|
{delivery.creator?.firstName} {delivery.creator?.lastName}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<div className="flex items-center justify-end space-x-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleViewDelivery(delivery)}
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
</Button>
|
|
{canEdit && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleEditDelivery(delivery)}
|
|
>
|
|
<Edit className="w-4 h-4" />
|
|
</Button>
|
|
)}
|
|
{canValidate && delivery.status !== 'delivered' && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleValidateDelivery(delivery)}
|
|
className="text-green-600 hover:text-green-700"
|
|
disabled={false}
|
|
>
|
|
<Check className="w-4 h-4 border border-gray-300 rounded p-0.5" />
|
|
</Button>
|
|
)}
|
|
{delivery.supplier?.requiresControl && !delivery.controlValidated && delivery.status === 'delivered' && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleMarkControlValidated(delivery.id)}
|
|
className="text-orange-600 hover:text-orange-700"
|
|
data-testid={`button-validate-control-${delivery.id}`}
|
|
title="Marquer le contrôle comme effectué"
|
|
>
|
|
<CheckCircle className="w-4 h-4" />
|
|
</Button>
|
|
)}
|
|
{canDelete && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleDeleteDelivery(delivery)}
|
|
className="text-red-600 hover:text-red-700"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Pagination */}
|
|
<Pagination
|
|
currentPage={currentPage}
|
|
totalPages={totalPages}
|
|
totalItems={totalItems}
|
|
itemsPerPage={itemsPerPage}
|
|
onPageChange={setCurrentPage}
|
|
onItemsPerPageChange={setItemsPerPage}
|
|
className="mt-4 p-4 border-t border-gray-200"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Modals */}
|
|
{showCreateModal && (
|
|
<CreateDeliveryModal
|
|
isOpen={showCreateModal}
|
|
onClose={() => setShowCreateModal(false)}
|
|
selectedDate={null}
|
|
/>
|
|
)}
|
|
|
|
{showEditModal && selectedDelivery && (
|
|
<EditDeliveryModal
|
|
isOpen={showEditModal}
|
|
onClose={() => setShowEditModal(false)}
|
|
delivery={selectedDelivery}
|
|
/>
|
|
)}
|
|
|
|
{showDetailModal && selectedDelivery && (
|
|
<OrderDetailModal
|
|
isOpen={showDetailModal}
|
|
onClose={() => setShowDetailModal(false)}
|
|
item={selectedDelivery}
|
|
/>
|
|
)}
|
|
|
|
{showValidateModal && deliveryToValidate && (
|
|
<ValidateDeliveryModal
|
|
isOpen={showValidateModal}
|
|
onClose={handleValidateModalClose}
|
|
delivery={deliveryToValidate}
|
|
/>
|
|
)}
|
|
|
|
{/* Modal de confirmation de suppression */}
|
|
<ConfirmDeleteModal
|
|
isOpen={showDeleteModal}
|
|
onClose={() => {
|
|
setShowDeleteModal(false);
|
|
setDeliveryToDelete(null);
|
|
}}
|
|
onConfirm={confirmDeleteDelivery}
|
|
title="Supprimer la livraison"
|
|
description="Êtes-vous sûr de vouloir supprimer cette livraison ?"
|
|
itemName={deliveryToDelete ? `${deliveryToDelete.supplier?.name} - ${safeFormat(deliveryToDelete.scheduledDate, 'dd/MM/yyyy')}` : undefined}
|
|
isLoading={deleteMutation.isPending}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|