mirror of
https://github.com/R0m1k3/Regisflow.git
synced 2026-10-23 23:28:18 +02:00
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
This commit is contained in:
1 parent
97c6f3ec1e
commit
a6a188e68f
3 files changed
+248
-24
No files matched your search
@@ -9,12 +9,12 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
|
|||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { useStoreContext } from '@/hooks/useStoreContext';
|
import { useStoreContext } from '@/hooks/useStoreContext';
|
||||||
import { useCamera } from '@/hooks/useCamera';
|
import { useSimpleCamera } from '@/hooks/useSimpleCamera';
|
||||||
import { useCameraTest } from '@/hooks/useCameraTest';
|
import { useCameraTest } from '@/hooks/useCameraTest';
|
||||||
import { apiRequest } from '@/lib/queryClient';
|
import { apiRequest } from '@/lib/queryClient';
|
||||||
import { validateRequiredFields, validateEAN13, ARTICLE_CATEGORY_MAPPING, IDENTITY_TYPES, PAYMENT_METHODS } from '@/lib/validation';
|
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 { 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';
|
import type { PhotoType } from '@/types/sale';
|
||||||
|
|
||||||
interface FormData {
|
interface FormData {
|
||||||
@@ -54,7 +54,7 @@ export default function NewSaleForm() {
|
|||||||
startCamera,
|
startCamera,
|
||||||
capturePhoto,
|
capturePhoto,
|
||||||
stopCamera
|
stopCamera
|
||||||
} = useCamera();
|
} = useSimpleCamera();
|
||||||
|
|
||||||
// Camera test functionality
|
// Camera test functionality
|
||||||
const { testResults, runCameraTest } = useCameraTest();
|
const { testResults, runCameraTest } = useCameraTest();
|
||||||
@@ -216,27 +216,9 @@ export default function NewSaleForm() {
|
|||||||
description: "Préparation de la caméra en cours...",
|
description: "Préparation de la caméra en cours...",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Démarrer la caméra
|
// Démarrer la caméra simple
|
||||||
await startCamera(photoType);
|
await startCamera(photoType);
|
||||||
console.log('Camera started successfully');
|
console.log('Simple 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');
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Camera start failed:', error);
|
console.error('Camera start failed:', error);
|
||||||
@@ -770,7 +752,7 @@ export default function NewSaleForm() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Camera Modal */}
|
{/* Camera Modal */}
|
||||||
<CameraModal
|
<SimpleCameraModal
|
||||||
isOpen={isCameraOpen}
|
isOpen={isCameraOpen}
|
||||||
photoType={currentPhotoType}
|
photoType={currentPhotoType}
|
||||||
videoRef={videoRef}
|
videoRef={videoRef}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Camera, X } from 'lucide-react';
|
||||||
|
import { PhotoType } from '@/types/sale';
|
||||||
|
|
||||||
|
interface SimpleCameraModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
photoType: PhotoType | null;
|
||||||
|
videoRef: React.RefObject<HTMLVideoElement>;
|
||||||
|
canvasRef: React.RefObject<HTMLCanvasElement>;
|
||||||
|
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 (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-2xl w-full mx-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-center">
|
||||||
|
Capture photo {photoType ? photoType.charAt(0).toUpperCase() + photoType.slice(1) : ''}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="relative bg-black rounded-lg overflow-hidden">
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
autoPlay
|
||||||
|
playsInline
|
||||||
|
muted
|
||||||
|
className="w-full h-auto max-h-96"
|
||||||
|
style={{
|
||||||
|
minHeight: '200px',
|
||||||
|
objectFit: 'cover'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<canvas ref={canvasRef} className="hidden" />
|
||||||
|
|
||||||
|
{/* Overlay pour cadrer */}
|
||||||
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
|
<div className="absolute inset-4 border-2 border-white border-dashed rounded-lg opacity-80"></div>
|
||||||
|
<div className="absolute bottom-4 left-4 text-white text-sm bg-black bg-opacity-75 px-3 py-2 rounded">
|
||||||
|
Cadrez votre pièce d'identité dans le rectangle
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-4 right-4 text-white text-sm bg-black bg-opacity-75 px-3 py-2 rounded">
|
||||||
|
{photoType === 'recto' ? 'RECTO' : 'VERSO'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center space-x-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onClose}
|
||||||
|
className="flex-1 max-w-32"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4 mr-2" />
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={onCapture}
|
||||||
|
disabled={isCapturing}
|
||||||
|
className="bg-green-600 hover:bg-green-700 text-white flex-1 max-w-40"
|
||||||
|
>
|
||||||
|
<Camera className="h-4 w-4 mr-2" />
|
||||||
|
{isCapturing ? 'Capture...' : 'Capturer'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<PhotoType | null>(null);
|
||||||
|
const [isCapturing, setIsCapturing] = useState(false);
|
||||||
|
const [stream, setStream] = useState<MediaStream | null>(null);
|
||||||
|
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
const startCamera = useCallback(async (photoType: PhotoType): Promise<void> => {
|
||||||
|
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<string> => {
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in new issue
Block a user