@@ -208,66 +331,168 @@ const TimePicker = ({
);
};
+
+/* =========================================================
+ PAGE
+========================================================= */
+
const TimeClockPage = () => {
const videoRef = useRef(null);
const qrControlsRef = useRef(null);
- const { isAdmin, user: authUser, currentUser } = useAuth();
- const [scanMessage, setScanMessage] = useState('');
- const [scanConflict, setScanConflict] = useState(null);
- const [conflictTime, setConflictTime] = useState(new Date().toTimeString().slice(0, 5));
- const [error, setError] = useState('');
- const [timeEntries, setTimeEntries] = useState([]);
- const [users, setUsers] = useState([]);
- const [selectedUserId, setSelectedUserId] = useState('');
- const [showAllUsers, setShowAllUsers] = useState(false);
- const [loading, setLoading] = useState(true);
- const [isScannerDialogOpen, setIsScannerDialogOpen] = useState(false);
- const [cameraPermissionError, setCameraPermissionError] = useState(false);
- const scannerSessionRef = useRef(0);
- const [newEntry, setNewEntry] = useState({
- date: new Date().toISOString().split('T')[0],
- end_date: new Date().toISOString().split('T')[0],
+
+ const {
+ isAdmin,
+ user: authUser,
+ currentUser,
+ } = useAuth();
+
+ const [
+ scanMessage,
+ setScanMessage,
+ ] = useState('');
+
+ const [
+ scanConflict,
+ setScanConflict,
+ ] = useState(null);
+
+ const [
+ conflictTime,
+ setConflictTime,
+ ] = useState(
+ new Date()
+ .toTimeString()
+ .slice(0, 5),
+ );
+
+ const [
+ error,
+ setError,
+ ] = useState('');
+
+ const [
+ timeEntries,
+ setTimeEntries,
+ ] = useState([]);
+
+ const [
+ users,
+ setUsers,
+ ] = useState([]);
+
+ const [
+ selectedUserId,
+ setSelectedUserId,
+ ] = useState('');
+
+ const [
+ showAllUsers,
+ setShowAllUsers,
+ ] = useState(false);
+
+ const [
+ loading,
+ setLoading,
+ ] = useState(true);
+
+ const [
+ isScannerDialogOpen,
+ setIsScannerDialogOpen,
+ ] = useState(false);
+
+ const [
+ cameraPermissionError,
+ setCameraPermissionError,
+ ] = useState(false);
+
+ const scannerSessionRef =
+ useRef(0);
+
+ const [
+ newEntry,
+ setNewEntry,
+ ] = useState({
+ date: new Date()
+ .toISOString()
+ .split('T')[0],
+
+ end_date: new Date()
+ .toISOString()
+ .split('T')[0],
+
start_time: '',
- end_time: ''
+ end_time: '',
});
- const [editingEntry, setEditingEntry] = useState(null);
- const [editForm, setEditForm] = useState({
+
+ const [
+ editingEntry,
+ setEditingEntry,
+ ] = useState(null);
+
+ const [
+ editForm,
+ setEditForm,
+ ] = useState({
start_time: '',
- end_time: ''
+ end_time: '',
});
-
+
+
+ /* =========================================================
+ USER HELPERS
+ ========================================================= */
const getUserId = (user) => {
if (!user) return '';
- if (typeof user === 'number') return String(user);
+ if (typeof user === 'number') {
+ return String(user);
+ }
+
if (typeof user === 'string') {
- return /^\d+$/.test(user) ? user : '';
+ return /^\d+$/.test(user)
+ ? user
+ : '';
}
return String(
user.id ??
- user.pk ??
- user.user_id ??
- user.userId ??
- user.user?.id ??
- user.user?.pk ??
- ''
+ user.pk ??
+ user.user_id ??
+ user.userId ??
+ user.user?.id ??
+ user.user?.pk ??
+ '',
);
};
- const getUserLabel = (user) => {
- if (!user) return 'Utilisateur';
- if (typeof user === 'string' || typeof user === 'number') {
+ const getUserLabel = (user) => {
+ if (!user) {
+ return 'Utilisateur';
+ }
+
+ if (
+ typeof user === 'string' ||
+ typeof user === 'number'
+ ) {
return /^\d+$/.test(String(user))
? `Utilisateur #${user}`
: String(user);
}
- const firstName = user.first_name || user.firstName || '';
- const lastName = user.last_name || user.lastName || '';
- const fullName = `${firstName} ${lastName}`.trim();
+ const firstName =
+ user.first_name ||
+ user.firstName ||
+ '';
+
+ const lastName =
+ user.last_name ||
+ user.lastName ||
+ '';
+
+ const fullName =
+ `${firstName} ${lastName}`.trim();
return (
fullName ||
@@ -279,6 +504,7 @@ const TimeClockPage = () => {
);
};
+
const getEntryUserId = (entry) => {
if (!entry) return '';
@@ -289,320 +515,627 @@ const TimeClockPage = () => {
entry.employeeId ??
entry.user_pk;
- if (directId !== undefined && directId !== null && directId !== '') {
+ if (
+ directId !== undefined &&
+ directId !== null &&
+ directId !== ''
+ ) {
return String(directId);
}
- if (entry.user && typeof entry.user === 'object') {
+ if (
+ entry.user &&
+ typeof entry.user === 'object'
+ ) {
return getUserId(entry.user);
}
- // Certaines réponses Django renvoient simplement `user: 2`
- // au lieu de `user: { id: 2, ... }`.
- if (typeof entry.user === 'number') {
+ if (
+ typeof entry.user === 'number'
+ ) {
return String(entry.user);
}
- if (typeof entry.user === 'string' && /^\d+$/.test(entry.user)) {
+ if (
+ typeof entry.user === 'string' &&
+ /^\d+$/.test(entry.user)
+ ) {
return entry.user;
}
return '';
};
- const getEntryUserLabel = (entry) => {
- if (!entry) return 'Utilisateur inconnu';
- if (entry.user && typeof entry.user === 'object') {
- return getUserLabel(entry.user);
+ const getEntryUserLabel = (entry) => {
+ if (!entry) {
+ return 'Utilisateur inconnu';
+ }
+
+ if (
+ entry.user &&
+ typeof entry.user === 'object'
+ ) {
+ return getUserLabel(
+ entry.user,
+ );
}
return (
entry.username ||
entry.user_name ||
entry.userName ||
- (typeof entry.user === 'string' && !/^\d+$/.test(entry.user)
- ? entry.user
- : '') ||
+ (
+ typeof entry.user === 'string' &&
+ !/^\d+$/.test(entry.user)
+ ? entry.user
+ : ''
+ ) ||
`Utilisateur #${getEntryUserId(entry)}`
);
};
+
const getCurrentUserId = () => {
- return getUserId(authUser) || getUserId(currentUser);
+ return (
+ getUserId(authUser) ||
+ getUserId(currentUser)
+ );
};
- const loadTimeEntries = async () => {
- try {
- setLoading(true);
- const entries = await timeApi.getTimeEntries();
- setTimeEntries(entries);
+ /* =========================================================
+ LOAD ENTRIES
+ ========================================================= */
- if (!isAdmin) return;
+ const loadTimeEntries =
+ async () => {
+ try {
+ setLoading(true);
- // Si l'API expose une liste d'utilisateurs, on l'utilise.
- // Sinon, on reconstruit une liste à partir des pointages retournés.
- let availableUsers = [];
+ const entries =
+ await timeApi.getTimeEntries();
- if (typeof timeApi.getUsers === 'function') {
- try {
- const apiUsers = await timeApi.getUsers();
+ setTimeEntries(entries);
- if (Array.isArray(apiUsers)) {
- availableUsers = apiUsers;
- } else if (Array.isArray(apiUsers?.results)) {
- availableUsers = apiUsers.results;
- } else if (Array.isArray(apiUsers?.data)) {
- availableUsers = apiUsers.data;
- } else if (Array.isArray(apiUsers?.users)) {
- availableUsers = apiUsers.users;
- }
- } catch (usersError) {
- console.warn('Impossible de charger les utilisateurs.', usersError);
- }
- }
+ if (!isAdmin) return;
- // Sécurité : on ajoute les utilisateurs trouvés dans les pointages
- // qui ne seraient pas présents dans /users/.
- const usersById = new Map(
- availableUsers
- .map((user) => [getUserId(user), user])
- .filter(([id]) => id),
- );
+ let availableUsers = [];
- entries.forEach((entry) => {
- const id = getEntryUserId(entry);
-
- if (!id || usersById.has(id)) return;
-
- const entryUser =
- typeof entry.user === 'object' && entry.user
- ? entry.user
- : {
- id,
- username: entry.username,
- name: entry.user_name || entry.userName,
- first_name: entry.first_name,
- last_name: entry.last_name,
- email: entry.email,
- };
-
- usersById.set(id, entryUser);
- });
-
- availableUsers = [...usersById.values()]
- .filter((user) => getUserId(user))
- .sort((a, b) =>
- getUserLabel(a).localeCompare(getUserLabel(b), 'fr'),
- );
-
- setUsers(availableUsers);
-
- const currentUserId = getCurrentUserId();
-
- setSelectedUserId((currentSelectedId) => {
if (
- currentSelectedId &&
- availableUsers.some(
- (user) => getUserId(user) === String(currentSelectedId),
- )
+ typeof timeApi.getUsers ===
+ 'function'
) {
- return currentSelectedId;
+ try {
+ const apiUsers =
+ await timeApi.getUsers();
+
+ if (
+ Array.isArray(apiUsers)
+ ) {
+ availableUsers =
+ apiUsers;
+ } else if (
+ Array.isArray(
+ apiUsers?.results,
+ )
+ ) {
+ availableUsers =
+ apiUsers.results;
+ } else if (
+ Array.isArray(
+ apiUsers?.data,
+ )
+ ) {
+ availableUsers =
+ apiUsers.data;
+ } else if (
+ Array.isArray(
+ apiUsers?.users,
+ )
+ ) {
+ availableUsers =
+ apiUsers.users;
+ }
+ } catch (
+ usersError
+ ) {
+ console.warn(
+ 'Impossible de charger les utilisateurs.',
+ usersError,
+ );
+ }
}
- return (
- currentUserId ||
- getUserId(availableUsers[0]) ||
- ''
+ const usersById =
+ new Map(
+ availableUsers
+ .map((user) => [
+ getUserId(user),
+ user,
+ ])
+ .filter(
+ ([id]) => id,
+ ),
+ );
+
+ entries.forEach(
+ (entry) => {
+ const id =
+ getEntryUserId(
+ entry,
+ );
+
+ if (
+ !id ||
+ usersById.has(id)
+ ) {
+ return;
+ }
+
+ const entryUser =
+ typeof entry.user ===
+ 'object' &&
+ entry.user
+ ? entry.user
+ : {
+ id,
+ username:
+ entry.username,
+ name:
+ entry.user_name ||
+ entry.userName,
+ first_name:
+ entry.first_name,
+ last_name:
+ entry.last_name,
+ email:
+ entry.email,
+ };
+
+ usersById.set(
+ id,
+ entryUser,
+ );
+ },
);
- });
- } catch (err) {
- setError('Erreur lors du chargement des pointages');
- console.error(err);
- } finally {
- setLoading(false);
- }
- };
+
+ availableUsers =
+ [...usersById.values()]
+ .filter(
+ (user) =>
+ getUserId(user),
+ )
+ .sort((a, b) =>
+ getUserLabel(a).localeCompare(
+ getUserLabel(b),
+ 'fr',
+ ),
+ );
+
+ setUsers(
+ availableUsers,
+ );
+
+ const currentUserId =
+ getCurrentUserId();
+
+ setSelectedUserId(
+ (currentSelectedId) => {
+ if (
+ currentSelectedId &&
+ availableUsers.some(
+ (user) =>
+ getUserId(user) ===
+ String(
+ currentSelectedId,
+ ),
+ )
+ ) {
+ return currentSelectedId;
+ }
+
+ return (
+ currentUserId ||
+ getUserId(
+ availableUsers[0],
+ ) ||
+ ''
+ );
+ },
+ );
+ } catch (err) {
+ setError(
+ 'Erreur lors du chargement des pointages',
+ );
+
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+
useEffect(() => {
loadTimeEntries();
}, []);
- const handleAddEntry = async (e) => {
- e.preventDefault();
- if (!newEntry.start_time || !newEntry.end_time) {
- setError('Veuillez remplir tous les champs');
- return;
- }
- if (isAdmin && !showAllUsers && !selectedUserId) {
- setError('Veuillez sélectionner un utilisateur.');
- return;
- }
+ /* =========================================================
+ ADD ENTRY
+ ========================================================= */
- try {
- if (newEntry.date === newEntry.end_date) {
- await timeApi.createTimeEntry({
- date: newEntry.date,
- start_time: newEntry.start_time,
- end_time: newEntry.end_time,
- ...(isAdmin && selectedUserId
- ? { user_id: selectedUserId }
- : {}),
- });
- } else {
- const dates = [];
- const cursor = new Date(`${newEntry.date}T12:00:00`);
- const lastDate = new Date(`${newEntry.end_date}T12:00:00`);
+ const handleAddEntry =
+ async (e) => {
+ e.preventDefault();
- while (cursor <= lastDate) {
- dates.push(cursor.toISOString().slice(0, 10));
- cursor.setDate(cursor.getDate() + 1);
- }
+ if (
+ !newEntry.start_time ||
+ !newEntry.end_time
+ ) {
+ setError(
+ 'Veuillez remplir tous les champs',
+ );
+ return;
+ }
- await Promise.all(dates.map((date, index) => {
- if (index === 0) {
- return timeApi.createTimeEntry({
- date,
- start_time: newEntry.start_time,
- ...(isAdmin && selectedUserId
- ? { user_id: selectedUserId }
- : {}),
- });
- }
- if (index === dates.length - 1) {
- return timeApi.createTimeEntry({
- date,
- end_time: newEntry.end_time,
- ...(isAdmin && selectedUserId
- ? { user_id: selectedUserId }
- : {}),
- });
- }
- return timeApi.createTimeEntry({
- date,
- start_time: '00:00',
- end_time: '23:59',
- ...(isAdmin && selectedUserId
- ? { user_id: selectedUserId }
+ if (
+ isAdmin &&
+ !showAllUsers &&
+ !selectedUserId
+ ) {
+ setError(
+ 'Veuillez sélectionner un utilisateur.',
+ );
+ return;
+ }
+
+ try {
+ if (
+ newEntry.date ===
+ newEntry.end_date
+ ) {
+ await timeApi.createTimeEntry({
+ date: newEntry.date,
+ start_time:
+ newEntry.start_time,
+ end_time:
+ newEntry.end_time,
+
+ ...(isAdmin &&
+ selectedUserId
+ ? {
+ user_id:
+ selectedUserId,
+ }
: {}),
});
- }));
+ } 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,
+
+ ...(isAdmin &&
+ selectedUserId
+ ? {
+ user_id:
+ selectedUserId,
+ }
+ : {}),
+ },
+ );
+ }
+
+ if (
+ index ===
+ dates.length - 1
+ ) {
+ return timeApi.createTimeEntry(
+ {
+ date,
+ end_time:
+ newEntry.end_time,
+
+ ...(isAdmin &&
+ selectedUserId
+ ? {
+ user_id:
+ selectedUserId,
+ }
+ : {}),
+ },
+ );
+ }
+
+ return timeApi.createTimeEntry(
+ {
+ date,
+ start_time:
+ '00:00',
+ end_time:
+ '23:59',
+
+ ...(isAdmin &&
+ selectedUserId
+ ? {
+ user_id:
+ selectedUserId,
+ }
+ : {}),
+ },
+ );
+ },
+ ),
+ );
+ }
+
+ setNewEntry({
+ date: new Date()
+ .toISOString()
+ .split('T')[0],
+
+ end_date: new Date()
+ .toISOString()
+ .split('T')[0],
+
+ start_time: '',
+ end_time: '',
+ });
+
+ loadTimeEntries();
+ setError('');
+ } catch (err) {
+ setError(
+ "Erreur lors de l'ajout du pointage",
+ );
+
+ console.error(err);
}
- setNewEntry({
- date: new Date().toISOString().split('T')[0],
- end_date: new Date().toISOString().split('T')[0],
- start_time: '',
- end_time: ''
+ };
+
+
+ /* =========================================================
+ EDIT ENTRY
+ ========================================================= */
+
+ const handleEditEntry =
+ (entry) => {
+ setEditingEntry(entry);
+
+ setEditForm({
+ start_time:
+ entry.start_time,
+ end_time:
+ entry.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;
- }
+ const handleUpdateEntry =
+ async (e) => {
+ e.preventDefault();
- 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);
- }
- };
+ if (
+ !editForm.start_time &&
+ !editForm.end_time
+ ) {
+ setError(
+ 'Veuillez remplir tous les champs',
+ );
- const handleDeleteEntry = async (id) => {
- try {
- await timeApi.deleteTimeEntry(id);
- loadTimeEntries(); // Refresh the list
- setError('');
- } catch (err) {
- setError('Erreur lors de la suppression du pointage');
- console.error(err);
- }
- };
+ return;
+ }
+
+ try {
+ await timeApi.updateTimeEntry(
+ editingEntry.id,
+ editForm,
+ );
+
+ setEditingEntry(null);
+
+ setEditForm({
+ start_time: '',
+ end_time: '',
+ });
+
+ loadTimeEntries();
+ setError('');
+ } catch (err) {
+ setError(
+ 'Erreur lors de la mise à jour du pointage',
+ );
+
+ console.error(err);
+ }
+ };
+
+
+ /* =========================================================
+ DELETE ENTRY
+ ========================================================= */
+
+ const handleDeleteEntry =
+ async (id) => {
+ try {
+ await timeApi.deleteTimeEntry(
+ id,
+ );
+
+ loadTimeEntries();
+ setError('');
+ } catch (err) {
+ setError(
+ 'Erreur lors de la suppression du pointage',
+ );
+
+ console.error(err);
+ }
+ };
+
+
+ /* =========================================================
+ SCANNER
+ ========================================================= */
const stopScanner = () => {
scannerSessionRef.current += 1;
- if (qrControlsRef.current) {
+ if (
+ qrControlsRef.current
+ ) {
try {
qrControlsRef.current.stop();
} catch (stopError) {
- console.warn('Arrêt du lecteur QR déjà effectué.', stopError);
+ console.warn(
+ 'Arrêt du lecteur QR déjà effectué.',
+ stopError,
+ );
}
+
qrControlsRef.current = null;
}
- if (videoRef.current?.srcObject) {
- const stream = videoRef.current.srcObject;
- stream.getTracks().forEach((track) => track.stop());
- videoRef.current.srcObject = null;
+ if (
+ videoRef.current?.srcObject
+ ) {
+ const stream =
+ videoRef.current
+ .srcObject;
+
+ stream
+ .getTracks()
+ .forEach(
+ (track) =>
+ track.stop(),
+ );
+
+ videoRef.current.srcObject =
+ null;
}
};
+
const closeScanner = () => {
stopScanner();
setIsScannerDialogOpen(false);
};
- const handleQrResult = async (code, sessionId) => {
- if (sessionId !== scannerSessionRef.current) return;
- setScanConflict(null);
- closeScanner();
-
- 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 handleQrResult =
+ async (
+ code,
+ sessionId,
+ ) => {
+ if (
+ sessionId !==
+ scannerSessionRef.current
+ ) {
+ return;
}
- }
- };
- 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);
- }
- };
+
+ closeScanner();
+
+ 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(() => {
return () => {
@@ -610,95 +1143,143 @@ const TimeClockPage = () => {
};
}, []);
- useEffect(() => {
- if (!isScannerDialogOpen) return undefined;
- const sessionId = scannerSessionRef.current;
+ useEffect(() => {
+ if (
+ !isScannerDialogOpen
+ ) {
+ return undefined;
+ }
+
+ const sessionId =
+ scannerSessionRef.current;
+
let cancelled = false;
let reader = null;
- const startReader = async () => {
- await new Promise((resolve) => {
- requestAnimationFrame(() => {
- requestAnimationFrame(resolve);
- });
- });
-
- if (
- cancelled ||
- sessionId !== scannerSessionRef.current ||
- !videoRef.current
- ) {
- return;
- }
-
- reader = new BrowserQRCodeReader();
-
- try {
- const controls = await reader.decodeFromConstraints(
- {
- video: {
- facingMode: { ideal: 'environment' },
- },
- audio: false,
- },
- videoRef.current,
- (result) => {
- if (
- !result ||
- cancelled ||
- sessionId !== scannerSessionRef.current
- ) {
- return;
- }
-
- handleQrResult(result.getText(), sessionId);
+ const startReader =
+ async () => {
+ await new Promise(
+ (resolve) => {
+ requestAnimationFrame(
+ () => {
+ requestAnimationFrame(
+ resolve,
+ );
+ },
+ );
},
);
if (
cancelled ||
- sessionId !== scannerSessionRef.current ||
- !isScannerDialogOpen
+ sessionId !==
+ scannerSessionRef.current ||
+ !videoRef.current
) {
- try {
- controls.stop();
- } catch (stopError) {
- console.warn('Arrêt du scanner ignoré.', stopError);
+ return;
+ }
+
+ reader =
+ new BrowserQRCodeReader();
+
+ try {
+ const controls =
+ await reader.decodeFromConstraints(
+ {
+ video: {
+ facingMode: {
+ ideal:
+ 'environment',
+ },
+ },
+ audio: false,
+ },
+ videoRef.current,
+ (result) => {
+ if (
+ !result ||
+ cancelled ||
+ sessionId !==
+ scannerSessionRef.current
+ ) {
+ return;
+ }
+
+ handleQrResult(
+ result.getText(),
+ sessionId,
+ );
+ },
+ );
+
+ if (
+ cancelled ||
+ sessionId !==
+ scannerSessionRef.current ||
+ !isScannerDialogOpen
+ ) {
+ try {
+ controls.stop();
+ } catch (
+ stopError
+ ) {
+ console.warn(
+ 'Arrêt du scanner ignoré.',
+ stopError,
+ );
+ }
+
+ return;
}
- return;
- }
- qrControlsRef.current = controls;
- } catch (scannerError) {
- if (
- cancelled ||
- sessionId !== scannerSessionRef.current
+ qrControlsRef.current =
+ controls;
+ } catch (
+ scannerError
) {
- return;
- }
+ if (
+ cancelled ||
+ sessionId !==
+ scannerSessionRef.current
+ ) {
+ return;
+ }
- console.error('Erreur scanner QR:', scannerError);
-
- const permissionDenied =
- scannerError?.name === 'NotAllowedError' ||
- scannerError?.name === 'PermissionDeniedError';
-
- if (permissionDenied) {
- setCameraPermissionError(true);
- setError(
- 'L’accès à la caméra a été refusé. Autorisez la caméra dans les paramètres du site puis réessayez.',
+ console.error(
+ 'Erreur scanner QR:',
+ scannerError,
);
- } else {
- setCameraPermissionError(false);
- setError(
- 'Impossible d’accéder à la caméra. Vérifiez les permissions et utilisez HTTPS sur le réseau.',
- );
- }
- closeScanner();
- }
- };
+ const permissionDenied =
+ scannerError?.name ===
+ 'NotAllowedError' ||
+ scannerError?.name ===
+ 'PermissionDeniedError';
+
+ if (
+ permissionDenied
+ ) {
+ setCameraPermissionError(
+ true,
+ );
+
+ setError(
+ "L’accès à la caméra a été refusé. Autorisez la caméra dans les paramètres du site puis réessayez.",
+ );
+ } else {
+ setCameraPermissionError(
+ false,
+ );
+
+ setError(
+ "Impossible d’accéder à la caméra. Vérifiez les permissions et utilisez HTTPS sur le réseau.",
+ );
+ }
+
+ closeScanner();
+ }
+ };
startReader();
@@ -707,18 +1288,26 @@ const TimeClockPage = () => {
try {
reader?.reset();
- } catch (resetError) {
+ } catch (
+ resetError
+ ) {
console.warn(
'Réinitialisation du lecteur QR ignorée.',
resetError,
);
}
- if (sessionId === scannerSessionRef.current) {
+ if (
+ sessionId ===
+ scannerSessionRef.current
+ ) {
stopScanner();
}
};
- }, [isScannerDialogOpen]);
+ }, [
+ isScannerDialogOpen,
+ ]);
+
const startScanner = () => {
setError('');
@@ -728,436 +1317,1030 @@ const TimeClockPage = () => {
if (
!window.isSecureContext &&
- window.location.hostname !== 'localhost' &&
- window.location.hostname !== '192.168.1.19'
+ window.location.hostname !==
+ 'localhost' &&
+ window.location.hostname !==
+ '192.168.1.19'
) {
setError(
'La caméra nécessite une connexion HTTPS sur cette adresse réseau.',
);
+
return;
}
- if (!navigator.mediaDevices?.getUserMedia) {
- setError('Ce navigateur ne permet pas l’accès à la caméra.');
+ if (
+ !navigator.mediaDevices
+ ?.getUserMedia
+ ) {
+ setError(
+ 'Ce navigateur ne permet pas l’accès à la caméra.',
+ );
+
return;
}
scannerSessionRef.current += 1;
+
setIsScannerDialogOpen(true);
};
- const requestCameraPermission = async () => {
- setError('');
- if (
- !window.isSecureContext &&
- window.location.hostname !== 'localhost' &&
- window.location.hostname !== '192.168.1.19'
- ) {
- setError(
- 'La caméra nécessite une connexion HTTPS sur cette adresse réseau.',
- );
- return;
- }
-
- if (!navigator.mediaDevices?.getUserMedia) {
- setError('Ce navigateur ne permet pas l’accès à la caméra.');
- return;
- }
-
- try {
- const stream = await navigator.mediaDevices.getUserMedia({
- video: {
- facingMode: { ideal: 'environment' },
- },
- audio: false,
- });
-
- stream.getTracks().forEach((track) => track.stop());
- setCameraPermissionError(false);
- startScanner();
- } catch (permissionError) {
- console.error(
- 'Erreur lors de la demande de permission caméra:',
- permissionError,
- );
+ const requestCameraPermission =
+ async () => {
+ setError('');
if (
- permissionError?.name === 'NotAllowedError' ||
- permissionError?.name === 'PermissionDeniedError'
+ !window.isSecureContext &&
+ window.location.hostname !==
+ 'localhost' &&
+ window.location.hostname !==
+ '192.168.1.19'
) {
- setCameraPermissionError(true);
setError(
- 'La caméra est toujours refusée. Autorisez-la dans les paramètres du site, puis cliquez à nouveau sur « Autoriser la caméra ».',
- );
- } else {
- setError(
- 'Impossible d’obtenir l’accès à la caméra. Vérifiez les permissions du navigateur et la connexion HTTPS.',
+ 'La caméra nécessite une connexion HTTPS sur cette adresse réseau.',
);
+
+ return;
}
- }
- };
- 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')}`;
- };
+ if (
+ !navigator.mediaDevices
+ ?.getUserMedia
+ ) {
+ setError(
+ 'Ce navigateur ne permet pas l’accès à la caméra.',
+ );
- const getDisplayTimeEntries = () => {
- const toMinutes = (time) => {
- const [hours, minutes] = time.split(':').map(Number);
- return hours * 60 + minutes;
+ return;
+ }
+
+ try {
+ const stream =
+ await navigator.mediaDevices.getUserMedia(
+ {
+ video: {
+ facingMode: {
+ ideal:
+ 'environment',
+ },
+ },
+ audio: false,
+ },
+ );
+
+ stream
+ .getTracks()
+ .forEach(
+ (track) =>
+ track.stop(),
+ );
+
+ setCameraPermissionError(
+ false,
+ );
+
+ startScanner();
+ } catch (
+ permissionError
+ ) {
+ console.error(
+ 'Erreur lors de la demande de permission caméra:',
+ permissionError,
+ );
+
+ if (
+ permissionError?.name ===
+ 'NotAllowedError' ||
+ permissionError?.name ===
+ 'PermissionDeniedError'
+ ) {
+ setCameraPermissionError(
+ true,
+ );
+
+ setError(
+ 'La caméra est toujours refusée. Autorisez-la dans les paramètres du site, puis cliquez à nouveau sur « Autoriser la caméra ».',
+ );
+ } else {
+ setError(
+ "Impossible d’obtenir l’accès à la caméra. Vérifiez les permissions du navigateur et la connexion HTTPS.",
+ );
+ }
+ }
};
- 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;
- });
+
+ /* =========================================================
+ DISPLAY HELPERS
+ ========================================================= */
+
+ 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 formatDate = (dateString) => {
- const date = new Date(dateString);
- return date.toLocaleDateString('fr-FR');
+
+ 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');
+
+ 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}`;
};
- const displayedTimeEntries = isAdmin && !showAllUsers && selectedUserId
- ? timeEntries.filter(
- (entry) => getEntryUserId(entry) === String(selectedUserId),
- )
- : timeEntries;
- const selectedUser = users.find(
- (user) => getUserId(user) === String(selectedUserId),
- );
+ const displayedTimeEntries =
+ isAdmin &&
+ !showAllUsers &&
+ selectedUserId
+ ? timeEntries.filter(
+ (entry) =>
+ getEntryUserId(
+ entry,
+ ) ===
+ String(
+ selectedUserId,
+ ),
+ )
+ : timeEntries;
+
+
+ const selectedUser =
+ users.find(
+ (user) =>
+ getUserId(user) ===
+ String(
+ selectedUserId,
+ ),
+ );
+
+
+ /* =========================================================
+ RENDER
+ ========================================================= */
return (
-
-
- Pointage
+
+
+
+
+ Pointage
+
-
+
+
+
+ {/* =========================
+ ALERTES
+ ========================== */}
+
{error && (
-
- {error}
+
+
+ {error}
+
)}
+
{cameraPermissionError && (
-
)}
+
{scanMessage && (
-
- {scanMessage}
+
+
+ {scanMessage}
+
)}
+
+ {/* =========================
+ CONFLIT
+ ========================== */}
+
{scanConflict && (
-
+
- Conflit de pointage
+
+ Conflit de pointage
+
+
- {scanConflict.message}
-
+
+ {scanConflict.message}
+
+
+
+
-
-
resolveScanConflict(scanConflict.type === 'open_entry' ? 'cancel_entry' : 'cancel_exit')}
+
+
+
+
+ resolveScanConflict(
+ scanConflict.type ===
+ 'open_entry'
+ ? 'cancel_entry'
+ : 'cancel_exit',
+ )
+ }
variant="destructive"
>
Annuler le pointage
- resolveScanConflict(scanConflict.type === 'open_entry' ? 'complete_entry' : 'complete_exit')}
+
+
+ resolveScanConflict(
+ scanConflict.type ===
+ 'open_entry'
+ ? 'complete_entry'
+ : 'complete_exit',
+ )
+ }
variant="default"
>
Renseigner l’heure
+
)}
- {/* Time tracking section */}
-
- {isAdmin && (
-
-
-
-
+
+ {/* =====================================================
+ POINTAGE + HISTORIQUE
+ ====================================================== */}
+
+
+
+ {/* =================================================
+ COLONNE GAUCHE
+ POINTAGE
+ ================================================== */}
+
+
+
+ {/* =========================
+ BOUTON POINTER
+ ========================== */}
+
+
+
+ {/* =========================
+ GESTION ADMIN
+ ========================== */}
+
+ {isAdmin && (
+
+
+
+
+ Gestion des pointages
+
+
+
+
+
+ {/* UTILISATEUR */}
+
+
+
+
+
+
+
+
-
-
-
-
- )}
- {isAdmin && !showAllUsers && (
-
+ {/* SEPARATION */}
+
+ {!showAllUsers && (
+
+
+
+
+ {editingEntry
+ ? 'Modifier le pointage'
+ : `Ajouter un pointage pour${
+ selectedUser
+ ? ` ${getUserLabel(
+ selectedUser,
+ )}`
+ : ''
+ }`}
+
+
+
+
+ {/* EDITION */}
+
+ {editingEntry ? (
+
+ ) : (
+
+ /* AJOUT */
+
+
+ )}
+
+
+ )}
+
+
+
+
+ )}
+
+
+
+
+ {/* =================================================
+ COLONNE DROITE
+ HISTORIQUE
+ ================================================== */}
+
+
+
+
+
- {editingEntry
- ? 'Modifier le pointage'
- : `Ajouter un pointage pour${selectedUser ? ` ${getUserLabel(selectedUser)}` : ''}`}
+ Historique des pointages{' '}
+ {!showAllUsers &&
+ `de ${getUserLabel(
+ selectedUser,
+ )}`}
+
+
- {editingEntry ? (
-
- ) : (
-
- )}
-
-
- )}
-
- {/* Pointage uniquement disponible sur mobile */}
-
-
-
-
- {/* Sur ordinateur, le pointage est volontairement indisponible */}
- {!isAdmin &&
-
-
- Le pointage est indisponible sur ordinateur.
-
-
- }
-
-
-
-
- Historique des pointages {
- !showAllUsers && `de ${getUserLabel(selectedUser)}`
- }
-
-
{loading ? (
- Chargement des pointages...
- ) : displayedTimeEntries.length === 0 ? (
- Aucun pointage enregistré
+
+ Chargement des
+ pointages...
+
+ ) : displayedTimeEntries.length ===
+ 0 ? (
+
+ Aucun pointage
+ enregistré
+
) : (
+
+
+
+
- {isAdmin && Nom}
- Date
- Entrée
- Sortie
- Durée
- {isAdmin && Actions}
+
+ {isAdmin && (
+
+ Nom
+
+ )}
+
+
+ Date
+
+
+
+ Entrée
+
+
+
+ Sortie
+
+
+
+ Durée
+
+
+ {isAdmin && (
+
+ Actions
+
+ )}
+
+
+
+
+
{getDisplayTimeEntries()
.filter(
(entry) =>
!isAdmin ||
showAllUsers ||
- getEntryUserId(entry) === String(selectedUserId),
+ getEntryUserId(
+ entry,
+ ) ===
+ String(
+ selectedUserId,
+ ),
)
- .map((entry) => (
-
- {isAdmin && {getEntryUserLabel(entry)}}
- {formatDate(entry.date)}
- {formatTime(entry.start_time)}
- {formatTime(entry.end_time)}
- {formatDuration(entry.duration)}
- {isAdmin && (
-
-
- handleEditEntry(entry)} variant="secondary" size="sm">Modifier
- handleDeleteEntry(entry.id)}>
- Supprimer
-
-
-
- )}
-
- ))}
+ .map(
+ (entry) => (
+
+
+ {isAdmin && (
+
+ {getEntryUserLabel(
+ entry,
+ )}
+
+ )}
+
+
+ {formatDate(
+ entry.date,
+ )}
+
+
+
+ {formatTime(
+ entry.start_time,
+ )}
+
+
+
+ {formatTime(
+ entry.end_time,
+ )}
+
+
+
+
+ {formatDuration(
+ entry.duration,
+ )}
+
+
+
+
+ {isAdmin && (
+
+
+
+
+
+ handleEditEntry(
+ entry,
+ )
+ }
+ variant="secondary"
+ size="sm"
+ >
+ Modifier
+
+
+
+
+ handleDeleteEntry(
+ entry.id,
+ )
+ }
+ >
+
+
+ Supprimer
+
+
+
+
+
+
+
+ )}
+
+
+ ),
+ )}
+
+
+
)}
+
+
+
+
+
);
};
+
export default TimeClockPage;
\ No newline at end of file