From a6a188e68feca3c0f0e57541360954a1b5cdb25e Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Fri, 11 Jul 2025 18:02:54 +0000 Subject: [PATCH] Implement basic camera feature to allow users to capture identity photos Replaces `useCamera` with `useSimpleCamera` and introduces `SimpleCameraModal` for capturing photos. Replit-Commit-Author: Agent Replit-Commit-Session-Id: dec54137-d5a2-4406-8ad4-5f2f9fb110bf Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0715008c-7244-40f4-befc-26c014852236/9ada347d-ab83-40f0-9a0e-5fa4d829189e.jpg --- client/src/components/NewSaleForm.tsx | 30 +---- client/src/components/SimpleCameraModal.tsx | 109 ++++++++++++++++ client/src/hooks/useSimpleCamera.tsx | 133 ++++++++++++++++++++ 3 files changed, 248 insertions(+), 24 deletions(-) create mode 100644 client/src/components/SimpleCameraModal.tsx create mode 100644 client/src/hooks/useSimpleCamera.tsx diff --git a/client/src/components/NewSaleForm.tsx b/client/src/components/NewSaleForm.tsx index 4444622..c5f5f4d 100644 --- a/client/src/components/NewSaleForm.tsx +++ b/client/src/components/NewSaleForm.tsx @@ -9,12 +9,12 @@ import { Alert, AlertDescription } from '@/components/ui/alert'; import { useToast } from '@/hooks/use-toast'; import { useAuth } from '@/hooks/useAuth'; import { useStoreContext } from '@/hooks/useStoreContext'; -import { useCamera } from '@/hooks/useCamera'; +import { useSimpleCamera } from '@/hooks/useSimpleCamera'; import { useCameraTest } from '@/hooks/useCameraTest'; 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, TestTube, Upload, Trash2 } from 'lucide-react'; -import CameraModal from '@/components/CameraModal'; +import SimpleCameraModal from '@/components/SimpleCameraModal'; import type { PhotoType } from '@/types/sale'; interface FormData { @@ -54,7 +54,7 @@ export default function NewSaleForm() { startCamera, capturePhoto, stopCamera - } = useCamera(); + } = useSimpleCamera(); // Camera test functionality const { testResults, runCameraTest } = useCameraTest(); @@ -216,27 +216,9 @@ export default function NewSaleForm() { description: "Préparation de la caméra en cours...", }); - // Démarrer la caméra + // Démarrer la caméra simple await startCamera(photoType); - console.log('Camera started successfully'); - - // Attendre 2 secondes supplémentaires pour s'assurer que la vidéo est prête - console.log('Waiting for video to be fully ready...'); - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Vérifier l'état final de la vidéo - if (videoRef.current) { - const video = videoRef.current; - console.log('Final video state check:', { - videoWidth: video.videoWidth, - videoHeight: video.videoHeight, - readyState: video.readyState, - paused: video.paused, - currentTime: video.currentTime - }); - } - - console.log('Camera should be ready for capture'); + console.log('Simple camera started successfully'); } catch (error) { console.error('Camera start failed:', error); @@ -770,7 +752,7 @@ export default function NewSaleForm() { {/* Camera Modal */} - ; + canvasRef: React.RefObject; + isCapturing: boolean; + onClose: () => void; + onCapture: () => void; +} + +export default function SimpleCameraModal({ + isOpen, + photoType, + videoRef, + canvasRef, + isCapturing, + onClose, + onCapture +}: SimpleCameraModalProps) { + + useEffect(() => { + console.log('SimpleCameraModal - isOpen:', isOpen); + if (isOpen && videoRef.current) { + console.log('SimpleCameraModal - Video element available'); + const video = videoRef.current; + + // Événements de diagnostic + video.onloadedmetadata = () => { + console.log('SimpleCameraModal - Video metadata loaded:', video.videoWidth, 'x', video.videoHeight); + }; + + video.onplay = () => { + console.log('SimpleCameraModal - Video started playing'); + }; + + video.onerror = (e) => { + console.error('SimpleCameraModal - Video error:', e); + }; + } + }, [isOpen, videoRef]); + + if (!isOpen) return null; + + return ( + + + + + Capture photo {photoType ? photoType.charAt(0).toUpperCase() + photoType.slice(1) : ''} + + + +
+
+
+ +
+ + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/hooks/useSimpleCamera.tsx b/client/src/hooks/useSimpleCamera.tsx new file mode 100644 index 0000000..6a15264 --- /dev/null +++ b/client/src/hooks/useSimpleCamera.tsx @@ -0,0 +1,133 @@ +import { useState, useRef, useCallback } from 'react'; +import { PhotoType } from '@/types/sale'; + +export function useSimpleCamera() { + const [isOpen, setIsOpen] = useState(false); + const [currentPhotoType, setCurrentPhotoType] = useState(null); + const [isCapturing, setIsCapturing] = useState(false); + const [stream, setStream] = useState(null); + + const videoRef = useRef(null); + const canvasRef = useRef(null); + + const startCamera = useCallback(async (photoType: PhotoType): Promise => { + console.log('=== SIMPLE CAMERA START ==='); + + try { + // Nettoyer le stream précédent s'il existe + if (stream) { + stream.getTracks().forEach(track => track.stop()); + } + + // Demander l'accès à la caméra + const mediaStream = await navigator.mediaDevices.getUserMedia({ + video: { + width: { ideal: 1280 }, + height: { ideal: 720 }, + facingMode: 'environment' + } + }); + + console.log('Media stream obtained:', mediaStream); + setStream(mediaStream); + setCurrentPhotoType(photoType); + setIsOpen(true); + + // Attendre que la modal soit ouverte et l'élément vidéo soit prêt + setTimeout(() => { + if (videoRef.current && mediaStream) { + const video = videoRef.current; + console.log('Assigning stream to video element'); + + video.srcObject = mediaStream; + video.autoplay = true; + video.playsInline = true; + video.muted = true; + + // Forcer le play + video.play().then(() => { + console.log('Video playing successfully'); + }).catch(error => { + console.error('Video play failed:', error); + }); + } + }, 100); + + } catch (error) { + console.error('Camera start failed:', error); + throw error; + } + }, [stream]); + + const capturePhoto = useCallback(async (): Promise => { + console.log('=== SIMPLE CAMERA CAPTURE ==='); + + if (!videoRef.current || !canvasRef.current) { + throw new Error('Video ou canvas non disponible'); + } + + const video = videoRef.current; + const canvas = canvasRef.current; + const context = canvas.getContext('2d'); + + if (!context) { + throw new Error('Contexte canvas non disponible'); + } + + // Vérifier que la vidéo a des dimensions + if (video.videoWidth === 0 || video.videoHeight === 0) { + throw new Error('Vidéo non prête - dimensions non disponibles'); + } + + setIsCapturing(true); + + try { + // Configurer le canvas aux dimensions de la vidéo + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + // Dessiner l'image de la vidéo sur le canvas + context.drawImage(video, 0, 0, canvas.width, canvas.height); + + // Convertir en base64 + const photoData = canvas.toDataURL('image/jpeg', 0.8); + + console.log('Photo captured successfully, size:', photoData.length); + return photoData; + + } finally { + setIsCapturing(false); + } + }, []); + + const stopCamera = useCallback(() => { + console.log('=== SIMPLE CAMERA STOP ==='); + + if (stream) { + stream.getTracks().forEach(track => { + track.stop(); + console.log('Track stopped:', track.kind); + }); + setStream(null); + } + + if (videoRef.current) { + videoRef.current.srcObject = null; + } + + setIsOpen(false); + setCurrentPhotoType(null); + setIsCapturing(false); + }, [stream]); + + return { + isOpen, + currentPhotoType, + isCapturing, + videoRef, + canvasRef, + startCamera, + capturePhoto, + stopCamera + }; +} \ No newline at end of file