diff --git a/frontend/src/pages/TimeClockPage.jsx b/frontend/src/pages/TimeClockPage.jsx index 361cb94..f1c2149 100644 --- a/frontend/src/pages/TimeClockPage.jsx +++ b/frontend/src/pages/TimeClockPage.jsx @@ -53,57 +53,219 @@ import { /* ========================================================= - DATE PICKER + DATE HELPERS ========================================================= */ +const getTodayValue = () => { + const date = new Date(); + + const year = date.getFullYear(); + const month = String( + date.getMonth() + 1, + ).padStart(2, '0'); + const day = String( + date.getDate(), + ).padStart(2, '0'); + + return `${year}-${month}-${day}`; +}; + + const parseDateValue = (value) => { if (!value) return undefined; - const [year, month, day] = value.split('-').map(Number); + const [ + year, + month, + day, + ] = value.split('-').map(Number); - if (!year || !month || !day) return undefined; + if (!year || !month || !day) { + return undefined; + } - return new Date(year, month - 1, day); + return new Date( + year, + month - 1, + day, + ); }; const formatDateValue = (date) => { if (!date) return ''; - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); + const year = + date.getFullYear(); + + const month = String( + date.getMonth() + 1, + ).padStart(2, '0'); + + const day = String( + date.getDate(), + ).padStart(2, '0'); return `${year}-${month}-${day}`; }; const formatDateLabel = (value) => { - const date = parseDateValue(value); + const date = + parseDateValue(value); - if (!date) return 'Choisir une date'; + if (!date) { + return 'Choisir une date'; + } - return date.toLocaleDateString('fr-FR', { - day: '2-digit', - month: 'long', - year: 'numeric', - }); + return date.toLocaleDateString( + 'fr-FR', + { + day: '2-digit', + month: 'long', + year: 'numeric', + }, + ); }; +const formatDate = ( + dateString, +) => { + if (!dateString) { + return '────'; + } + + const date = + parseDateValue(dateString); + + if (!date) { + return '────'; + } + + return date.toLocaleDateString( + 'fr-FR', + ); +}; + + +/* ========================================================= + DATE/TIME DURATION HELPERS +========================================================= */ + +const parseDateTime = ( + dateValue, + timeValue, +) => { + if (!dateValue || !timeValue) { + return null; + } + + const [ + year, + month, + day, + ] = dateValue.split('-').map(Number); + + const [ + hours, + minutes, + seconds = 0, + ] = timeValue + .split(':') + .map(Number); + + if ( + !year || + !month || + !day || + Number.isNaN(hours) || + Number.isNaN(minutes) + ) { + return null; + } + + return new Date( + year, + month - 1, + day, + hours, + minutes, + Number(seconds) || 0, + ); +}; + + +const calculateDuration = ({ + startDate, + startTime, + endDate, + endTime, +}) => { + if ( + !startDate || + !startTime || + !endDate || + !endTime + ) { + return null; + } + + const start = + parseDateTime( + startDate, + startTime, + ); + + const end = + parseDateTime( + endDate, + endTime, + ); + + if (!start || !end) { + return null; + } + + const difference = + end.getTime() - + start.getTime(); + + if (difference < 0) { + return null; + } + + return ( + difference / + (1000 * 60 * 60) + ); +}; + + +/* ========================================================= + DATE PICKER +========================================================= */ + const DatePicker = ({ value, onChange, min, placeholder = 'Choisir une date', }) => { - const [open, setOpen] = useState(false); + const [open, setOpen] = + useState(false); - const selectedDate = parseDateValue(value); - const minDate = parseDateValue(min); + const selectedDate = + parseDateValue(value); + + const minDate = + parseDateValue(min); return ( - + { if (!date) return; - onChange(formatDateValue(date)); + onChange( + formatDateValue(date), + ); + setOpen(false); }} /> @@ -167,10 +334,15 @@ const TimePicker = ({ onChange, placeholder = 'Choisir une heure', }) => { - const [open, setOpen] = useState(false); + const [open, setOpen] = + useState(false); - const [hours = '', minutes = ''] = - value ? value.split(':') : []; + const [ + hours = '', + minutes = '', + ] = value + ? value.split(':') + : []; const setTimePart = ( part, @@ -191,17 +363,25 @@ const TimePicker = ({ ); }; - const hourOptions = Array.from( - { length: 24 }, - (_, index) => - String(index).padStart(2, '0'), - ); + const hourOptions = + Array.from( + { length: 24 }, + (_, index) => + String(index).padStart( + 2, + '0', + ), + ); - const minuteOptions = Array.from( - { length: 60 }, - (_, index) => - String(index).padStart(2, '0'), - ); + const minuteOptions = + Array.from( + { length: 60 }, + (_, index) => + String(index).padStart( + 2, + '0', + ), + ); return ( {/* HEURES */} +

Heure

- {hourOptions.map((hour) => ( - - ))} + {hourOptions.map( + (hour) => ( + + ), + )}
+ {/* MINUTES */} +

Minutes @@ -337,8 +522,11 @@ const TimePicker = ({ ========================================================= */ const TimeClockPage = () => { - const videoRef = useRef(null); - const qrControlsRef = useRef(null); + const videoRef = + useRef(null); + + const qrControlsRef = + useRef(null); const { isAdmin, @@ -408,18 +596,15 @@ const TimeClockPage = () => { const scannerSessionRef = useRef(0); + const today = + getTodayValue(); + const [ newEntry, setNewEntry, ] = useState({ - date: new Date() - .toISOString() - .split('T')[0], - - end_date: new Date() - .toISOString() - .split('T')[0], - + date: today, + end_date: today, start_time: '', end_time: '', }); @@ -478,7 +663,9 @@ const TimeClockPage = () => { typeof user === 'string' || typeof user === 'number' ) { - return /^\d+$/.test(String(user)) + return /^\d+$/.test( + String(user), + ) ? `Utilisateur #${user}` : String(user); } @@ -507,7 +694,9 @@ const TimeClockPage = () => { }; - const getEntryUserId = (entry) => { + const getEntryUserId = ( + entry, + ) => { if (!entry) return ''; const directId = @@ -529,7 +718,9 @@ const TimeClockPage = () => { entry.user && typeof entry.user === 'object' ) { - return getUserId(entry.user); + return getUserId( + entry.user, + ); } if ( @@ -548,7 +739,10 @@ const TimeClockPage = () => { return ''; }; - const getEntryUserLabel = (entry) => { + + const getEntryUserLabel = ( + entry, + ) => { if (!entry) { return 'Utilisateur inconnu'; } @@ -572,7 +766,9 @@ const TimeClockPage = () => { ? entry.user : '' ) || - `Utilisateur #${getEntryUserId(entry)}` + `Utilisateur #${getEntryUserId( + entry, + )}` ); }; @@ -584,11 +780,235 @@ const TimeClockPage = () => { ); }; - const getDisplayTimeEntries = - () => { - return timeEntries; + + /* ========================================================= + GROUP MULTI-DAY ENTRIES + ========================================================= */ + + const getEntrySortTime = ( + entry, + ) => { + if (!entry?.start_time) { + return ''; + } + + return entry.start_time; }; + + const buildDisplayTimeEntries = ( + entries, + ) => { + if (!Array.isArray(entries)) { + return []; + } + + const groupedEntries = + new Map(); + + const normalEntries = []; + + entries.forEach( + (entry) => { + if (!entry?.group_id) { + normalEntries.push({ + ...entry, + isGrouped: false, + sourceEntries: [entry], + displayStartDate: + entry.date, + displayEndDate: + entry.date, + displayStartTime: + entry.start_time, + displayEndTime: + entry.end_time, + displayDuration: + calculateDuration({ + startDate: + entry.date, + startTime: + entry.start_time, + endDate: + entry.date, + endTime: + entry.end_time, + }), + }); + + return; + } + + const groupKey = + String(entry.group_id); + + if ( + !groupedEntries.has( + groupKey, + ) + ) { + groupedEntries.set( + groupKey, + [], + ); + } + + groupedEntries + .get(groupKey) + .push(entry); + }, + ); + + + const groupedDisplayEntries = + Array.from( + groupedEntries.values(), + ).map( + (groupEntries) => { + const sortedEntries = + [...groupEntries].sort( + (a, b) => { + const dateCompare = + String( + a.date || '', + ).localeCompare( + String( + b.date || '', + ), + ); + + if ( + dateCompare !== 0 + ) { + return dateCompare; + } + + return getEntrySortTime( + a, + ).localeCompare( + getEntrySortTime( + b, + ), + ); + }, + ); + + const firstEntry = + sortedEntries[0]; + + const lastEntry = + sortedEntries[ + sortedEntries.length - 1 + ]; + + const startDate = + firstEntry?.date || + ''; + + const endDate = + lastEntry?.date || + startDate; + + const startTime = + firstEntry?.start_time || + null; + + const endTime = + lastEntry?.end_time || + null; + + return { + ...firstEntry, + + id: `group-${String( + firstEntry.group_id, + )}`, + + group_id: + firstEntry.group_id, + + isGrouped: true, + + sourceEntries: + sortedEntries, + + date: startDate, + + end_date: endDate, + + start_time: + startTime, + + end_time: + endTime, + + displayStartDate: + startDate, + + displayEndDate: + endDate, + + displayStartTime: + startTime, + + displayEndTime: + endTime, + + displayDuration: + calculateDuration({ + startDate, + startTime, + endDate, + endTime, + }), + }; + }, + ); + + + const result = [ + ...normalEntries, + ...groupedDisplayEntries, + ]; + + + return result.sort( + (a, b) => { + const dateCompare = + String( + b.displayStartDate || + b.date || + '', + ).localeCompare( + String( + a.displayStartDate || + a.date || + '', + ), + ); + + if ( + dateCompare !== 0 + ) { + return dateCompare; + } + + return String( + b.displayStartTime || + b.start_time || + '', + ).localeCompare( + String( + a.displayStartTime || + a.start_time || + '', + ), + ); + }, + ); + }; + + /* ========================================================= LOAD ENTRIES ========================================================= */ @@ -652,6 +1072,7 @@ const TimeClockPage = () => { } } + const usersById = new Map( availableUsers @@ -664,6 +1085,7 @@ const TimeClockPage = () => { ), ); + entries.forEach( (entry) => { const id = @@ -705,6 +1127,7 @@ const TimeClockPage = () => { }, ); + availableUsers = [...usersById.values()] .filter( @@ -712,26 +1135,35 @@ const TimeClockPage = () => { getUserId(user), ) .sort((a, b) => - getUserLabel(a).localeCompare( - getUserLabel(b), + getUserLabel( + a, + ).localeCompare( + getUserLabel( + b, + ), 'fr', ), ); + setUsers( availableUsers, ); + const currentUserId = getCurrentUserId(); + setSelectedUserId( (currentSelectedId) => { if ( currentSelectedId && availableUsers.some( (user) => - getUserId(user) === + getUserId( + user, + ) === String( currentSelectedId, ), @@ -767,174 +1199,385 @@ const TimeClockPage = () => { /* ========================================================= - ADD ENTRY + VALIDATION ========================================================= */ - const handleAddEntry = - async (e) => { - e.preventDefault(); - - if ( - !newEntry.start_time || - !newEntry.end_time - ) { - setError( - 'Veuillez renseigner une heure d’entrée et une heure de sortie.', - ); - - return; + const validateEntry = ({ + date, + end_date, + start_time, + end_time, + }) => { + if (!date || !end_date) { + return 'Veuillez renseigner les dates.'; } if ( - !newEntry.date || - !newEntry.end_date + !start_time || + !end_time ) { - setError( - 'Veuillez renseigner les dates.', - ); + return 'Veuillez renseigner une heure d’entrée et une heure de sortie.'; + } - return; + if (end_date < date) { + return 'La date de sortie doit être postérieure ou égale à la date d’entrée.'; } if ( - newEntry.end_date < - newEntry.date + date === end_date && + end_time <= start_time ) { - setError( - 'La date de sortie doit être postérieure ou égale à la date d’entrée.', - ); - - return; + return "L'heure de sortie doit être après l'heure d'entrée lorsque le pointage est sur la même journée."; } - if ( - newEntry.date === - newEntry.end_date && - newEntry.end_time <= - newEntry.start_time - ) { - setError( - "L'heure de sortie doit être après l'heure d'entrée.", - ); - - return; - } - - if ( - isAdmin && - !showAllUsers && - !selectedUserId - ) { - setError( - 'Veuillez sélectionner un utilisateur.', - ); - - return; - } - - try { - await timeApi.createTimeEntry({ - date: newEntry.date, - - end_date: - newEntry.end_date, - - start_time: - newEntry.start_time, - - end_time: - newEntry.end_time, - - ...(isAdmin && - selectedUserId - ? { - user_id: - selectedUserId, - } - : {}), - }); - - const today = - new Date() - .toISOString() - .split('T')[0]; - - setNewEntry({ - date: today, - end_date: today, - start_time: '', - end_time: '', - }); - - await loadTimeEntries(); - - setError(''); - } catch (err) { - setError( - err.message || - "Erreur lors de l'ajout du pointage", - ); - - console.error(err); - } + return ''; }; /* ========================================================= - EDIT ENTRY + ADD ENTRY ========================================================= */ - const handleEditEntry = - (entry) => { - setEditingEntry(entry); - - setEditForm({ - date: entry.date || '', - end_date: - entry.end_date || - entry.date || - '', - start_time: - entry.start_time || '', - end_time: - entry.end_time || '', - }); - }; - - - const handleUpdateEntry = + const handleAddEntry = async (e) => { e.preventDefault(); + const validationError = + validateEntry(newEntry); + + if (validationError) { + setError( + validationError, + ); + + return; + } + if ( - !editForm.start_time && - !editForm.end_time + isAdmin && + !showAllUsers && + !selectedUserId ) { setError( - 'Veuillez remplir tous les champs', + 'Veuillez sélectionner un utilisateur.', ); return; } try { - await timeApi.updateTimeEntry( - editingEntry.id, - editForm, + await timeApi.createTimeEntry( + { + date: newEntry.date, + + end_date: + newEntry.end_date, + + start_time: + newEntry.start_time, + + end_time: + newEntry.end_time, + + ...(isAdmin && + selectedUserId + ? { + user_id: + selectedUserId, + } + : {}), + }, ); - setEditingEntry(null); - setEditForm({ + const today = + getTodayValue(); + + + setNewEntry({ + date: today, + end_date: today, start_time: '', end_time: '', }); - loadTimeEntries(); + + await loadTimeEntries(); + setError(''); } catch (err) { setError( - 'Erreur lors de la mise à jour du pointage', + err.message || + "Erreur lors de l'ajout du pointage", + ); + + console.error(err); + } + }; + + + /* ========================================================= + EDIT ENTRY + ========================================================= */ + + const handleEditEntry = ( + entry, + ) => { + setEditingEntry(entry); + + if (entry.isGrouped) { + const sortedEntries = + [...( + entry.sourceEntries || + [] + )].sort( + (a, b) => { + const dateCompare = + String( + a.date || '', + ).localeCompare( + String( + b.date || '', + ), + ); + + if ( + dateCompare !== 0 + ) { + return dateCompare; + } + + return String( + a.start_time || '', + ).localeCompare( + String( + b.start_time || '', + ), + ); + }, + ); + + const firstEntry = + sortedEntries[0]; + + const lastEntry = + sortedEntries[ + sortedEntries.length - 1 + ]; + + setEditForm({ + date: + firstEntry?.date || + entry.date || + '', + + end_date: + lastEntry?.date || + entry.end_date || + entry.date || + '', + + start_time: + firstEntry?.start_time || + '', + + end_time: + lastEntry?.end_time || + '', + }); + } else { + setEditForm({ + date: + entry.date || '', + + end_date: + entry.end_date || + entry.date || + '', + + start_time: + entry.start_time || '', + + end_time: + entry.end_time || '', + }); + } + + setError(''); + }; + + + const handleUpdateEntry = + async (e) => { + e.preventDefault(); + + if (!editingEntry) { + return; + } + + const validationError = + validateEntry(editForm); + + if (validationError) { + setError( + validationError, + ); + + return; + } + + + try { + /* ===================================================== + GROUPE MULTI-JOURS + + On ne modifie que : + - start_time de la première ligne + - end_time de la dernière ligne + + Les dates restent celles du groupe car le serializer + actuel de mise à jour ne gère pas date/end_date. + ====================================================== */ + + if ( + editingEntry.isGrouped + ) { + const sortedEntries = + [...( + editingEntry.sourceEntries || + [] + )].sort( + (a, b) => { + const dateCompare = + String( + a.date || '', + ).localeCompare( + String( + b.date || '', + ), + ); + + if ( + dateCompare !== 0 + ) { + return dateCompare; + } + + return String( + a.start_time || '', + ).localeCompare( + String( + b.start_time || '', + ), + ); + }, + ); + + + const firstEntry = + sortedEntries[0]; + + const lastEntry = + sortedEntries[ + sortedEntries.length - 1 + ]; + + + if ( + !firstEntry || + !lastEntry + ) { + throw new Error( + 'Impossible de déterminer les lignes du pointage multi-jours.', + ); + } + + + /* + * Cas très improbable où le groupe ne possède + * qu'une seule ligne. + */ + if ( + firstEntry.id === + lastEntry.id + ) { + await timeApi.updateTimeEntry( + firstEntry.id, + { + start_time: + editForm.start_time, + end_time: + editForm.end_time, + }, + ); + } else { + /* + * Première journée : + * on conserve son end_time = 23:59:59 + * et on change uniquement start_time. + */ + await timeApi.updateTimeEntry( + firstEntry.id, + { + start_time: + editForm.start_time, + end_time: + firstEntry.end_time, + }, + ); + + + /* + * Dernière journée : + * on conserve son start_time = 00:00 + * et on change uniquement end_time. + */ + await timeApi.updateTimeEntry( + lastEntry.id, + { + start_time: + lastEntry.start_time, + end_time: + editForm.end_time, + }, + ); + } + } else { + /* + * Pointage classique : + * le backend actuel ne prend en charge que + * start_time/end_time pour la modification. + */ + await timeApi.updateTimeEntry( + editingEntry.id, + { + start_time: + editForm.start_time, + + end_time: + editForm.end_time, + }, + ); + } + + + setEditingEntry(null); + + setEditForm({ + date: '', + end_date: '', + start_time: '', + end_time: '', + }); + + + await loadTimeEntries(); + + setError(''); + } catch (err) { + setError( + err.message || + 'Erreur lors de la mise à jour du pointage', ); console.error(err); @@ -947,13 +1590,35 @@ const TimeClockPage = () => { ========================================================= */ const handleDeleteEntry = - async (id) => { + async (entry) => { try { - await timeApi.deleteTimeEntry( - id, - ); + if ( + entry?.isGrouped && + Array.isArray( + entry.sourceEntries, + ) + ) { + /* + * Suppression de toutes les lignes + * appartenant au même group_id. + */ + for ( + const sourceEntry of + entry.sourceEntries + ) { + await timeApi.deleteTimeEntry( + sourceEntry.id, + ); + } + } else { + await timeApi.deleteTimeEntry( + entry.id, + ); + } + + + await loadTimeEntries(); - loadTimeEntries(); setError(''); } catch (err) { setError( @@ -984,9 +1649,11 @@ const TimeClockPage = () => { ); } - qrControlsRef.current = null; + qrControlsRef.current = + null; } + if ( videoRef.current?.srcObject ) { @@ -1009,216 +1676,223 @@ const TimeClockPage = () => { const closeScanner = () => { stopScanner(); - setIsScannerDialogOpen(false); - }; - - const handleQrResult = - async ( - code, - sessionId, - ) => { - if ( - sessionId !== - scannerSessionRef.current - ) { - return; - } - - setError(''); - setScanMessage(''); - setScanConflict(null); - - closeScanner(); - - try { - const response = - await timeApi.scanTimeEntry( - code, - ); - - setScanMessage( - response.message, - ); - - await loadTimeEntries(); - } catch ( - scanError - ) { - if ( - scanError.code === 409 && - scanError.conflict - ) { - const nowTime = - new Date() - .toTimeString() - .slice(0, 5); - - setConflictTime( - scanError.scanTime || - nowTime, - ); - - setScanConflict({ - code, - type: - scanError.conflict, - message: - scanError.message, - resolution: - scanError.resolution, - missingTime: - scanError.missingTime, - scanTime: - scanError.scanTime, - step: 'confirm', - }); - - return; - } - - setError( - scanError.message, - ); - } - }; - - - const getScanConflictConfig = () => { - if (!scanConflict) { - return null; - } - - if ( - scanConflict.type === 'open_entry' - ) { - return { - confirmationTitle: - 'Une entrée est déjà ouverte', - - confirmationMessage: - 'Voulez-vous enregistrer une sortie pour cette entrée ?', - - timeLabel: - 'Heure de sortie', - - resolution: - 'complete_exit', - }; - } - - if ( - scanConflict.type === 'missing_entry' - ) { - return { - confirmationTitle: - 'Aucune entrée ouverte', - - confirmationMessage: - 'Voulez-vous enregistrer cette sortie malgré l’absence d’entrée ?', - - timeLabel: - "Heure d'entrée", - - resolution: - 'complete_missing_entry', - }; - } - - if ( - scanConflict.type === 'open_exit' - ) { - return { - confirmationTitle: - 'Une sortie sans entrée existe', - - confirmationMessage: - 'Voulez-vous renseigner l’heure d’entrée ?', - - timeLabel: - "Heure d'entrée", - - resolution: - 'complete_open_exit', - }; - } - - return null; -}; - - -const startScanConflictResolution = - () => { - setScanConflict( - (current) => - current - ? { - ...current, - step: 'time', - } - : null, + setIsScannerDialogOpen( + false, ); }; -const cancelScanConflict = () => { - setScanConflict(null); - setError(''); -}; - - -const resolveScanConflict = - async () => { - if (!scanConflict) { - return; - } - - const config = - getScanConflictConfig(); - - if (!config) { - return; - } - - setError(''); - - try { - const options = { - resolution: - config.resolution, - time: conflictTime, - }; - + const handleQrResult = + async ( + code, + sessionId, + ) => { if ( - scanConflict.scanTime + sessionId !== + scannerSessionRef.current ) { - options.scan_time = - scanConflict.scanTime; + return; } - const response = - await timeApi.scanTimeEntry( - scanConflict.code, - options, - ); - - setScanMessage( - response.message, - ); - + setError(''); + setScanMessage(''); setScanConflict(null); - await loadTimeEntries(); - } catch ( - resolutionError - ) { - setError( - resolutionError.message, + closeScanner(); + + try { + const response = + await timeApi.scanTimeEntry( + code, + ); + + setScanMessage( + response.message, + ); + + await loadTimeEntries(); + } catch ( + scanError + ) { + if ( + scanError.code === 409 && + scanError.conflict + ) { + const nowTime = + new Date() + .toTimeString() + .slice(0, 5); + + setConflictTime( + scanError.scanTime || + nowTime, + ); + + setScanConflict({ + code, + type: + scanError.conflict, + message: + scanError.message, + resolution: + scanError.resolution, + missingTime: + scanError.missingTime, + scanTime: + scanError.scanTime, + step: 'confirm', + }); + + return; + } + + setError( + scanError.message, + ); + } + }; + + + const getScanConflictConfig = + () => { + if (!scanConflict) { + return null; + } + + if ( + scanConflict.type === + 'open_entry' + ) { + return { + confirmationTitle: + 'Une entrée est déjà ouverte', + + confirmationMessage: + 'Voulez-vous enregistrer une sortie pour cette entrée ?', + + timeLabel: + 'Heure de sortie', + + resolution: + 'complete_exit', + }; + } + + if ( + scanConflict.type === + 'missing_entry' + ) { + return { + confirmationTitle: + 'Aucune entrée ouverte', + + confirmationMessage: + 'Voulez-vous enregistrer cette sortie malgré l’absence d’entrée ?', + + timeLabel: + "Heure d'entrée", + + resolution: + 'complete_missing_entry', + }; + } + + if ( + scanConflict.type === + 'open_exit' + ) { + return { + confirmationTitle: + 'Une sortie sans entrée existe', + + confirmationMessage: + "Voulez-vous renseigner l’heure d’entrée ?", + + timeLabel: + "Heure d'entrée", + + resolution: + 'complete_open_exit', + }; + } + + return null; + }; + + + const startScanConflictResolution = + () => { + setScanConflict( + (current) => + current + ? { + ...current, + step: 'time', + } + : null, ); - } + }; + + + const cancelScanConflict = () => { + setScanConflict(null); + setError(''); }; + const resolveScanConflict = + async () => { + if (!scanConflict) { + return; + } + + const config = + getScanConflictConfig(); + + if (!config) { + return; + } + + setError(''); + + try { + const options = { + resolution: + config.resolution, + time: conflictTime, + }; + + if ( + scanConflict.scanTime + ) { + options.scan_time = + scanConflict.scanTime; + } + + const response = + await timeApi.scanTimeEntry( + scanConflict.code, + options, + ); + + setScanMessage( + response.message, + ); + + setScanConflict(null); + + await loadTimeEntries(); + } catch ( + resolutionError + ) { + setError( + resolutionError.message, + ); + } + }; + + useEffect(() => { return () => { stopScanner(); @@ -1295,6 +1969,7 @@ const resolveScanConflict = }, ); + if ( cancelled || sessionId !== @@ -1395,7 +2070,9 @@ const resolveScanConflict = setError(''); setScanMessage(''); setScanConflict(null); - setCameraPermissionError(false); + setCameraPermissionError( + false, + ); if ( !window.isSecureContext && @@ -1422,9 +2099,12 @@ const resolveScanConflict = return; } - scannerSessionRef.current += 1; + scannerSessionRef.current += + 1; - setIsScannerDialogOpen(true); + setIsScannerDialogOpen( + true, + ); }; @@ -1529,35 +2209,33 @@ const resolveScanConflict = minutes, ] = timeString.split(':'); - return `${String(hours).padStart( + return `${String( + hours, + ).padStart( 2, '0', - )}h${String(minutes).padStart( + )}h${String( + minutes, + ).padStart( 2, '0', )}`; }; - const formatDate = ( - dateString, - ) => { - const date = - new Date(dateString); - - return date.toLocaleDateString( - 'fr-FR', - ); - }; - const formatDuration = ( duration, ) => { + if ( + duration === null || + duration === undefined + ) { + return '────'; + } + const totalMinutes = Math.round( - Number( - duration || 0, - ) * 60, + Number(duration) * 60, ); const hours = String( @@ -1574,7 +2252,47 @@ const resolveScanConflict = }; - const displayedTimeEntries = + const formatEntryDate = ( + entry, + ) => { + if (!entry?.date) { + return '────'; + } + + const startDate = + formatDate(entry.date); + + const endDate = + entry.end_date && + entry.end_date !== entry.date + ? formatDate( + entry.end_date, + ) + : null; + + if (!endDate) { + return startDate; + } + + return ( + + {startDate} + + + → + + + {endDate} + + ); + }; + + + /* ========================================================= + FILTER + GROUP + ========================================================= */ + + const filteredTimeEntries = isAdmin && !showAllUsers && selectedUserId @@ -1590,6 +2308,12 @@ const resolveScanConflict = : timeEntries; + const displayedTimeEntries = + buildDisplayTimeEntries( + filteredTimeEntries, + ); + + const selectedUser = users.find( (user) => @@ -1605,15 +2329,6 @@ const resolveScanConflict = ========================================================= */ return ( - /* - MOBILE : - - le Card principal devient le conteneur scrollable - - son contenu peut prendre toute sa hauteur - - DESKTOP : - - le Card principal ne scroll pas - - le scroll est délégué à l'historique - */ @@ -1622,6 +2337,7 @@ const resolveScanConflict = + {/* ========================= @@ -1684,7 +2400,8 @@ const resolveScanConflict = - {scanConflict.step === 'confirm' + {scanConflict.step === + 'confirm' ? 'Confirmation du pointage' : 'Heure manquante'} @@ -1706,7 +2423,9 @@ const resolveScanConflict = return ( <>

- {config.confirmationMessage} + { + config.confirmationMessage + }

@@ -1736,12 +2455,16 @@ const resolveScanConflict = return ( <>

- {config.confirmationMessage} + { + config.confirmationMessage + }

@@ -1864,7 +2587,6 @@ const resolveScanConflict = {/* ================================================= COLONNE GAUCHE - GESTION ADMIN ================================================== */}
@@ -1878,6 +2600,7 @@ const resolveScanConflict = + {/* UTILISATEUR */} @@ -1900,7 +2623,8 @@ const resolveScanConflict = event, ) => { setSelectedUserId( - event.target.value, + event.target + .value, ); setEditingEntry( @@ -1993,7 +2717,9 @@ const resolveScanConflict =
- {/* EDITION */} + {/* ============================= + EDITION + ============================== */} {editingEntry ? (
+ + {/* DATE D'ENTRÉE */} +
- - setEditForm({ - ...editForm, - date: value, - end_date: - editForm.end_date && - editForm.end_date < value - ? value - : editForm.end_date, - }) - } - /> + {editingEntry.isGrouped ? ( +
+ {formatDateLabel( + editForm.date, + )} +
+ ) : ( + + setEditForm( + { + ...editForm, + date: + value, + + end_date: + editForm.end_date && + editForm.end_date < + value + ? value + : editForm.end_date, + }, + ) + } + /> + )}
+ + {/* DATE DE SORTIE */} +
- - setEditForm({ - ...editForm, - end_date: value, - }) - } - /> + {editingEntry.isGrouped ? ( +
+ {formatDateLabel( + editForm.end_date, + )} +
+ ) : ( + + setEditForm( + { + ...editForm, + end_date: + value, + }, + ) + } + /> + )}
+ + {/* HEURE D'ENTRÉE */} +
- setEditForm({ - ...editForm, - start_time: value, - }) + value={ + editForm.start_time + } + onChange={( + value, + ) => + setEditForm( + { + ...editForm, + start_time: + value, + }, + ) } />
+ + {/* HEURE DE SORTIE */} +
- setEditForm({ - ...editForm, - end_time: value, - }) + value={ + editForm.end_time + } + onChange={( + value, + ) => + setEditForm( + { + ...editForm, + end_time: + value, + }, + ) } />
@@ -2076,6 +2862,16 @@ const resolveScanConflict =
+ {editingEntry.isGrouped && ( +

+ Les dates d’un pointage + multi-jours sont liées au + groupe. Seules les heures + peuvent être modifiées ici. +

+ )} + +