diff --git a/client/src/components/RouterProduction.tsx b/client/src/components/RouterProduction.tsx index 7acb02c..13f4f5a 100644 --- a/client/src/components/RouterProduction.tsx +++ b/client/src/components/RouterProduction.tsx @@ -38,6 +38,9 @@ import MobileCalendarPage from "@/pages/mobile/CalendarPage"; import MobileTasksPage from "@/pages/mobile/TasksPage"; import MobilePublicitiesPage from "@/pages/mobile/PublicitiesPage"; import MobileCustomerOrdersPage from "@/pages/mobile/CustomerOrdersPage"; +import MobileDlcPage from "@/pages/mobile/DlcPage"; +import MobileSavPage from "@/pages/mobile/SavPage"; +import MobileAvoirsPage from "@/pages/mobile/AvoirsPage"; function RouterProduction() { const { isAuthenticated, isLoading, user, environment, error } = useAuthUnified(); @@ -106,10 +109,10 @@ function RouterProduction() { - + - - + + diff --git a/client/src/pages/mobile/AvoirsPage.tsx b/client/src/pages/mobile/AvoirsPage.tsx new file mode 100644 index 0000000..9ea2025 --- /dev/null +++ b/client/src/pages/mobile/AvoirsPage.tsx @@ -0,0 +1,377 @@ +/** + * MobileAvoirsPage.tsx + * Version mobile de la page Gestion des Avoirs + */ +import { useState } 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 { + FileText, + Search, + Plus, + Filter, + CheckCircle, + AlertCircle, + Clock, + MoreVertical, + Trash2, + Ban +} 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, +} 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, +} from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; + +const avoirSchema = z.object({ + supplierId: z.coerce.number().min(1, "Fournisseur requis"), + invoiceReference: z.string().optional(), + amount: z.coerce.number().optional(), + comment: z.string().optional(), + status: z.enum(["En attente de demande", "Demandé", "Reçu"]).default("En attente de demande"), +}); + +export default function MobileAvoirsPage() { + const { user } = useAuthUnified(); + const { selectedStoreId } = useStore(); + const [searchTerm, setSearchTerm] = useState(""); + const [isCreateOpen, setIsCreateOpen] = useState(false); + const { toast } = useToast(); + const queryClient = useQueryClient(); + + // Queries + const { data: avoirs = [], isLoading } = useQuery({ + queryKey: ["/api/avoirs", selectedStoreId], + queryFn: async () => { + if (!selectedStoreId) return []; + const res = await fetch(`/api/avoirs?storeId=${selectedStoreId}`, { credentials: 'include' }); + if (!res.ok) throw new Error("Erreur fetch Avoirs"); + return res.json(); + }, + enabled: !!selectedStoreId && !!user, + }); + + const { data: suppliers = [] } = useQuery({ + queryKey: ['/api/suppliers'], + queryFn: () => apiRequest('/api/suppliers') + }); + + const { data: groups = [] } = useQuery({ + queryKey: ['/api/groups'], + queryFn: () => apiRequest('/api/groups') + }); + + // Mutations + const createMutation = useMutation({ + mutationFn: (data: any) => apiRequest('/api/avoirs', 'POST', data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/avoirs"] }); + toast({ title: "Avoir créé avec succès" }); + setIsCreateOpen(false); + form.reset(); + }, + onError: (err: any) => toast({ title: "Erreur", description: err.message, variant: "destructive" }) + }); + + const updateStatusMutation = useMutation({ + mutationFn: ({ id, data }: { id: number, data: any }) => apiRequest(`/api/avoirs/${id}`, 'PUT', data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/avoirs"] }); + toast({ title: "Statut mis à jour" }); + } + }); + + const deleteMutation = useMutation({ + mutationFn: (id: number) => apiRequest(`/api/avoirs/${id}`, 'DELETE'), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/avoirs"] }); + toast({ title: "Avoir supprimé" }); + } + }); + + // Form + const form = useForm({ + resolver: zodResolver(avoirSchema), + defaultValues: { + supplierId: 0, + invoiceReference: "", + amount: 0, + comment: "", + status: "En attente de demande" + } + }); + + 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: "Aucun magasin sélectionné", variant: "destructive" }); + return; + } + + createMutation.mutate({ + ...data, + groupId, + commercialProcessed: false + }); + }; + + const sortedAvoirs = [...avoirs].sort((a: any, b: any) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + + const filteredAvoirs = sortedAvoirs.filter((a: any) => + (a.supplier?.name?.toLowerCase().includes(searchTerm.toLowerCase()) || "") || + (a.invoiceReference?.toLowerCase().includes(searchTerm.toLowerCase()) || "") + ); + + const getStatusBadge = (status: string) => { + switch (status) { + case "En attente de demande": return En attente; + case "Demandé": return Demandé; + case "Reçu": return Reçu; + default: return {status}; + } + }; + + return ( + + + {/* Header */} + + + Suivi des Avoirs + + + {/* Search */} + + + setSearchTerm(e.target.value)} + className="pl-9 h-10" + /> + + + {/* List */} + + {isLoading ? ( + + + + ) : filteredAvoirs.length === 0 ? ( + + + Aucun avoir trouvé + + ) : ( + filteredAvoirs.map((avoir: any) => ( + + + + + + {getStatusBadge(avoir.status)} + + {format(new Date(avoir.createdAt), "dd/MM", { locale: fr })} + + + {avoir.supplier?.name} + + {avoir.invoiceReference && ( + + {avoir.invoiceReference} + + )} + {avoir.amount && ( + + {parseFloat(avoir.amount).toFixed(2)} € + + )} + + + + + + + + + + + updateStatusMutation.mutate({ + id: avoir.id, + data: { ...avoir, status: 'En attente de demande' } + })}> + Marquer En attente + + updateStatusMutation.mutate({ + id: avoir.id, + data: { ...avoir, status: 'Demandé' } + })}> + Marquer Demandé + + updateStatusMutation.mutate({ + id: avoir.id, + data: { ...avoir, status: 'Reçu' } + })}> + Marquer Reçu + + deleteMutation.mutate(avoir.id)} + className="text-red-600" + > + + Supprimer + + + + + + {avoir.comment && ( + + "{avoir.comment}" + + )} + + {avoir.nocodbVerified && ( + + + Vérifié Compta + + )} + + + )) + )} + + + + {/* Create Sheet */} + + + + + + Nouvel Avoir + + + + + + ( + + Fournisseur + field.onChange(parseInt(val))} + defaultValue={field.value ? field.value.toString() : undefined} + > + + + + + + + {suppliers.map((s: any) => ( + + {s.name} + + ))} + + + + )} + /> + + + ( + + Réf. Facture + + + )} + /> + ( + + Montant + + + )} + /> + + + ( + + Commentaire / Raison + + + + + )} + /> + + + {createMutation.isPending ? "Création..." : "Créer la demande"} + + + + + + + {/* FAB */} + setIsCreateOpen(true)} + > + + + + ); +} diff --git a/client/src/pages/mobile/CustomerOrdersPage.tsx b/client/src/pages/mobile/CustomerOrdersPage.tsx index d683ae0..7b77be5 100644 --- a/client/src/pages/mobile/CustomerOrdersPage.tsx +++ b/client/src/pages/mobile/CustomerOrdersPage.tsx @@ -1,6 +1,6 @@ /** * MobileCustomerOrdersPage.tsx - * Version mobile de la page Commandes Clients + * Version mobile de la page Commandes Clients avec création */ import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; @@ -18,7 +18,9 @@ import { User, ClipboardList, Plus, - MoreVertical + MoreVertical, + X, + Filter } from "lucide-react"; import { format } from "date-fns"; import { fr } from "date-fns/locale"; @@ -30,14 +32,56 @@ import { 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"; + +// 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().optional(), + notes: z.string().optional(), + deposit: z.coerce.number().optional().default(0), +}); export default function MobileCustomerOrdersPage() { const { user } = useAuthUnified(); const { selectedStoreId } = useStore(); const [searchTerm, setSearchTerm] = useState(""); + const [isCreateOpen, setIsCreateOpen] = useState(false); const { toast } = useToast(); const queryClient = useQueryClient(); + // Queries const { data: orders = [], isLoading } = useQuery({ queryKey: ["/api/customer-orders", selectedStoreId], queryFn: async () => { @@ -52,7 +96,23 @@ export default function MobileCustomerOrdersPage() { enabled: !!selectedStoreId && !!user, }); - // Mutation pour changer le statut + 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 }), @@ -62,22 +122,74 @@ export default function MobileCustomerOrdersPage() { }, }); + 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 + } + }); + + 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: false, + customerNotified: false + }; + + createMutation.mutate(submitData); + }; + const filteredOrders = orders.filter((order: any) => order.customerName.toLowerCase().includes(searchTerm.toLowerCase()) || order.productDesignation.toLowerCase().includes(searchTerm.toLowerCase()) || (order.productReference && order.productReference.toLowerCase().includes(searchTerm.toLowerCase())) ); - const getStatusVariant = (status: string) => { - switch (status) { - case "Disponible": return "success"; // vert - case "Commande en Cours": return "default"; // bleu - case "Retiré": return "secondary"; // gris - case "Annulé": return "destructive"; // rouge - default: return "outline"; - } - }; - const getStatusColor = (status: string) => { switch (status) { case "Disponible": return "bg-green-100 text-green-800 border-green-200"; @@ -189,12 +301,152 @@ export default function MobileCustomerOrdersPage() { + {/* Create Order Sheet */} + + + + + + Nouvelle Commande + + + + + + + + Client + ( + + Nom + + + + + )} + /> + ( + + Téléphone + + + + + )} + /> + + + + Produit + ( + + Désignation + + + + + )} + /> + + ( + + Qté + + + + + )} + /> + ( + + Acompte (€) + + + + + )} + /> + + ( + + Fournisseur + field.onChange(parseInt(val))} + defaultValue={field.value.toString()} + > + + + + + + + {suppliers.map((s: any) => ( + + {s.name} + + ))} + + + + )} + /> + ( + + Gencode (Scanner si dispo) + + + + + )} + /> + + + ( + + Notes internes + + + + + )} + /> + + + {createMutation.isPending ? "Création..." : "Valider la commande"} + + + + + + {/* FAB */} { - // TODO: Create modal logic - }} + className="fixed bottom-20 right-4 h-14 w-14 rounded-full shadow-lg bg-blue-600 hover:bg-blue-700 p-0 flex items-center justify-center z-50 transition-transform active:scale-95" + onClick={() => setIsCreateOpen(true)} > diff --git a/client/src/pages/mobile/DlcPage.tsx b/client/src/pages/mobile/DlcPage.tsx new file mode 100644 index 0000000..1168f52 --- /dev/null +++ b/client/src/pages/mobile/DlcPage.tsx @@ -0,0 +1,409 @@ +/** + * MobileDlcPage.tsx + * Version mobile de la page Gestion des DLC + */ +import { useState, 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 { + Clock, + Search, + Plus, + Filter, + AlertTriangle, + CheckCircle, + X, + Calendar, + MoreVertical +} 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 { 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"), +}); + +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" + } + }); + + 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 + + + + + + + + DLC + DDM + DLUO + + + + )} + /> + + + ( + + Fournisseur + field.onChange(parseInt(val))} + defaultValue={field.value.toString()} + > + + + + + + + {suppliers.map((s: any) => ( + + {s.name} + + ))} + + + + )} + /> + + ( + + Gencode (Scanner si dispo) + + + + + )} + /> + + + {createMutation.isPending ? "Ajout..." : "Ajouter le produit"} + + + + + + + {/* FAB */} + setIsCreateOpen(true)} + > + + + + ); +} diff --git a/client/src/pages/mobile/SavPage.tsx b/client/src/pages/mobile/SavPage.tsx new file mode 100644 index 0000000..fdffdd0 --- /dev/null +++ b/client/src/pages/mobile/SavPage.tsx @@ -0,0 +1,347 @@ +/** + * MobileSavPage.tsx + * Version mobile de la page SAV (Service Après-Vente) + */ +import { useState } 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 { + Wrench, + Search, + Plus, + Filter, + Camera, + Image as ImageIcon, + MoreVertical, + CheckCircle, + XCircle, + Phone +} 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, +} 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 { Textarea } from "@/components/ui/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +// Schema +const savFormSchema = z.object({ + customerName: z.string().min(1, "Nom client requis"), + customerPhone: z.string().optional(), + productName: z.string().min(1, "Produit requis"), + issueDescription: z.string().min(1, "Description requise"), + priority: z.enum(["low", "medium", "high", "critical"]).default("medium"), +}); + +export default function MobileSavPage() { + const { user } = useAuthUnified(); + const { selectedStoreId } = useStore(); + const [searchTerm, setSearchTerm] = useState(""); + const [isCreateOpen, setIsCreateOpen] = useState(false); + const { toast } = useToast(); + const queryClient = useQueryClient(); + + // Queries + const { data: tickets = [], isLoading } = useQuery({ + queryKey: ["/api/sav-tickets", selectedStoreId], + queryFn: async () => { + if (!selectedStoreId) return []; + const res = await fetch(`/api/sav-tickets?storeId=${selectedStoreId}`, { credentials: 'include' }); + if (!res.ok) throw new Error("Erreur fetch SAV"); + return res.json(); + }, + enabled: !!selectedStoreId && !!user, + }); + + const { data: groups = [] } = useQuery({ + queryKey: ['/api/groups'], + queryFn: () => apiRequest('/api/groups') + }); + + // Mutations + const statusMutation = useMutation({ + mutationFn: ({ id, status }: { id: number; status: string }) => + apiRequest(`/api/sav-tickets/${id}/status`, 'PATCH', { status }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/sav-tickets"] }); + toast({ title: "Statut mis à jour" }); + } + }); + + const createMutation = useMutation({ + mutationFn: (data: any) => apiRequest('/api/sav-tickets', 'POST', data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/sav-tickets"] }); + toast({ title: "Ticket SAV créé" }); + setIsCreateOpen(false); + form.reset(); + }, + onError: (err: any) => toast({ title: "Erreur", description: err.message, variant: "destructive" }) + }); + + // Form + const form = useForm({ + resolver: zodResolver(savFormSchema), + defaultValues: { + customerName: "", + customerPhone: "", + productName: "", + issueDescription: "", + priority: "medium" + } + }); + + 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: "Aucun magasin sélectionné", variant: "destructive" }); + return; + } + + createMutation.mutate({ + ...data, + groupId, + status: "open" + }); + }; + + const filteredTickets = tickets.filter((t: any) => + t.customerName.toLowerCase().includes(searchTerm.toLowerCase()) || + t.productName.toLowerCase().includes(searchTerm.toLowerCase()) || + t.ticketNumber?.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const getStatusBadge = (status: string) => { + switch (status) { + case "open": return Ouvert; + case "in_progress": return En cours; + case "waiting_parts": return Pièces; + case "resolved": return Résolu; + case "closed": return Fermé; + default: return {status}; + } + }; + + return ( + + + {/* Header */} + + + SAV / Réparations + + + {/* Search */} + + + setSearchTerm(e.target.value)} + className="pl-9 h-10" + /> + + + {/* List */} + + {isLoading ? ( + + + + ) : filteredTickets.length === 0 ? ( + + + Aucun ticket SAV + + ) : ( + filteredTickets.map((ticket: any) => ( + + + + + + {getStatusBadge(ticket.status)} + + #{ticket.ticketNumber} • {format(new Date(ticket.createdAt), "dd/MM", { locale: fr })} + + + {ticket.customerName} + {ticket.productName} + + + + + + + + + + statusMutation.mutate({ id: ticket.id, status: 'in_progress' })}> + En cours + + statusMutation.mutate({ id: ticket.id, status: 'waiting_parts' })}> + Attente Pièces + + statusMutation.mutate({ id: ticket.id, status: 'resolved' })} className="text-green-600"> + Résolu + + statusMutation.mutate({ id: ticket.id, status: 'closed' })} className="text-gray-600"> + Fermer + + + + + + + {ticket.issueDescription} + + + {ticket.customerPhone && ( + + + + Appeler + + + )} + + + )) + )} + + + + {/* Create Sheet */} + + + + + + Nouveau Ticket SAV + + + + + + + Client + ( + + Nom + + + )} + /> + ( + + Téléphone + + + )} + /> + + + + Problème + ( + + Produit + + + )} + /> + ( + + Priorité + + + + Faible + Moyenne + Haute + Critique + + + + )} + /> + ( + + Description Panne + + + )} + /> + + + + {createMutation.isPending ? "Création..." : "Créer le ticket"} + + + + + + + {/* FAB */} + setIsCreateOpen(true)} + > + + + + ); +}
Aucun avoir trouvé
Aucun produit trouvé
+ {p.supplier?.name} {p.gencode ? `• ${p.gencode}` : ''} +
Aucun ticket SAV
{ticket.productName}