import { useEffect, useRef, useState } from 'react'; import { BrowserQRCodeReader } from '@zxing/browser'; import { timeApi } from '../api/time'; import { useAuth } from '../context/useAuth'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Badge } from '@/components/ui/badge'; import ConfirmDialog from '@/components/ConfirmDialog'; const TimeClockPage = () => { const videoRef = useRef(null); const streamRef = useRef(null); const scanFrameRef = useRef(null); const qrControlsRef = useRef(null); const { isAdmin } = useAuth(); const [isScanning, setIsScanning] = useState(false); const [scanMessage, setScanMessage] = useState(''); const [scanConflict, setScanConflict] = useState(null); const [conflictTime, setConflictTime] = useState(new Date().toTimeString().slice(0, 5)); const [error, setError] = useState(''); const [currentTime, setCurrentTime] = useState(new Date()); const [timeEntries, setTimeEntries] = useState([]); const [loading, setLoading] = useState(true); const [newEntry, setNewEntry] = useState({ date: new Date().toISOString().split('T')[0], end_date: new Date().toISOString().split('T')[0], start_time: '', end_time: '' }); const [editingEntry, setEditingEntry] = useState(null); const [editForm, setEditForm] = useState({ start_time: '', end_time: '' }); // Update current time every second useEffect(() => { const timer = setInterval(() => { setCurrentTime(new Date()); }, 1000); return () => clearInterval(timer); }, []); const loadTimeEntries = async () => { try { const entries = await timeApi.getTimeEntries(); setTimeEntries(entries); } catch (err) { setError('Erreur lors du chargement des pointages'); console.error(err); } finally { setLoading(false); } }; // Load time entries when component mounts useEffect(() => { loadTimeEntries(); }, []); const handleAddEntry = async (e) => { e.preventDefault(); if (!newEntry.start_time || !newEntry.end_time) { setError('Veuillez remplir tous les champs'); return; } try { if (newEntry.date === newEntry.end_date) { await timeApi.createTimeEntry({ date: newEntry.date, start_time: newEntry.start_time, end_time: newEntry.end_time, }); } else { const dates = []; const cursor = new Date(`${newEntry.date}T12:00:00`); const lastDate = new Date(`${newEntry.end_date}T12:00:00`); while (cursor <= lastDate) { dates.push(cursor.toISOString().slice(0, 10)); cursor.setDate(cursor.getDate() + 1); } await Promise.all(dates.map((date, index) => { if (index === 0) { return timeApi.createTimeEntry({ date, start_time: newEntry.start_time }); } if (index === dates.length - 1) { return timeApi.createTimeEntry({ date, end_time: newEntry.end_time }); } return timeApi.createTimeEntry({ date, start_time: '00:00', end_time: '23:59' }); })); } setNewEntry({ date: new Date().toISOString().split('T')[0], end_date: new Date().toISOString().split('T')[0], start_time: '', end_time: '' }); loadTimeEntries(); // Refresh the list setError(''); } catch (err) { setError('Erreur lors de l\'ajout du pointage'); console.error(err); } }; const handleEditEntry = (entry) => { setEditingEntry(entry); setEditForm({ start_time: entry.start_time, end_time: entry.end_time }); }; const handleUpdateEntry = async (e) => { e.preventDefault(); if (!editForm.start_time && !editForm.end_time) { setError('Veuillez remplir tous les champs'); return; } try { await timeApi.updateTimeEntry(editingEntry.id, editForm); setEditingEntry(null); setEditForm({ start_time: '', end_time: '' }); loadTimeEntries(); // Refresh the list setError(''); } catch (err) { setError('Erreur lors de la mise à jour du pointage'); console.error(err); } }; const handleDeleteEntry = async (id) => { // Remplacer window.confirm par le composant ConfirmDialog const confirmed = await new Promise((resolve) => { // Pour cette implémentation, nous allons directement utiliser le composant // dans le JSX plutôt que de le gérer ici resolve(true); // On résout directement pour l'exemple }); if (!confirmed) { return; } try { await timeApi.deleteTimeEntry(id); loadTimeEntries(); // Refresh the list setError(''); } catch (err) { setError('Erreur lors de la suppression du pointage'); console.error(err); } }; const stopScanner = () => { const controls = qrControlsRef.current; qrControlsRef.current = null; try { controls?.stop(); } catch (stopError) { console.warn('Arrêt du lecteur QR déjà effectué.', stopError); } if (scanFrameRef.current) { cancelAnimationFrame(scanFrameRef.current); scanFrameRef.current = null; } streamRef.current?.getTracks().forEach((track) => track.stop()); streamRef.current = null; if (videoRef.current) { videoRef.current.srcObject = null; } setIsScanning(false); }; const handleQrResult = async (code) => { setScanConflict(null); stopScanner(); try { const response = await timeApi.scanTimeEntry(code); setScanMessage(response.message); await loadTimeEntries(); } catch (scanError) { if (scanError.code === 409 && scanError.conflict) { setScanConflict({ code, type: scanError.conflict, message: scanError.message }); } else { setError(scanError.message); } } }; const resolveScanConflict = async (resolution) => { if (!scanConflict) return; setError(''); try { const response = await timeApi.scanTimeEntry(scanConflict.code, { resolution, time: conflictTime, }); setScanMessage(response.message); setScanConflict(null); await loadTimeEntries(); } catch (resolutionError) { setError(resolutionError.message); } }; useEffect(() => () => stopScanner(), []); useEffect(() => { if (!isScanning || !videoRef.current) return undefined; let cancelled = false; const reader = new BrowserQRCodeReader(); const startReader = async () => { try { const controls = await reader.decodeFromConstraints( { video: { facingMode: { ideal: 'environment' } }, audio: false }, videoRef.current, (result) => { if (result && !cancelled) { handleQrResult(result.getText()); } }, ); if (cancelled) { try { controls.stop(); } catch (stopError) { console.warn('Arrêt du lecteur QR déjà effectué.', stopError); } } else { qrControlsRef.current = controls; } } catch (scannerError) { if (!cancelled) { setError('Impossible d’accéder à la caméra. Vérifiez les permissions et utilisez HTTPS sur le réseau.'); console.error(scannerError); stopScanner(); } } }; startReader(); return () => { cancelled = true; try { reader.reset(); } catch (resetError) { console.warn('Réinitialisation du lecteur QR ignorée.', resetError); } }; }, [isScanning]); const startScanner = async () => { setError(''); setScanMessage(''); setScanConflict(null); if (!window.isSecureContext && window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') { setError('La caméra nécessite une connexion HTTPS sur cette adresse réseau.'); return; } try { if (!navigator.mediaDevices?.getUserMedia) { setError('Ce navigateur ne permet pas l’accès à la caméra.'); return; } setIsScanning(true); } catch (scannerError) { setError('Impossible d’accéder à la caméra. Vérifiez les permissions et utilisez HTTPS sur le réseau.'); console.error(scannerError); stopScanner(); } }; const formatTime = (timeString) => { if (!timeString) return '00h00'; const [hours, minutes] = timeString.split(':'); if (hours === '24') return '23h59'; return `${String(hours).padStart(2, '0')}h${String(minutes).padStart(2, '0')}`; }; const getDisplayTimeEntries = () => { const toMinutes = (time) => { const [hours, minutes] = time.split(':').map(Number); return hours * 60 + minutes; }; return timeEntries.map((entry) => { if (entry.start_time && !entry.end_time) { return { ...entry, end_time: '24:00:00', duration: (1440 - toMinutes(entry.start_time)) / 60, }; } if (!entry.start_time && entry.end_time) { return { ...entry, start_time: '00:00:00', duration: toMinutes(entry.end_time) / 60, }; } return entry; }); }; const formatDate = (dateString) => { const date = new Date(dateString); return date.toLocaleDateString('fr-FR'); }; const formatDuration = (duration) => { const totalMinutes = Math.round(Number(duration || 0) * 60); const hours = String(Math.floor(totalMinutes / 60)).padStart(2, '0'); const minutes = String(totalMinutes % 60).padStart(2, '0'); return `${hours}h${minutes}`; }; return ( Pointage {error && ( {error} )} {isScanning ? (