/** * MobileDlcPage.tsx * Version mobile de la page Gestion des DLC */ import { useState, useMemo, useEffect, useRef } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useAuthUnified } from "@/hooks/useAuthUnified"; import { useStore } from "@/contexts/StoreContext"; import MobileLayout from "./MobileLayout"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Clock, Search, Plus, Filter, AlertTriangle, CheckCircle, X, Calendar, MoreVertical, Loader2 } from "lucide-react"; import { format, parseISO } from "date-fns"; import { fr } from "date-fns/locale"; import { apiRequest } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Sheet, SheetContent, SheetHeader, SheetTitle, } from "@/components/ui/sheet"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; const dlcFormSchema = z.object({ productName: z.string().min(1, "Nom du produit requis"), gencode: z.string().optional(), dlcDate: z.string().min(1, "Date requise"), dateType: z.enum(["dlc", "ddm", "dluo"]).default("dlc"), supplierId: z.coerce.number().min(1, "Fournisseur requis"), status: z.string().default("en_cours"), notes: z.string().optional(), }); export default function MobileDlcPage() { const { user } = useAuthUnified(); const { selectedStoreId } = useStore(); const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState("en_cours"); // en_cours, expires_soon, expires, valides const [isCreateOpen, setIsCreateOpen] = useState(false); const { toast } = useToast(); const queryClient = useQueryClient(); // Data fetching const { data: dlcProducts = [], isLoading } = useQuery({ queryKey: ["/api/dlc-products", selectedStoreId, statusFilter, searchTerm], queryFn: async () => { const params = new URLSearchParams(); if (selectedStoreId) params.append("storeId", selectedStoreId.toString()); // Simplify fetching everything client side for mobile smoothness or filter server side? // Let's filter client side for better UX given the likely smaller dataset per store if (selectedStoreId) { const res = await fetch(`/api/dlc-products?storeId=${selectedStoreId}`, { credentials: 'include' }); return res.json(); } return []; }, enabled: !!selectedStoreId && !!user, }); const { data: suppliers = [] } = useQuery({ queryKey: ["/api/suppliers", "dlc"], queryFn: () => apiRequest("/api/suppliers?dlc=true"), }); const { data: groups = [] } = useQuery({ queryKey: ["/api/groups"], queryFn: () => apiRequest("/api/groups"), }); // Mutations const createMutation = useMutation({ mutationFn: (data: any) => apiRequest("/api/dlc-products", "POST", data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"] }); toast({ title: "Produit ajouté" }); setIsCreateOpen(false); form.reset(); }, onError: (err: any) => toast({ title: "Erreur", description: err.message, variant: "destructive" }) }); const validateMutation = useMutation({ mutationFn: (id: number) => apiRequest(`/api/dlc-products/${id}/validate`, "POST"), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"] }); toast({ title: "Produit validé (retiré)" }); } }); // Form const form = useForm({ resolver: zodResolver(dlcFormSchema), defaultValues: { productName: "", gencode: "", dlcDate: format(new Date(), 'yyyy-MM-dd'), dateType: "dlc", supplierId: 1, status: "en_cours", notes: "" } }); // EAN lookup from external API const [eanLookupLoading, setEanLookupLoading] = useState(false); const eanDebounceRef = useRef | null>(null); const gencodeValue = form.watch("gencode"); useEffect(() => { if (!gencodeValue || gencodeValue.length < 8) return; if (eanDebounceRef.current) clearTimeout(eanDebounceRef.current); eanDebounceRef.current = setTimeout(async () => { setEanLookupLoading(true); try { // Étape 1 : récupérer l'article par EAN const res = await fetch(`/api/ffnancy/articles?ean=${encodeURIComponent(gencodeValue)}&limit=1`, { credentials: 'include' }); if (!res.ok) return; const data = await res.json(); const article = data.articles?.[0]; if (!article) return; form.setValue("productName", article.libelle1, { shouldValidate: true }); // Étape 2 : chercher le fournisseur de la dernière entrée en stock let codefouToMatch = article.codefou_principal; try { const mvtRes = await fetch( `/api/ffnancy/mouvements/entrees?artNoId=${article.no_id}&limit=1&dateDebut=2000-01-01`, { credentials: 'include' } ); if (mvtRes.ok) { const mvtData = await mvtRes.json(); const lastEntree = mvtData.entrees?.[0]; if (lastEntree?.codefou) codefouToMatch = lastEntree.codefou; } } catch { /* fallback sur codefou_principal */ } // Matching : codefou exact en priorité, fallback par nom if (codefouToMatch) { const matched = (suppliers as any[]).find((s: any) => s.codefou && s.codefou.trim().toLowerCase() === codefouToMatch.trim().toLowerCase() ) || (suppliers as any[]).find((s: any) => s.name.toLowerCase().trim().includes(article.nom_fou_principal?.toLowerCase().trim() || '') || (article.nom_fou_principal?.toLowerCase().trim() || '').includes(s.name.toLowerCase().trim()) ); if (matched) { form.setValue("supplierId", matched.id, { shouldValidate: true }); } } } catch { // Lookup best-effort, on ignore les erreurs } finally { setEanLookupLoading(false); } }, 600); return () => { if (eanDebounceRef.current) clearTimeout(eanDebounceRef.current); }; }, [gencodeValue, suppliers]); const onSubmit = (data: any) => { let groupId = selectedStoreId; if (!groupId && user?.userGroups?.[0]?.groupId) groupId = user.userGroups[0].groupId; if (!groupId && groups.length > 0 && user?.role === 'admin') groupId = groups[0].id; if (!groupId) { toast({ title: "Erreur", description: "Magasin introuvable", variant: "destructive" }); return; } const submitData = { ...data, name: data.productName, expiryDate: new Date(data.dlcDate), quantity: 1, unit: "unité", location: "Magasin", alertThreshold: 15, groupId }; createMutation.mutate(submitData); }; // Filtering logic const filteredProducts = useMemo(() => { if (!Array.isArray(dlcProducts)) return []; let filtered = dlcProducts; // Search if (searchTerm) { const lower = searchTerm.toLowerCase(); filtered = filtered.filter((p: any) => p.productName.toLowerCase().includes(lower) || (p.gencode && p.gencode.includes(lower)) ); } // Status logic const today = new Date(); filtered = filtered.filter((p: any) => { const expiry = new Date(p.expiryDate); const daysUntil = Math.ceil((expiry.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); if (p.status === 'valides') return statusFilter === 'valides'; if (statusFilter === 'expires') return daysUntil < 0; if (statusFilter === 'expires_soon') return daysUntil >= 0 && daysUntil <= 15; if (statusFilter === 'en_cours') return daysUntil > 15; return true; // 'all' }); return filtered.sort((a: any, b: any) => new Date(a.expiryDate).getTime() - new Date(b.expiryDate).getTime()); }, [dlcProducts, searchTerm, statusFilter]); const getDaysBadge = (dateStr: string) => { const today = new Date(); const expiry = new Date(dateStr); const days = Math.ceil((expiry.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); if (days < 0) return Expiré J{days}; if (days <= 3) return J-{days}; if (days <= 15) return J-{days}; return J-{days}; }; return (
{/* Header */}

Gestion DLC

{/* Search */}
setSearchTerm(e.target.value)} className="pl-9 h-10" />
{/* Tabs for quick filtering */} OK Bientôt Expirés Validés {/* List */}
{isLoading ? (
) : filteredProducts.length === 0 ? (

Aucun produit trouvé

) : ( filteredProducts.map((p: any) => (
{getDaysBadge(p.expiryDate)} {format(new Date(p.expiryDate), 'dd/MM/yy')}

{p.productName}

{p.supplier?.name} {p.gencode ? `• ${p.gencode}` : ''}

{statusFilter !== 'valides' && ( validateMutation.mutate(p.id)}> Valider (Sortir) )}
)) )}
{/* Create Sheet */} Nouveau Produit DLC
( Nom du produit )} />
( Date Expiration )} /> ( Type )} />
( Fournisseur )} /> ( Gencode (Scanner si dispo)
{eanLookupLoading && ( )}
)} /> ( Notes internes