diff --git a/client/src/components/NewSaleFormMultiProduct.tsx b/client/src/components/NewSaleFormMultiProduct.tsx new file mode 100644 index 0000000..974886b --- /dev/null +++ b/client/src/components/NewSaleFormMultiProduct.tsx @@ -0,0 +1,683 @@ +import { useState } from "react"; +import { useForm, useFieldArray } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { useToast } from "@/hooks/use-toast"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { apiRequest } from "@/lib/queryClient"; +import { useStoreContext } from "@/hooks/useStoreContext"; +import { Package, User, Users, IdCard, Camera, Plus, Minus, Info } from "lucide-react"; +import { validateEAN13, ARTICLE_CATEGORY_MAPPING, PAYMENT_METHODS, IDENTITY_TYPES } from "@/lib/validation"; +import SimpleCameraModal from "@/components/SimpleCameraModal"; + +// Types pour les photos +type PhotoType = 'recto' | 'verso' | 'ticket'; + +// Schema de validation pour un produit +const productSchema = z.object({ + typeArticle: z.string().min(1, "Type d'article requis"), + categorie: z.string().min(1, "Catégorie requise"), + quantite: z.string().min(1, "Quantité requise").refine((val) => !isNaN(Number(val)) && Number(val) > 0, "Quantité invalide"), + gencode: z.string().length(13, "Code EAN-13 requis (13 chiffres)").refine(validateEAN13, "Code EAN-13 invalide"), +}); + +// Schema principal du formulaire +const formSchema = z.object({ + vendeur: z.string().min(1, "Vendeur requis"), + products: z.array(productSchema).min(1, "Au moins un produit requis"), + nom: z.string().min(1, "Nom requis"), + prenom: z.string().min(1, "Prénom requis"), + dateNaissance: z.string().min(1, "Date de naissance requise"), + lieuNaissance: z.string().min(1, "Lieu de naissance requis"), + modePaiement: z.string().min(1, "Mode de paiement requis"), + typeIdentite: z.string().min(1, "Type d'identité requis"), + numeroIdentite: z.string().min(1, "Numéro d'identité requis"), + autoriteDelivrance: z.string().optional(), + dateDelivrance: z.string().min(1, "Date de délivrance requise"), + photoRecto: z.string().optional(), + photoVerso: z.string().optional(), + photoTicket: z.string().optional(), +}); + +type FormData = z.infer; + +export default function NewSaleFormMultiProduct() { + const { toast } = useToast(); + const queryClient = useQueryClient(); + const { selectedStoreId } = useStoreContext(); + + // États pour les photos + const [currentPhotoType, setCurrentPhotoType] = useState(null); + const [isCameraOpen, setIsCameraOpen] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Formulaire avec valeurs par défaut + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + vendeur: '', + products: [{ typeArticle: '', categorie: '', quantite: '1', gencode: '' }], + nom: '', + prenom: '', + dateNaissance: '', + lieuNaissance: '', + modePaiement: '', + typeIdentite: '', + numeroIdentite: '', + autoriteDelivrance: '', + dateDelivrance: '', + photoRecto: '', + photoVerso: '', + photoTicket: '', + }, + }); + + // Gestion des produits dynamiques + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "products" + }); + + // Mutation pour créer une vente + const createSaleMutation = useMutation({ + mutationFn: async (data: any) => { + return await apiRequest(`/api/sales`, { + method: 'POST', + body: JSON.stringify(data), + }); + }, + onSuccess: () => { + toast({ + title: "Vente enregistrée", + description: "La vente a été enregistrée avec succès", + }); + + // Réinitialiser le formulaire + form.reset({ + vendeur: '', + products: [{ typeArticle: '', categorie: '', quantite: '1', gencode: '' }], + nom: '', + prenom: '', + dateNaissance: '', + lieuNaissance: '', + modePaiement: '', + typeIdentite: '', + numeroIdentite: '', + autoriteDelivrance: '', + dateDelivrance: '', + photoRecto: '', + photoVerso: '', + photoTicket: '', + }); + + // Invalider le cache des ventes + queryClient.invalidateQueries({ queryKey: ['/api/sales'] }); + }, + onError: (error: any) => { + toast({ + title: "Erreur", + description: error.message || "Erreur lors de l'enregistrement de la vente", + variant: "destructive", + }); + }, + }); + + // Ajouter un nouveau produit + const addProduct = () => { + append({ typeArticle: '', categorie: '', quantite: '1', gencode: '' }); + }; + + // Supprimer un produit + const removeProduct = (index: number) => { + if (fields.length > 1) { + remove(index); + } + }; + + // Soumission du formulaire + const onSubmit = async (data: FormData) => { + if (!selectedStoreId) { + toast({ + title: "Erreur", + description: "Aucun magasin sélectionné", + variant: "destructive", + }); + return; + } + + setIsSubmitting(true); + + try { + const saleData = { + storeId: selectedStoreId, + vendeur: data.vendeur, + products: data.products.map(product => ({ + typeArticle: product.typeArticle, + categorie: product.categorie, + quantite: parseInt(product.quantite), + gencode: product.gencode, + })), + nom: data.nom, + prenom: data.prenom, + dateNaissance: data.dateNaissance, + lieuNaissance: data.lieuNaissance, + modePaiement: data.modePaiement, + typeIdentite: data.typeIdentite, + numeroIdentite: data.numeroIdentite, + autoriteDelivrance: data.autoriteDelivrance, + dateDelivrance: data.dateDelivrance, + photoRecto: data.photoRecto, + photoVerso: data.photoVerso, + photoTicket: data.photoTicket, + }; + + await createSaleMutation.mutateAsync(saleData); + } catch (error) { + console.error('Sale creation error:', error); + } finally { + setIsSubmitting(false); + } + }; + + // Gestion des photos + const handlePhotoCapture = (photoType: PhotoType) => { + setCurrentPhotoType(photoType); + setIsCameraOpen(true); + }; + + const handlePhotoTaken = (photoData: string) => { + if (currentPhotoType === 'recto') { + form.setValue('photoRecto', photoData); + } else if (currentPhotoType === 'verso') { + form.setValue('photoVerso', photoData); + } else if (currentPhotoType === 'ticket') { + form.setValue('photoTicket', photoData); + } + + toast({ + title: "Photo capturée", + description: `Photo ${currentPhotoType} enregistrée avec succès`, + }); + + setIsCameraOpen(false); + setCurrentPhotoType(null); + }; + + const handleRemovePhoto = (photoType: PhotoType) => { + if (photoType === 'recto') { + form.setValue('photoRecto', ''); + } else if (photoType === 'verso') { + form.setValue('photoVerso', ''); + } else if (photoType === 'ticket') { + form.setValue('photoTicket', ''); + } + + toast({ + title: "Photo supprimée", + description: `Photo ${photoType} supprimée`, + }); + }; + + return ( +
+
+
+ + {/* Section Vendeur */} +
+
+ + Informations Vendeur +
+
+ ( + + Vendeur * + + + + + + )} + /> +
+
+ + {/* Section Produits */} +
+
+ + Produits ({fields.length}) + +
+ +
+ {fields.map((field, index) => ( +
+
+

+ Produit {index + 1} +

+ {fields.length > 1 && ( + + )} +
+ +
+ ( + + Type d'article * + + + + )} + /> + + { + const selectedType = form.watch(`products.${index}.typeArticle`); + const availableCategories = selectedType ? + ARTICLE_CATEGORY_MAPPING[selectedType as keyof typeof ARTICLE_CATEGORY_MAPPING] || [] : []; + + return ( + + Catégorie * + + + + ); + }} + /> + + ( + + Quantité * + + + + + + )} + /> + + ( + + Code EAN-13 * + + + + + + )} + /> +
+
+ ))} +
+ + + + + Information réglementaire : F1 = vente interdite aux mineurs de moins de 12 ans • F2/F3 = artifices puissants réservés aux majeurs, vente encadrée + + +
+ + {/* Section Client */} +
+
+ + Informations Client +
+
+ ( + + Nom * + + + + + + )} + /> + ( + + Prénom * + + + + + + )} + /> + ( + + Date de naissance * + + + + + + )} + /> + ( + + Lieu de naissance * + + + + + + )} + /> + ( + + Mode de paiement * + + + + )} + /> +
+
+ + {/* Section Identité */} +
+
+ + Pièce d'Identité +
+
+ ( + + Type d'identité * + + + + )} + /> + ( + + Numéro * + + + + + + )} + /> + ( + + Autorité de délivrance + + + + + + )} + /> + ( + + Date de délivrance * + + + + + + )} + /> +
+ + {/* Section Photos */} +
+

Photos des documents

+
+ {/* Photo Recto */} +
+ +
+ + {form.watch('photoRecto') && ( + + )} +
+
+ + {/* Photo Verso */} +
+ +
+ + {form.watch('photoVerso') && ( + + )} +
+
+ + {/* Photo Ticket */} +
+ +
+ + {form.watch('photoTicket') && ( + + )} +
+
+
+
+
+ + {/* Bouton de soumission */} +
+ +
+
+ +
+ + {/* Modal Camera */} + { + setIsCameraOpen(false); + setCurrentPhotoType(null); + }} + onPhotoTaken={handlePhotoTaken} + title={`Capturer la photo ${currentPhotoType}`} + /> +
+ ); +} \ No newline at end of file diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx index 8540cdf..1f992de 100644 --- a/client/src/pages/Dashboard.tsx +++ b/client/src/pages/Dashboard.tsx @@ -5,7 +5,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useAuth } from "@/hooks/useAuth"; import { StoreProvider, useStoreContext } from "@/hooks/useStoreContext"; -import NewSaleForm from "@/components/NewSaleForm"; +import NewSaleFormMultiProduct from "@/components/NewSaleFormMultiProduct"; import SalesHistory from "@/components/SalesHistory"; import Administration from "@/components/Administration"; import { Package, History, Settings, LogOut, User, Building2, Shield } from "lucide-react"; @@ -146,7 +146,7 @@ function DashboardContent() {
- +