/** * MobileCustomerOrdersPage.tsx * Version mobile de la page Commandes Clients avec création */ import { useState, useEffect, useRef, useMemo } 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 { Package, Search, Phone, PhoneCall, User, ClipboardList, Plus, MoreVertical, X, Filter, Loader2, MessageSquare } from "lucide-react"; import { format } 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, SheetTrigger, } 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 { Checkbox } from "@/components/ui/checkbox"; // Schema for form validation const customerOrderFormSchema = z.object({ customerName: z.string().min(1, "Nom du client est requis"), contactNumber: z.string().optional(), productName: z.string().min(1, "Désignation est requise"), productReference: z.string().optional(), gencode: z.string().optional(), quantity: z.coerce.number().int().positive().default(1), supplierId: z.coerce.number().int().positive().default(1), orderTaker: z.string().min(1, "Prise de commande requise"), notes: z.string().optional(), deposit: z.coerce.number().optional().default(0), }); // Référence stable tant que la liste n'est pas chargée const NO_ORDERS: any[] = []; export default function MobileCustomerOrdersPage() { const { user } = useAuthUnified(); const { selectedStoreId } = useStore(); const [searchTerm, setSearchTerm] = useState(""); const [isCreateOpen, setIsCreateOpen] = useState(false); const [contactOrder, setContactOrder] = useState(null); const [contactComment, setContactComment] = useState(""); const { toast } = useToast(); const queryClient = useQueryClient(); // Queries const { data: orders = NO_ORDERS, isLoading } = useQuery({ queryKey: ["/api/customer-orders", selectedStoreId], queryFn: async () => { if (!selectedStoreId) return []; const response = await fetch( `/api/customer-orders?storeId=${selectedStoreId}`, { credentials: 'include' } ); if (!response.ok) throw new Error("Failed to fetch customer orders"); return response.json(); }, enabled: !!selectedStoreId && !!user, }); const { data: suppliers = [] } = useQuery({ queryKey: ['/api/suppliers'], queryFn: async () => { const res = await fetch('/api/suppliers', { credentials: 'include' }); return res.json(); } }); const { data: groups = [] } = useQuery({ queryKey: ['/api/groups'], queryFn: async () => { const res = await fetch('/api/groups', { credentials: 'include' }); return res.json(); } }); // Mutations const statusMutation = useMutation({ mutationFn: ({ id, status }: { id: number; status: string }) => apiRequest(`/api/customer-orders/${id}`, 'PUT', { status }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/customer-orders"] }); toast({ title: "Statut mis à jour" }); }, }); const markCalledMutation = useMutation({ mutationFn: ({ id, comment }: { id: number; comment?: string }) => apiRequest(`/api/customer-orders/${id}/mark-called`, 'PATCH', { comment }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/customer-orders"] }); toast({ title: "Client marqué comme contacté" }); setContactOrder(null); setContactComment(""); }, }); const unmarkCalledMutation = useMutation({ mutationFn: (id: number) => apiRequest(`/api/customer-orders/${id}`, 'PUT', { customerNotified: false, notifiedAt: null, notifiedComment: null }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/customer-orders"] }); toast({ title: "Contact annulé" }); setContactOrder(null); }, }); const createMutation = useMutation({ mutationFn: (data: any) => apiRequest('/api/customer-orders', 'POST', data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/customer-orders"] }); toast({ title: "Commande créée avec succès" }); setIsCreateOpen(false); form.reset(); }, onError: (err: any) => { toast({ title: "Erreur lors de la création", description: err.message, variant: "destructive" }); } }); // Form setup const form = useForm({ resolver: zodResolver(customerOrderFormSchema), defaultValues: { customerName: "", contactNumber: "", productName: "", productReference: "", gencode: "", quantity: 1, supplierId: 1, orderTaker: user?.username || "", notes: "", deposit: 0 } }); // Lookup API ffnancy par gencode ou référence (déclenché manuellement) const [articleLookupLoading, setArticleLookupLoading] = useState(false); const [articleNotFound, setArticleNotFound] = useState(false); const fetchAndFillArticle = async (params: URLSearchParams) => { setArticleLookupLoading(true); setArticleNotFound(false); try { const res = await fetch(`/api/ffnancy/articles?${params}&limit=1`, { credentials: 'include' }); if (!res.ok) { setArticleNotFound(true); return; } const data = await res.json(); const article = data.articles?.[0]; if (!article) { setArticleNotFound(true); return; } setArticleNotFound(false); form.setValue("productName", article.libelle1, { shouldValidate: true }); if (article.gtin) form.setValue("gencode", article.gtin, { shouldValidate: true }); if (article.ref_fou_principale) form.setValue("productReference", article.ref_fou_principale, { shouldValidate: true }); 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 codefou_principal */ } 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 { setArticleNotFound(true); } finally { setArticleLookupLoading(false); } }; const handleSearchByGencode = () => { const val = form.getValues("gencode"); if (val) fetchAndFillArticle(new URLSearchParams({ ean: val })); }; const handleSearchByReference = () => { const val = form.getValues("productReference"); if (val) fetchAndFillArticle(new URLSearchParams({ codein: val })); }; const onSubmit = (data: any) => { // Validation basique groupe 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: "Impossible de déterminer le magasin", variant: "destructive" }); return; } const submitData = { ...data, customerPhone: data.contactNumber, productDesignation: data.productName, status: "En attente de Commande", groupId, isPromotionalPrice: data.isPromotionalPrice, customerNotified: data.customerNotified }; createMutation.mutate(submitData); }; // Recalculé seulement quand la liste ou la recherche change (et non à // chaque frappe dans le commentaire de contact) const filteredOrders = useMemo(() => { const search = searchTerm.toLowerCase(); return orders.filter((order: any) => order.customerName.toLowerCase().includes(search) || order.productDesignation.toLowerCase().includes(search) || (order.productReference && order.productReference.toLowerCase().includes(search)) ); }, [orders, searchTerm]); const getStatusColor = (status: string) => { switch (status) { case "Disponible": return "bg-green-100 text-green-800 border-green-200"; case "Commande en Cours": return "bg-blue-100 text-blue-800 border-blue-200"; case "Retiré": return "bg-gray-100 text-gray-800 border-gray-200"; case "Annulé": return "bg-red-100 text-red-800 border-red-200"; default: return "bg-yellow-100 text-yellow-800 border-yellow-200"; } }; return (
{/* Header */}

Commandes Clients

{/* Search */}
setSearchTerm(e.target.value)} className="pl-9 h-10" />
{/* Orders List */}
{isLoading ? (
) : filteredOrders.length === 0 ? (

Aucune commande trouvée

) : ( filteredOrders.map((order: any) => (
{order.status} {format(new Date(order.createdAt), "dd/MM", { locale: fr })}

{order.customerName}

{order.productDesignation}

statusMutation.mutate({ id: order.id, status: 'Disponible' })}> Marquer Disponible statusMutation.mutate({ id: order.id, status: 'Retiré' })}> Marquer Retiré statusMutation.mutate({ id: order.id, status: 'Annulé' })}> Annuler
{order.customerPhone && ( Appeler )}
Qty: {order.quantity || 1}
{order.deposit && parseFloat(order.deposit) > 0 && (
Acompte: {parseFloat(order.deposit)}€
)}
)) )}
{/* Create Order Sheet */} Nouvelle Commande

Client

( Nom )} /> ( Téléphone )} />

Vendeur

( Prise par )} />

Produit

( Désignation