diff --git a/client/src/components/NewSaleForm.tsx b/client/src/components/NewSaleForm.tsx index 3e96abb..22a7d4d 100644 --- a/client/src/components/NewSaleForm.tsx +++ b/client/src/components/NewSaleForm.tsx @@ -10,9 +10,10 @@ import { useToast } from '@/hooks/use-toast'; import { useAuth } from '@/hooks/useAuth'; import { useStoreContext } from '@/hooks/useStoreContext'; import { useCamera } from '@/hooks/useCamera'; +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 } from 'lucide-react'; +import { Package, User, Users, Save, Calendar, BadgeCheck, Info, Camera, TestTube, Upload, Trash2 } from 'lucide-react'; import CameraModal from '@/components/CameraModal'; import type { PhotoType } from '@/types/sale'; @@ -55,6 +56,9 @@ export default function NewSaleForm() { stopCamera } = useCamera(); + // Camera test functionality + const { testResults, runCameraTest } = useCameraTest(); + const form = useForm({ defaultValues: { vendeur: '', @@ -180,21 +184,36 @@ export default function NewSaleForm() { // Photo capture functions const handlePhotoCapture = async (photoType: PhotoType) => { - // Vérifier d'abord si les caméras sont supportées + console.log('Photo capture requested for:', photoType); + + // Vérifications préliminaires if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { toast({ - title: "Caméra non supportée", - description: "Votre navigateur ne supporte pas l'accès aux caméras", + 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...'); await startCamera(photoType); + console.log('Camera started successfully'); } catch (error) { + console.error('Camera start failed:', error); toast({ - title: "Erreur caméra", + title: "Erreur d'accès à la caméra", description: error instanceof Error ? error.message : "Impossible d'accéder à la caméra", variant: "destructive", }); @@ -233,7 +252,69 @@ export default function NewSaleForm() { toast({ title: "Photo supprimée", description: `Photo ${photoType} supprimée`, - variant: "success", + }); + }; + + // File upload fallback + const handleFileUpload = (photoType: PhotoType, event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + if (!file.type.startsWith('image/')) { + toast({ + title: "Type de fichier invalide", + description: "Veuillez sélectionner une image", + variant: "destructive", + }); + return; + } + + if (file.size > 5 * 1024 * 1024) { // 5MB limit + toast({ + title: "Fichier trop volumineux", + description: "La taille maximale est de 5MB", + variant: "destructive", + }); + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + const result = e.target?.result as string; + form.setValue(photoType === 'recto' ? 'photoRecto' : 'photoVerso', result); + toast({ + title: "Photo ajoutée", + description: `Photo ${photoType} ajoutée avec succès`, + }); + }; + 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", }); }; @@ -519,7 +600,19 @@ export default function NewSaleForm() { {/* Section Photos */}
-

Photos de la pièce d'identité

+
+

Photos de la pièce d'identité

+ +
+
{/* Photo Recto */}
@@ -538,19 +631,37 @@ export default function NewSaleForm() { className="absolute top-2 right-2" onClick={() => handleRemovePhoto('recto')} > - ✕ +
) : ( - +
+ +
+ handleFileUpload('recto', e)} + /> + +
+
)}
@@ -571,19 +682,37 @@ export default function NewSaleForm() { className="absolute top-2 right-2" onClick={() => handleRemovePhoto('verso')} > - ✕ +
) : ( - +
+ +
+ handleFileUpload('verso', e)} + /> + +
+
)} diff --git a/client/src/hooks/useCamera.tsx b/client/src/hooks/useCamera.tsx index f14bd96..a45b2ae 100644 --- a/client/src/hooks/useCamera.tsx +++ b/client/src/hooks/useCamera.tsx @@ -11,59 +11,43 @@ export function useCamera() { const streamRef = useRef(null); const startCamera = useCallback(async (photoType: PhotoType) => { + console.log('Starting camera for photoType:', photoType); + + // Vérifications de base + if (!navigator.mediaDevices) { + throw new Error('Votre navigateur ne supporte pas l\'accès aux médias. Utilisez Chrome, Firefox ou Safari récent.'); + } + + if (!navigator.mediaDevices.getUserMedia) { + throw new Error('getUserMedia n\'est pas supporté par votre navigateur.'); + } + try { - // Vérification des permissions avant d'essayer - const devices = await navigator.mediaDevices.enumerateDevices(); - const videoDevices = devices.filter(device => device.kind === 'videoinput'); - console.log('Available video devices:', videoDevices.length); + console.log('Testing basic camera access...'); - if (videoDevices.length === 0) { - throw new Error('Aucune caméra détectée sur cet appareil'); - } - - // Configuration avancée pour tablettes et appareils mobiles - const constraints = { - video: { - facingMode: 'environment', // Caméra arrière par défaut - width: { ideal: 1280, max: 1920 }, // Résolution optimale - height: { ideal: 720, max: 1080 }, - aspectRatio: { ideal: 16/9 }, - frameRate: { ideal: 30 } - } - }; - + // Test simple d'abord - juste video: true let stream: MediaStream; try { - // Essai avec caméra arrière - stream = await navigator.mediaDevices.getUserMedia(constraints); - console.log('Back camera started successfully'); - } catch (backCameraError) { - console.warn('Caméra arrière non disponible, essai avec caméra avant:', backCameraError); + stream = await navigator.mediaDevices.getUserMedia({ video: true }); + console.log('Basic camera access successful'); + } catch (basicError) { + console.error('Basic camera access failed:', basicError); - // Fallback vers caméra avant si arrière non disponible - const frontConstraints = { - video: { - facingMode: 'user', - width: { ideal: 1280, max: 1920 }, - height: { ideal: 720, max: 1080 }, - aspectRatio: { ideal: 16/9 }, - frameRate: { ideal: 30 } + // Vérifier le type d'erreur + if (basicError instanceof Error) { + if (basicError.name === 'NotAllowedError') { + throw new Error('Permission d\'accès à la caméra refusée. Autorisez l\'accès dans votre navigateur.'); + } else if (basicError.name === 'NotFoundError') { + throw new Error('Aucune caméra trouvée sur cet appareil.'); + } else if (basicError.name === 'NotReadableError') { + throw new Error('Caméra déjà utilisée par une autre application.'); + } else if (basicError.name === 'OverconstrainedError') { + throw new Error('Contraintes de caméra non supportées.'); } - }; - - try { - stream = await navigator.mediaDevices.getUserMedia(frontConstraints); - console.log('Front camera started successfully'); - } catch (frontCameraError) { - console.warn('Caméra avant non disponible, essai basique:', frontCameraError); - - // Fallback minimal pour compatibilité maximale - stream = await navigator.mediaDevices.getUserMedia({ - video: true - }); - console.log('Basic camera started successfully'); } + + throw new Error(`Erreur d'accès à la caméra: ${basicError}`); } streamRef.current = stream; @@ -72,20 +56,45 @@ export function useCamera() { if (videoRef.current) { videoRef.current.srcObject = stream; + console.log('Video stream attached to video element'); // Attendre que la vidéo soit prête - await new Promise((resolve) => { - videoRef.current!.onloadedmetadata = () => { - console.log('Video metadata loaded'); - resolve(); + return new Promise((resolve, reject) => { + const video = videoRef.current!; + + const handleLoadedMetadata = () => { + console.log('Video metadata loaded, dimensions:', video.videoWidth, 'x', video.videoHeight); + video.removeEventListener('loadedmetadata', handleLoadedMetadata); + video.removeEventListener('error', handleError); + resolve(true); }; + + const handleError = (error: Event) => { + console.error('Video error:', error); + video.removeEventListener('loadedmetadata', handleLoadedMetadata); + video.removeEventListener('error', handleError); + reject(new Error('Erreur lors du chargement de la vidéo')); + }; + + video.addEventListener('loadedmetadata', handleLoadedMetadata); + video.addEventListener('error', handleError); + + // Timeout de sécurité + setTimeout(() => { + video.removeEventListener('loadedmetadata', handleLoadedMetadata); + video.removeEventListener('error', handleError); + resolve(true); // Continuer même si metadata pas encore chargé + }, 5000); }); } return true; } catch (error) { - console.error('Camera error:', error); - throw new Error('Impossible d\'accéder à la caméra. Vérifiez les permissions et la disponibilité des caméras.'); + console.error('Camera start error:', error); + if (error instanceof Error) { + throw error; + } + throw new Error('Erreur inconnue lors de l\'accès à la caméra'); } }, []); diff --git a/client/src/hooks/useCameraTest.tsx b/client/src/hooks/useCameraTest.tsx new file mode 100644 index 0000000..d946263 --- /dev/null +++ b/client/src/hooks/useCameraTest.tsx @@ -0,0 +1,81 @@ +import { useState, useCallback } from 'react'; + +export function useCameraTest() { + const [testResults, setTestResults] = useState<{ + hasMediaDevices: boolean; + hasGetUserMedia: boolean; + hasEnumerateDevices: boolean; + videoDevicesCount: number; + permissions: string; + error?: string; + } | null>(null); + + const runCameraTest = useCallback(async () => { + const results = { + hasMediaDevices: !!navigator.mediaDevices, + hasGetUserMedia: !!(navigator.mediaDevices?.getUserMedia), + hasEnumerateDevices: !!(navigator.mediaDevices?.enumerateDevices), + videoDevicesCount: 0, + permissions: 'unknown' as string, + error: undefined as string | undefined + }; + + try { + // Test 1: Check basic API availability + if (!navigator.mediaDevices) { + results.error = 'navigator.mediaDevices not available'; + setTestResults(results); + return results; + } + + if (!navigator.mediaDevices.getUserMedia) { + results.error = 'getUserMedia not available'; + setTestResults(results); + return results; + } + + // Test 2: Check permissions + try { + const permission = await navigator.permissions?.query({ name: 'camera' as PermissionName }); + results.permissions = permission?.state || 'unknown'; + } catch (e) { + results.permissions = 'query_failed'; + } + + // Test 3: Enumerate devices + if (navigator.mediaDevices.enumerateDevices) { + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + const videoDevices = devices.filter(device => device.kind === 'videoinput'); + results.videoDevicesCount = videoDevices.length; + + if (videoDevices.length === 0) { + results.error = 'No video devices found'; + } + } catch (e) { + results.error = `Device enumeration failed: ${e}`; + } + } + + // Test 4: Try basic camera access + try { + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + stream.getTracks().forEach(track => track.stop()); // Clean up immediately + results.error = undefined; // Camera works! + } catch (e: any) { + results.error = `Camera access failed: ${e.name} - ${e.message}`; + } + + } catch (e: any) { + results.error = `Test failed: ${e.message}`; + } + + setTestResults(results); + return results; + }, []); + + return { + testResults, + runCameraTest + }; +} \ No newline at end of file diff --git a/replit.md b/replit.md index 33a588c..7da604d 100644 --- a/replit.md +++ b/replit.md @@ -259,6 +259,11 @@ Preferred communication style: Simple, everyday language. - ✅ Enhanced error handling with device-specific camera availability checks - ✅ Camera permission detection and clear error messages for troubleshooting - ✅ Support for both front and rear cameras on tablets and mobile devices +- ✅ Camera diagnostic system with detailed test functionality +- ✅ File upload fallback when camera access fails +- ✅ Comprehensive error handling with specific messages for different failure types +- ✅ HTTPS requirement detection for secure camera access +- ✅ Multiple capture methods: camera → file upload for maximum compatibility ### Production Deployment System (January 11, 2025) - ✅ Complete production deployment configuration with Docker Compose