mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-17 20:27:10 +02:00
Conditionally disable verbose logging in production to resolve latency issues caused by excessive console output in API requests and responses. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7c468936-2a88-4686-ac0a-84921a45222d Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/7c468936-2a88-4686-ac0a-84921a45222d/B7k6m01
867 lines
34 KiB
TypeScript
867 lines
34 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Pagination, usePagination } from "@/components/ui/pagination";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { AlertTriangle, Plus, Eye, Edit, Trash2, CheckCircle, Package, Clock, AlertCircle, Filter, Download, FileText } from "lucide-react";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { z } from "zod";
|
|
import { format } from "date-fns";
|
|
import { fr } from "date-fns/locale";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
|
import { useStore } from "@/components/Layout";
|
|
import { apiRequest } from "@/lib/queryClient";
|
|
import type { DlcProductWithRelations, InsertDlcProduct } from "@shared/schema";
|
|
|
|
const dlcFormSchema = z.object({
|
|
productName: z.string().min(1, "Le nom du produit est obligatoire"),
|
|
gencode: z.string().optional(),
|
|
dlcDate: z.string().min(1, "La date d'expiration est obligatoire"),
|
|
dateType: z.enum(["dlc", "ddm", "dluo"], { required_error: "Le type de date est obligatoire" }),
|
|
supplierId: z.number().min(1, "Le fournisseur est obligatoire"),
|
|
status: z.enum(["en_cours", "expires_soon", "expires", "valides"]).default("en_cours"),
|
|
notes: z.string().optional(),
|
|
});
|
|
|
|
type DlcFormData = z.infer<typeof dlcFormSchema>;
|
|
|
|
export default function DlcPage() {
|
|
const { user, isLoading: authLoading } = useAuthUnified();
|
|
const { toast } = useToast();
|
|
const queryClient = useQueryClient();
|
|
const { selectedStoreId } = useStore();
|
|
const [statusFilter, setStatusFilter] = useState<string>("all");
|
|
const [supplierFilter, setSupplierFilter] = useState<string>("all");
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
const [editingProduct, setEditingProduct] = useState<DlcProductWithRelations | null>(null);
|
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
const [productToDelete, setProductToDelete] = useState<number | null>(null);
|
|
|
|
// Fetch stores/groups
|
|
const { data: stores = [] } = useQuery({
|
|
queryKey: ["/api/groups"],
|
|
enabled: !authLoading,
|
|
});
|
|
|
|
|
|
|
|
// Fetch suppliers with DLC enabled only
|
|
const { data: suppliers = [] } = useQuery({
|
|
queryKey: ["/api/suppliers", "dlc"],
|
|
queryFn: () => apiRequest("/api/suppliers?dlc=true"),
|
|
enabled: !authLoading,
|
|
});
|
|
|
|
// Fetch DLC products
|
|
const { data: dlcProducts = [], isLoading: productsLoading } = useQuery({
|
|
queryKey: ["/api/dlc-products", selectedStoreId, statusFilter, supplierFilter],
|
|
queryFn: () => {
|
|
const params = new URLSearchParams();
|
|
if (selectedStoreId && user?.role === 'admin') params.append("storeId", selectedStoreId.toString());
|
|
if (statusFilter && statusFilter !== "all") params.append("status", statusFilter);
|
|
if (supplierFilter && supplierFilter !== "all") params.append("supplierId", supplierFilter);
|
|
|
|
if (import.meta.env.DEV) {
|
|
console.log("🔍 Requête DLC products:", {
|
|
url: `/api/dlc-products?${params.toString()}`,
|
|
user: user?.role,
|
|
selectedStoreId,
|
|
userGroups: user?.userGroups?.map(ug => ({ groupId: ug.groupId, groupName: ug.group?.name }))
|
|
});
|
|
}
|
|
|
|
return apiRequest(`/api/dlc-products?${params.toString()}`);
|
|
},
|
|
enabled: !authLoading,
|
|
onSuccess: (data) => {
|
|
if (import.meta.env.DEV) {
|
|
console.log("📋 DLC products reçus:", {
|
|
total: data.length,
|
|
sample: data.slice(0, 3).map(d => ({ id: d.id, productName: d.productName, groupId: d.groupId }))
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
// Fetch DLC stats
|
|
const { data: stats = { active: 0, expiringSoon: 0, expired: 0 } } = useQuery({
|
|
queryKey: ["/api/dlc-products/stats", selectedStoreId],
|
|
queryFn: () => {
|
|
const params = new URLSearchParams();
|
|
if (selectedStoreId && user?.role === 'admin') params.append("storeId", selectedStoreId.toString());
|
|
return apiRequest(`/api/dlc-products/stats?${params.toString()}`);
|
|
},
|
|
enabled: !authLoading,
|
|
});
|
|
|
|
// Form setup
|
|
const form = useForm<DlcFormData>({
|
|
resolver: zodResolver(dlcFormSchema),
|
|
defaultValues: {
|
|
productName: "",
|
|
gencode: "",
|
|
dateType: "dlc",
|
|
status: "en_cours",
|
|
notes: "",
|
|
},
|
|
});
|
|
|
|
// Create mutation
|
|
const createMutation = useMutation({
|
|
mutationFn: (data: InsertDlcProduct) => apiRequest("/api/dlc-products", "POST", data),
|
|
onSuccess: () => {
|
|
// Invalidate all DLC queries with any combination of parameters
|
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"], exact: false });
|
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"], exact: false });
|
|
if (import.meta.env.DEV) {
|
|
console.log("🔄 Cache DLC invalidé après création");
|
|
}
|
|
toast({ title: "Produit DLC créé avec succès" });
|
|
setIsDialogOpen(false);
|
|
form.reset();
|
|
setEditingProduct(null);
|
|
},
|
|
onError: (error: any) => {
|
|
toast({
|
|
title: "Erreur lors de la création",
|
|
description: error.message || "Une erreur est survenue",
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
// Update mutation
|
|
const updateMutation = useMutation({
|
|
mutationFn: ({ id, data }: { id: number; data: Partial<InsertDlcProduct> }) =>
|
|
apiRequest(`/api/dlc-products/${id}`, "PUT", data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"], exact: false });
|
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"], exact: false });
|
|
if (import.meta.env.DEV) {
|
|
console.log("🔄 Cache DLC invalidé après mise à jour");
|
|
}
|
|
toast({ title: "Produit DLC mis à jour avec succès" });
|
|
setIsDialogOpen(false);
|
|
form.reset();
|
|
setEditingProduct(null);
|
|
},
|
|
onError: (error: any) => {
|
|
toast({
|
|
title: "Erreur lors de la mise à jour",
|
|
description: error.message || "Une erreur est survenue",
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
// Validate mutation
|
|
const validateMutation = useMutation({
|
|
mutationFn: (id: number) => apiRequest(`/api/dlc-products/${id}/validate`, "PUT"),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"], exact: false });
|
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"], exact: false });
|
|
if (import.meta.env.DEV) {
|
|
console.log("🔄 Cache DLC invalidé après validation");
|
|
}
|
|
toast({ title: "Produit validé avec succès" });
|
|
},
|
|
onError: (error: any) => {
|
|
toast({
|
|
title: "Erreur lors de la validation",
|
|
description: error.message || "Une erreur est survenue",
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
// Delete mutation
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (id: number) => apiRequest(`/api/dlc-products/${id}`, "DELETE"),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"], exact: false });
|
|
queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"], exact: false });
|
|
console.log("🔄 Cache DLC invalidé après suppression");
|
|
toast({ title: "Produit supprimé avec succès" });
|
|
},
|
|
onError: (error: any) => {
|
|
toast({
|
|
title: "Erreur lors de la suppression",
|
|
description: error.message || "Une erreur est survenue",
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
const onSubmit = (data: DlcFormData) => {
|
|
// Calculer la date d'expiration et le seuil d'alerte (15 jours avant)
|
|
const dlcDate = new Date(data.dlcDate);
|
|
|
|
// Déterminer le groupId correctement selon le rôle utilisateur
|
|
let groupId;
|
|
|
|
// FORCE L'UTILISATION DU GROUPE ASSIGNÉ EN PREMIER
|
|
if (user?.userGroups?.[0]?.groupId) {
|
|
// Utilisateur avec groupe assigné : TOUJOURS utiliser ce groupe
|
|
groupId = user.userGroups[0].groupId;
|
|
console.log("🎯 DLC Frontend: Using user's assigned group (PRIORITY):", groupId);
|
|
} else if (user?.role === 'admin' && selectedStoreId) {
|
|
// Admin avec magasin sélectionné ET pas de groupe assigné
|
|
groupId = selectedStoreId;
|
|
console.log("🎯 DLC Frontend: Using admin selected store:", groupId);
|
|
} else if (user?.role === 'admin' && stores?.[0]?.id) {
|
|
// Admin sans sélection : premier magasin disponible
|
|
groupId = stores[0].id;
|
|
console.log("🎯 DLC Frontend: Using first available store for admin:", groupId);
|
|
} else {
|
|
// Fallback d'urgence
|
|
groupId = 1;
|
|
console.log("🚨 DLC Frontend: Using emergency fallback groupId:", groupId);
|
|
}
|
|
|
|
console.log("🏪 DLC GroupId Selection DEBUG:", {
|
|
userRole: user?.role,
|
|
selectedStoreId,
|
|
userGroups: user?.userGroups?.map(ug => ({groupId: ug.groupId, groupName: ug.group?.name})),
|
|
availableStores: stores.map(s => ({id: s.id, name: s.name})),
|
|
userGroupsRaw: user?.userGroups,
|
|
firstUserGroup: user?.userGroups?.[0],
|
|
finalGroupId: groupId,
|
|
logicPath: !groupId ? 'need-fallback' : 'already-set'
|
|
});
|
|
|
|
const dlcData: InsertDlcProduct = {
|
|
...data,
|
|
dlcDate,
|
|
quantity: 1, // Valeur par défaut
|
|
unit: "unité", // Valeur par défaut
|
|
location: "Magasin", // Valeur par défaut
|
|
alertThreshold: 15, // Toujours 15 jours
|
|
groupId,
|
|
};
|
|
|
|
if (editingProduct) {
|
|
updateMutation.mutate({ id: editingProduct.id, data: dlcData });
|
|
} else {
|
|
createMutation.mutate(dlcData);
|
|
}
|
|
};
|
|
|
|
const handleEdit = (product: DlcProductWithRelations) => {
|
|
setEditingProduct(product);
|
|
form.reset({
|
|
productName: product.productName,
|
|
gencode: product.gencode || "",
|
|
dlcDate: product.dlcDate ? format(new Date(product.dlcDate), "yyyy-MM-dd") : "",
|
|
dateType: product.dateType as "dlc" | "ddm" | "dluo",
|
|
supplierId: product.supplierId,
|
|
status: product.status as "en_cours" | "expires_soon" | "expires" | "valides",
|
|
notes: product.notes || "",
|
|
});
|
|
setIsDialogOpen(true);
|
|
};
|
|
|
|
const handleDelete = (id: number) => {
|
|
setProductToDelete(id);
|
|
setDeleteDialogOpen(true);
|
|
};
|
|
|
|
const confirmDelete = () => {
|
|
if (productToDelete) {
|
|
deleteMutation.mutate(productToDelete);
|
|
setDeleteDialogOpen(false);
|
|
setProductToDelete(null);
|
|
}
|
|
};
|
|
|
|
const handleValidate = (id: number) => {
|
|
validateMutation.mutate(id);
|
|
};
|
|
|
|
// Fonction pour déterminer si un produit doit afficher le bouton de validation
|
|
const shouldShowValidateButton = (product: DlcProductWithRelations) => {
|
|
const today = new Date();
|
|
const expiry = new Date(product.dlcDate || new Date());
|
|
const daysUntilExpiry = Math.ceil((expiry.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
|
// Afficher le bouton si le produit expire dans 15 jours ou moins, ou est déjà expiré
|
|
return (daysUntilExpiry <= 15 && product.status !== "valide");
|
|
};
|
|
|
|
const getStatusBadge = (status: string, dlcDate: string | null) => {
|
|
if (!dlcDate) return <Badge variant="outline">Non défini</Badge>;
|
|
|
|
// LOGIQUE SIMPLIFIÉE ET COHÉRENTE :
|
|
// 1. Si le statut en base est "valides", afficher "Validé" (peu importe la date)
|
|
// 2. Sinon, calculer selon la date d'expiration
|
|
|
|
if (status === "valides") {
|
|
return <Badge variant="outline" className="bg-gray-100 text-gray-800">Validé</Badge>;
|
|
}
|
|
|
|
// Pour tous les autres statuts, calculer selon la date
|
|
const today = new Date();
|
|
const expiry = new Date(dlcDate);
|
|
const daysUntilExpiry = Math.ceil((expiry.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
|
if (daysUntilExpiry < 0) {
|
|
return <Badge variant="destructive">Expiré</Badge>;
|
|
} else if (daysUntilExpiry <= 15) {
|
|
return <Badge variant="secondary" className="bg-orange-100 text-orange-800">Expire bientôt</Badge>;
|
|
} else {
|
|
return <Badge variant="default" className="bg-green-100 text-green-800">Actif</Badge>;
|
|
}
|
|
};
|
|
|
|
const printExpiringSoon = () => {
|
|
const expiringSoon = filteredProducts.filter(product => {
|
|
const today = new Date();
|
|
const expiry = new Date(product.dlcDate || new Date());
|
|
const diffTime = expiry.getTime() - today.getTime();
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
return diffDays > 0 && diffDays <= 15;
|
|
});
|
|
|
|
const printContent = `
|
|
<html>
|
|
<head>
|
|
<title>Produits expirant bientôt</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
|
th { background-color: #f2f2f2; }
|
|
h1 { color: #ff6b35; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Produits expirant dans les 15 prochains jours</h1>
|
|
<p>Date d'impression: ${format(new Date(), "dd/MM/yyyy HH:mm")}</p>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Produit</th>
|
|
<th>Code EAN13</th>
|
|
<th>Date d'expiration</th>
|
|
<th>Type</th>
|
|
<th>Fournisseur</th>
|
|
<th>Jours restants</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${expiringSoon.map(product => {
|
|
const today = new Date();
|
|
const expiry = new Date(product.dlcDate || new Date());
|
|
const diffDays = Math.ceil((expiry.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
|
return `
|
|
<tr>
|
|
<td>${product.productName}</td>
|
|
<td>${product.gencode || '-'}</td>
|
|
<td>${product.dlcDate ? format(new Date(product.dlcDate), "dd/MM/yyyy") : 'N/A'}</td>
|
|
<td>${product.dateType.toUpperCase()}</td>
|
|
<td>${product.supplier?.name || 'N/A'}</td>
|
|
<td>${diffDays}</td>
|
|
</tr>
|
|
`;
|
|
}).join('')}
|
|
</tbody>
|
|
</table>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
const printWindow = window.open('', '_blank');
|
|
if (printWindow) {
|
|
printWindow.document.write(printContent);
|
|
printWindow.document.close();
|
|
printWindow.print();
|
|
}
|
|
};
|
|
|
|
const printExpired = () => {
|
|
const expired = filteredProducts.filter(product => {
|
|
const today = new Date();
|
|
const expiry = new Date(product.dlcDate || new Date());
|
|
return expiry < today;
|
|
});
|
|
|
|
const printContent = `
|
|
<html>
|
|
<head>
|
|
<title>Produits expirés</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
|
th { background-color: #f2f2f2; }
|
|
h1 { color: #dc2626; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Produits expirés</h1>
|
|
<p>Date d'impression: ${format(new Date(), "dd/MM/yyyy HH:mm")}</p>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Produit</th>
|
|
<th>Code EAN13</th>
|
|
<th>Date d'expiration</th>
|
|
<th>Type</th>
|
|
<th>Fournisseur</th>
|
|
<th>Jours dépassés</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${expired.map(product => {
|
|
const today = new Date();
|
|
const expiry = new Date(product.dlcDate || new Date());
|
|
const diffDays = Math.ceil((today.getTime() - expiry.getTime()) / (1000 * 60 * 60 * 24));
|
|
return `
|
|
<tr>
|
|
<td>${product.productName}</td>
|
|
<td>${product.gencode || '-'}</td>
|
|
<td>${product.dlcDate ? format(new Date(product.dlcDate), "dd/MM/yyyy") : 'N/A'}</td>
|
|
<td>${product.dateType.toUpperCase()}</td>
|
|
<td>${product.supplier?.name || 'N/A'}</td>
|
|
<td>${diffDays}</td>
|
|
</tr>
|
|
`;
|
|
}).join('')}
|
|
</tbody>
|
|
</table>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
const printWindow = window.open('', '_blank');
|
|
if (printWindow) {
|
|
printWindow.document.write(printContent);
|
|
printWindow.document.close();
|
|
printWindow.print();
|
|
}
|
|
};
|
|
|
|
const filteredProducts = (dlcProducts || []).filter(product => {
|
|
if (searchTerm) {
|
|
return product.productName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
(product.supplier?.name && product.supplier.name.toLowerCase().includes(searchTerm.toLowerCase()));
|
|
}
|
|
return true;
|
|
});
|
|
|
|
// Pagination
|
|
const {
|
|
currentPage,
|
|
setCurrentPage,
|
|
itemsPerPage,
|
|
setItemsPerPage,
|
|
totalPages,
|
|
paginatedData: paginatedProducts,
|
|
totalItems
|
|
} = usePagination(filteredProducts, 10);
|
|
|
|
if (authLoading) {
|
|
return <div className="flex justify-center items-center h-64">Chargement...</div>;
|
|
}
|
|
|
|
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">
|
|
<Clock className="w-6 h-6 mr-3 text-orange-600" />
|
|
Gestion DLC
|
|
</h2>
|
|
<p className="text-gray-600 mt-1">
|
|
Gestion des dates limites de consommation
|
|
</p>
|
|
</div>
|
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button className="bg-blue-600 hover:bg-blue-700 text-white shadow-md">
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Nouveau produit DLC
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{editingProduct ? "Modifier le produit DLC" : "Nouveau produit DLC"}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="productName"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Nom du produit</FormLabel>
|
|
<FormControl>
|
|
<Input {...field} placeholder="Nom du produit" />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="supplierId"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Fournisseur</FormLabel>
|
|
<Select
|
|
onValueChange={(value) => field.onChange(parseInt(value))}
|
|
value={field.value?.toString()}
|
|
>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Sélectionner un fournisseur" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
{suppliers.map((supplier: any) => (
|
|
<SelectItem key={supplier.id} value={supplier.id.toString()}>
|
|
{supplier.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="gencode"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Code EAN13 (optionnel)</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
value={field.value || ""}
|
|
placeholder="1234567890123"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="dlcDate"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Date d'expiration</FormLabel>
|
|
<FormControl>
|
|
<Input type="date" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="dateType"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Type de date</FormLabel>
|
|
<Select onValueChange={field.onChange} value={field.value}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Sélectionner le type" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value="dlc">DLC (Date Limite de Consommation)</SelectItem>
|
|
<SelectItem value="ddm">DDM (Date de Durabilité Minimale)</SelectItem>
|
|
<SelectItem value="dluo">DLUO (Date Limite d'Utilisation Optimale)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="notes"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Notes (optionnel)</FormLabel>
|
|
<FormControl>
|
|
<Textarea {...field} placeholder="Notes ou observations..." />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="flex justify-end space-x-2">
|
|
<Button type="button" variant="outline" onClick={() => setIsDialogOpen(false)}>
|
|
Annuler
|
|
</Button>
|
|
<Button type="submit" disabled={createMutation.isPending || updateMutation.isPending}>
|
|
{editingProduct ? "Mettre à jour" : "Créer"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Filter className="w-5 h-5" />
|
|
Filtres
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="text-sm font-medium">Statut</label>
|
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Tous les statuts</SelectItem>
|
|
<SelectItem value="en_cours">Actifs</SelectItem>
|
|
<SelectItem value="expires_soon">Expire bientôt</SelectItem>
|
|
<SelectItem value="expires">Expirés</SelectItem>
|
|
<SelectItem value="valides">Validés</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium">Fournisseur</label>
|
|
<Select value={supplierFilter} onValueChange={setSupplierFilter}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Tous les fournisseurs" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Tous les fournisseurs</SelectItem>
|
|
{suppliers.map((supplier: any) => (
|
|
<SelectItem key={supplier.id} value={supplier.id.toString()}>
|
|
{supplier.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium">Recherche</label>
|
|
<Input
|
|
placeholder="Rechercher un produit..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Statistics */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Produits Actifs</CardTitle>
|
|
<Package className="h-4 w-4 text-green-600" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-green-600">{stats.active}</div>
|
|
<p className="text-xs text-muted-foreground">Produits en cours</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Expire Bientôt</CardTitle>
|
|
<div className="flex items-center gap-2">
|
|
<Clock className="h-4 w-4 text-orange-600" />
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={printExpiringSoon}
|
|
className="h-6 w-6 p-0 hover:bg-orange-100"
|
|
>
|
|
<FileText className="h-3 w-3 text-orange-600" />
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-orange-600">{stats.expiringSoon}</div>
|
|
<p className="text-xs text-muted-foreground">Dans les 15 prochains jours</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Expirés</CardTitle>
|
|
<div className="flex items-center gap-2">
|
|
<AlertCircle className="h-4 w-4 text-red-600" />
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={printExpired}
|
|
className="h-6 w-6 p-0 hover:bg-red-100"
|
|
>
|
|
<FileText className="h-3 w-3 text-red-600" />
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-red-600">{stats.expired}</div>
|
|
<p className="text-xs text-muted-foreground">Nécessitent une action</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Products Table Section */}
|
|
<div>
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex justify-between items-center">
|
|
<CardTitle>Produits DLC ({totalItems})</CardTitle>
|
|
<Button variant="outline" size="sm">
|
|
<Download className="w-4 h-4 mr-2" />
|
|
Exporter PDF
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{productsLoading ? (
|
|
<div className="flex justify-center items-center h-32">Chargement des produits...</div>
|
|
) : totalItems === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
Aucun produit DLC trouvé
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Produit</TableHead>
|
|
<TableHead>Code EAN13</TableHead>
|
|
<TableHead>Type</TableHead>
|
|
<TableHead>Date d'expiration</TableHead>
|
|
<TableHead>Fournisseur</TableHead>
|
|
<TableHead>Statut</TableHead>
|
|
<TableHead>Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{paginatedProducts.map((product) => (
|
|
<TableRow
|
|
key={product.id}
|
|
className={product.status === "valide" ? "opacity-60 bg-gray-50 dark:bg-gray-800" : ""}
|
|
>
|
|
<TableCell className="font-medium">{product.productName}</TableCell>
|
|
<TableCell className="text-sm text-muted-foreground">
|
|
{product.gencode || '-'}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant="outline" className="uppercase">
|
|
{product.dateType}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
{product.dlcDate ? format(new Date(product.dlcDate), "dd/MM/yyyy", { locale: fr }) : 'Date non définie'}
|
|
</TableCell>
|
|
<TableCell>{product.supplier?.name || 'Non défini'}</TableCell>
|
|
<TableCell>{getStatusBadge(product.status, product.dlcDate)}</TableCell>
|
|
<TableCell>
|
|
<div className="flex space-x-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleEdit(product)}
|
|
>
|
|
<Edit className="w-4 h-4" />
|
|
</Button>
|
|
{shouldShowValidateButton(product) && (user?.role === 'admin' || user?.role === 'manager') && (
|
|
<Button
|
|
variant="default"
|
|
size="sm"
|
|
onClick={() => handleValidate(product.id)}
|
|
disabled={validateMutation.isPending}
|
|
className="bg-green-600 hover:bg-green-700"
|
|
>
|
|
<CheckCircle className="w-4 h-4" />
|
|
</Button>
|
|
)}
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
onClick={() => handleDelete(product.id)}
|
|
disabled={deleteMutation.isPending}
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
|
|
{/* Pagination */}
|
|
{totalItems > 0 && (
|
|
<Pagination
|
|
currentPage={currentPage}
|
|
totalPages={totalPages}
|
|
totalItems={totalItems}
|
|
itemsPerPage={itemsPerPage}
|
|
onPageChange={setCurrentPage}
|
|
onItemsPerPageChange={setItemsPerPage}
|
|
className="mt-4 border-t border-gray-200 pt-4"
|
|
/>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Modal de confirmation de suppression */}
|
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Confirmer la suppression</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Êtes-vous sûr de vouloir supprimer ce produit DLC ? Cette action est irréversible.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
|
<AlertDialogAction onClick={confirmDelete} className="bg-red-600 hover:bg-red-700">
|
|
Supprimer
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
);
|
|
} |