From d1f3fced74fd9b8b8cce5f05f6f1a6e12dbfc0ea Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Sat, 19 Jul 2025 13:05:14 +0000 Subject: [PATCH] Improve photo capture and product entry in sales form Refactors CameraModal and NewSaleForm components to streamline photo capture and product entry using React hooks and react-hook-form. Replit-Commit-Author: Agent Replit-Commit-Session-Id: c7151ec8-4e6b-4fcf-b628-65b094ac18fd Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0715008c-7244-40f4-befc-26c014852236/c7151ec8-4e6b-4fcf-b628-65b094ac18fd/URbRU29 --- client/src/components/CameraModal.tsx | 198 ++++++----- client/src/components/NewSaleForm.tsx | 493 +++++++++++--------------- 2 files changed, 311 insertions(+), 380 deletions(-) diff --git a/client/src/components/CameraModal.tsx b/client/src/components/CameraModal.tsx index 46ba93a..e9b4b1c 100644 --- a/client/src/components/CameraModal.tsx +++ b/client/src/components/CameraModal.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useState, useRef, useEffect } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Camera, X } from 'lucide-react'; @@ -7,78 +7,113 @@ import { PhotoType } from '@/types/sale'; interface CameraModalProps { isOpen: boolean; photoType: PhotoType | null; - videoRef: React.RefObject; - canvasRef: React.RefObject; - isCapturing: boolean; onClose: () => void; - onCapture: () => void; + onPhotoTaken: (photoData: string) => void; } export default function CameraModal({ isOpen, photoType, - videoRef, - canvasRef, - isCapturing, onClose, - onCapture + onPhotoTaken }: CameraModalProps) { + const videoRef = useRef(null); + const canvasRef = useRef(null); + const [stream, setStream] = useState(null); + const [isCapturing, setIsCapturing] = useState(false); + useEffect(() => { - if (isOpen && videoRef.current) { - const video = videoRef.current; - console.log('CameraModal useEffect - video element ready'); - - const handleLoadedMetadata = () => { - console.log('CameraModal: Video metadata loaded, dimensions:', video.videoWidth, 'x', video.videoHeight); - }; - - const handleCanPlay = () => { - console.log('CameraModal: Video can play, readyState:', video.readyState); - }; - - const handleError = (error: Event) => { - console.error('CameraModal: Video error event:', error); - }; - - const handlePlay = () => { - console.log('CameraModal: Video started playing'); - }; - - video.addEventListener('loadedmetadata', handleLoadedMetadata); - video.addEventListener('canplay', handleCanPlay); - video.addEventListener('error', handleError); - video.addEventListener('play', handlePlay); - - // Forcer le play avec gestion d'erreur - const startPlayback = async () => { - try { - await video.play(); - console.log('CameraModal: Video play() successful'); - } catch (playError) { - console.error('CameraModal: Video play() failed:', playError); - } - }; - - startPlayback(); - - return () => { - video.removeEventListener('loadedmetadata', handleLoadedMetadata); - video.removeEventListener('canplay', handleCanPlay); - video.removeEventListener('error', handleError); - video.removeEventListener('play', handlePlay); - }; + if (isOpen) { + startCamera(); + } else { + stopCamera(); } - }, [isOpen, videoRef]); + + return () => { + stopCamera(); + }; + }, [isOpen]); + + const startCamera = async () => { + try { + const mediaStream = await navigator.mediaDevices.getUserMedia({ + video: { + facingMode: 'environment', + width: { ideal: 1280 }, + height: { ideal: 720 } + } + }); + + if (videoRef.current) { + videoRef.current.srcObject = mediaStream; + setStream(mediaStream); + } + } catch (error) { + console.error('Error accessing camera:', error); + // Fallback: try without specific constraints + try { + const mediaStream = await navigator.mediaDevices.getUserMedia({ + video: true + }); + + if (videoRef.current) { + videoRef.current.srcObject = mediaStream; + setStream(mediaStream); + } + } catch (fallbackError) { + console.error('Fallback camera access failed:', fallbackError); + } + } + }; + + const stopCamera = () => { + if (stream) { + stream.getTracks().forEach(track => track.stop()); + setStream(null); + } + }; + + const capturePhoto = () => { + if (!videoRef.current || !canvasRef.current) return; + + setIsCapturing(true); + const video = videoRef.current; + const canvas = canvasRef.current; + const context = canvas.getContext('2d'); + + if (!context) return; + + // Set canvas dimensions to match video + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + // Draw video frame to canvas + context.drawImage(video, 0, 0, canvas.width, canvas.height); + + // Convert to data URL + const photoData = canvas.toDataURL('image/jpeg', 0.8); + + onPhotoTaken(photoData); + setIsCapturing(false); + }; + + const handleClose = () => { + stopCamera(); + onClose(); + }; return ( - + - - Photo {photoType ? photoType.charAt(0).toUpperCase() + photoType.slice(1) : ''} + + Capturer photo {photoType} + - +
- -
- - +
); -} +} \ No newline at end of file diff --git a/client/src/components/NewSaleForm.tsx b/client/src/components/NewSaleForm.tsx index 60ce8ca..2d20a5e 100644 --- a/client/src/components/NewSaleForm.tsx +++ b/client/src/components/NewSaleForm.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { useForm } from 'react-hook-form'; +import { useForm, useFieldArray } from 'react-hook-form'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -9,11 +9,10 @@ import { Alert, AlertDescription } from '@/components/ui/alert'; import { useToast } from '@/hooks/use-toast'; import { useAuth } from '@/hooks/useAuth'; import { useStoreContext } from '@/hooks/useStoreContext'; -import { useSimpleCamera } from '@/hooks/useSimpleCamera'; import { apiRequest } from '@/lib/queryClient'; import { validateRequiredFields, validateEAN13, ARTICLE_CATEGORY_MAPPING, IDENTITY_TYPES, PAYMENT_METHODS } from '@/lib/validation'; -import { Package, User, Users, Save, Calendar, BadgeCheck, Info, Camera, Upload, Trash2 } from 'lucide-react'; +import { Package, User, Users, Save, Calendar, BadgeCheck, Info, Camera, Upload, Trash2, Plus, X } from 'lucide-react'; import CameraModal from '@/components/CameraModal'; import type { PhotoType } from '@/types/sale'; @@ -49,22 +48,14 @@ export default function NewSaleForm() { const { selectedStoreId } = useStoreContext(); const [isSubmitting, setIsSubmitting] = useState(false); - // Camera functionality - const { - isOpen: isCameraOpen, - currentPhotoType, - isCapturing, - videoRef, - canvasRef, - startCamera, - capturePhoto, - stopCamera - } = useSimpleCamera(); + // Camera state + const [isCameraOpen, setIsCameraOpen] = useState(false); + const [currentPhotoType, setCurrentPhotoType] = useState(null); const form = useForm({ defaultValues: { vendeur: '', - products: [{ typeArticle: '', categorie: '', quantite: '', gencode: '' }], + products: [{ typeArticle: '', categorie: '', quantite: '1', gencode: '' }], nom: '', prenom: '', dateNaissance: '', @@ -80,20 +71,10 @@ export default function NewSaleForm() { } }); - // Gestion des produits multiples - const products = form.watch('products'); - - const addProduct = () => { - const currentProducts = form.getValues('products'); - form.setValue('products', [...currentProducts, { typeArticle: '', categorie: '', quantite: '', gencode: '' }]); - }; - - const removeProduct = (index: number) => { - const currentProducts = form.getValues('products'); - if (currentProducts.length > 1) { - form.setValue('products', currentProducts.filter((_, i) => i !== index)); - } - }; + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "products" + }); const createSaleMutation = useMutation({ mutationFn: async (saleData: any) => { @@ -117,7 +98,7 @@ export default function NewSaleForm() { }); form.reset({ vendeur: '', - products: [{ typeArticle: '', categorie: '', quantite: '', gencode: '' }], + products: [{ typeArticle: '', categorie: '', quantite: '1', gencode: '' }], nom: '', prenom: '', dateNaissance: '', @@ -198,84 +179,28 @@ export default function NewSaleForm() { }; // Photo capture functions - const handlePhotoCapture = async (photoType: PhotoType) => { - console.log('=== PHOTO CAPTURE REQUESTED ==='); - console.log('Photo type:', photoType); - - // Vérifications préliminaires - if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { - toast({ - title: "Navigateur non compatible", - description: "Votre navigateur ne supporte pas la capture photo. Utilisez Chrome, Firefox ou Safari récent.", - variant: "destructive", - }); - return; - } - - // Vérifier si nous sommes en HTTPS (requis par certains navigateurs) - if (location.protocol !== 'https:' && location.hostname !== 'localhost') { - toast({ - title: "Connexion sécurisée requise", - description: "L'accès à la caméra nécessite une connexion HTTPS.", - variant: "destructive", - }); - return; - } - - try { - console.log('Attempting to start camera...'); - - // Afficher toast de démarrage - toast({ - title: "Démarrage de la caméra", - description: "Préparation de la caméra en cours...", - }); - - // Démarrer la caméra simple - await startCamera(photoType); - console.log('Simple camera started successfully'); - - } catch (error) { - console.error('Camera start failed:', error); - toast({ - title: "Erreur d'accès à la caméra", - description: error instanceof Error ? error.message : "Impossible d'accéder à la caméra", - variant: "destructive", - }); - } + const handlePhotoCapture = (photoType: PhotoType) => { + setCurrentPhotoType(photoType); + setIsCameraOpen(true); }; - const handleCapture = async () => { - if (!currentPhotoType) return; - - try { - console.log('Attempting to capture photo...'); - const photoData = await capturePhoto(); - console.log('Photo captured successfully, data length:', photoData.length); - - 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`, - variant: "success", - }); - - stopCamera(); - } catch (error) { - console.error('Photo capture error:', error); - toast({ - title: "Erreur de capture", - description: error instanceof Error ? error.message : "Erreur lors de la capture", - variant: "destructive", - }); + 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`, + variant: "success", + }); + + setIsCameraOpen(false); + setCurrentPhotoType(null); }; const handleRemovePhoto = (photoType: PhotoType) => { @@ -328,236 +253,222 @@ export default function NewSaleForm() { toast({ title: "Photo ajoutée", description: `Photo ${photoType} ajoutée avec succès`, + variant: "success", }); }; reader.readAsDataURL(file); }; - // Camera test function - const handleCameraTest = async () => { - toast({ - title: "Test en cours...", - description: "Vérification de l'accès aux caméras", - }); - - const results = await runCameraTest(); - - let message = `Test de caméra terminé:\n`; - message += `✓ Navigateur compatible: ${results.hasMediaDevices ? 'Oui' : 'Non'}\n`; - message += `✓ getUserMedia disponible: ${results.hasGetUserMedia ? 'Oui' : 'Non'}\n`; - message += `✓ Caméras détectées: ${results.videoDevicesCount}\n`; - message += `✓ Permissions: ${results.permissions}\n`; - - if (results.error) { - message += `❌ Erreur: ${results.error}`; - } else { - message += `✅ Caméra fonctionnelle`; - } - - toast({ - title: results.error ? "Test échoué" : "Test réussi", - description: message, - variant: results.error ? "destructive" : "default", - }); - }; - return ( -
-
+
+
- + {/* Section Vendeur */} -
-
- - Informations Vendeur -
-
- ( - - Vendeur * - - - - - - )} - /> +
+
+ +

Informations Vendeur

+ + ( + + Nom du vendeur * + + + + + + )} + />
- {/* Section Produit */} -
-
- - Informations Produit -
-
- ( - - Type d'article * - - - - )} - /> - ( - - Catégorie réglementaire * - - - - )} - /> - ( - - Quantité * - - - - - - )} - /> - ( - - Gencode (EAN13) * - - - - - - )} - /> + {/* Section Produits */} +
+
+
+ +

Produits

+
+
- - - - 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 - - +
+ {fields.map((field, index) => ( +
+ ( + + Type d'article * + + + + + + )} + /> + + ( + + Catégorie * + + + + )} + /> + + ( + + Quantité * + + + + + + )} + /> + + ( + + Gencode EAN13 * + + + + + + )} + /> + +
+ {fields.length > 1 && ( + + )} +
+
+ ))} +
{/* Section Client */} -
-
- - Informations Client +
+
+ +

Informations Client

-
+ +
( - Nom de famille * + Nom * - + )} /> + ( - Prénom * + Prénom * - + )} /> + ( - Date de naissance * + Date de naissance * - + )} /> + ( - Lieu de naissance * + Lieu de naissance * - + )} /> + ( - Mode de paiement * - - + {PAYMENT_METHODS.map((method) => ( - - {method.label} + + {method} ))} @@ -570,28 +481,29 @@ export default function NewSaleForm() {
{/* Section Identité */} -
-
- - Pièce d'Identité +
+
+ +

Pièce d'Identité

-
+ +
( - Type de pièce d'identité * - - + {IDENTITY_TYPES.map((type) => ( - - {type.label} + + {type} ))} @@ -600,19 +512,21 @@ export default function NewSaleForm() { )} /> + ( - Numéro de pièce d'identité * + Numéro de pièce d'identité * - + )} /> + Autorité de délivrance - + )} /> + ( - Date de délivrance * + Date de délivrance * - + )} />
- - {/* Section Photos */} + + {/* Photos pièce d'identité */}

Photos de la pièce d'identité

-
+
{/* Photo Recto */}
@@ -656,7 +571,7 @@ export default function NewSaleForm() { Photo recto
);