diff --git a/frontend/src/components/ResponsivePickers.jsx b/frontend/src/components/ResponsivePickers.jsx
new file mode 100644
index 0000000..5206151
--- /dev/null
+++ b/frontend/src/components/ResponsivePickers.jsx
@@ -0,0 +1,537 @@
+import { useRef, useState } from 'react';
+import { Calendar } from '@/components/ui/calendar';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { CalendarDays, Clock } from 'lucide-react';
+import { fr } from 'date-fns/locale';
+
+const parseDateValue = (value) => {
+ if (!value) return undefined;
+
+ const [
+ year,
+ month,
+ day,
+ ] = value.split('-').map(Number);
+
+ if (!year || !month || !day) {
+ return undefined;
+ }
+
+ 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');
+
+ return `${year}-${month}-${day}`;
+};
+
+
+const formatDateLabel = (value) => {
+ const date =
+ parseDateValue(value);
+
+ if (!date) {
+ return 'Choisir une date';
+ }
+
+ const numericDate =
+ date.toLocaleDateString(
+ 'fr-FR',
+ {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ },
+ );
+
+ const longDate =
+ date.toLocaleDateString(
+ 'fr-FR',
+ {
+ day: '2-digit',
+ month: 'long',
+ year: 'numeric',
+ },
+ );
+
+ return (
+ <>
+
+ {longDate}
+
+
+
+ {numericDate}
+
+ >
+ );
+};
+
+
+const DesktopDatePicker = ({
+ value,
+ onChange,
+ min,
+ placeholder = 'Choisir une date',
+}) => {
+ const [open, setOpen] =
+ useState(false);
+
+ const selectedDate =
+ parseDateValue(value);
+
+ const minDate =
+ parseDateValue(min);
+
+ return (
+
+
+ }
+ >
+
+
+
+ {selectedDate
+ ? formatDateLabel(value)
+ : placeholder}
+
+
+
+
+ {
+ if (!date) return;
+
+ onChange(
+ formatDateValue(date),
+ );
+
+ setOpen(false);
+ }}
+ />
+
+
+ );
+};
+
+
+/* =========================================================
+ TIME PICKER
+========================================================= */
+
+const DesktopTimePicker = ({
+ value,
+ onChange
+}) => {
+ const [open, setOpen] = useState(false);
+ const timeRef = useRef(value);
+ timeRef.current = value;
+ const mouseState = useRef({ active: false, lastY: 0, accumulated: 0 });
+
+ const touchState = useRef({
+ active: false,
+ lastY: 0,
+ accumulated: 0,
+ });
+
+ const [hours = '', minutes = ''] = value
+ ? value.split(':')
+ : [];
+
+ const hourOptions = Array.from(
+ { length: 24 },
+ (_, i) => String(i).padStart(2, '0'),
+ );
+
+ const minuteOptions = Array.from(
+ { length: 60 },
+ (_, i) => String(i).padStart(2, '0'),
+ );
+
+ const updateTime = (
+ nextHours,
+ nextMinutes,
+ ) => {
+ const nextValue = `${String(nextHours).padStart(2, '0')}:${String(
+ nextMinutes,
+ ).padStart(2, '0')}`;
+ timeRef.current = nextValue;
+ onChange(nextValue);
+ };
+
+ const changeHour = (direction) => {
+ const [currentHour = 0, currentMinute = 0] =
+ (timeRef.current || '00:00').split(':').map(Number);
+ updateTime((currentHour + direction % 24 + 24) % 24, currentMinute);
+ };
+
+ const changeMinute = (direction) => {
+ const [currentHour = 0, currentMinute = 0] =
+ (timeRef.current || '00:00').split(':').map(Number);
+ const total = ((currentHour * 60 + currentMinute + direction) % 1440 + 1440) % 1440;
+ updateTime(Math.floor(total / 60), total % 60);
+ };
+
+ const handleMouseDown = (event) => {
+ if (event.pointerType !== 'mouse' || event.button !== 0) return;
+ event.preventDefault();
+ event.currentTarget.setPointerCapture(event.pointerId);
+ mouseState.current = { active: true, lastY: event.clientY, accumulated: 0 };
+ };
+
+ const handleMouseMove = (event, type) => {
+ if (!mouseState.current.active || event.pointerType !== 'mouse') return;
+ const state = mouseState.current;
+ state.accumulated += state.lastY - event.clientY;
+ state.lastY = event.clientY;
+ const steps = Math.trunc(state.accumulated / 18);
+ if (!steps) return;
+ state.accumulated -= steps * 18;
+ if (type === 'hours') changeHour(steps);
+ else changeMinute(steps);
+ };
+
+ const handleMouseUp = () => {
+ mouseState.current.active = false;
+ mouseState.current.accumulated = 0;
+ };
+
+ const handleWheel = (event, type) => {
+ event.stopPropagation();
+
+ const direction =
+ event.deltaY > 0 ? 1 : -1;
+
+ if (type === 'hours') {
+ changeHour(direction);
+ } else {
+ changeMinute(direction);
+ }
+ };
+
+ const handleTouchStart = (event) => {
+ const touch = event.touches[0];
+
+ touchState.current = {
+ active: true,
+ lastY: touch.clientY,
+ accumulated: 0,
+ };
+ };
+
+ const handleTouchMove = (event, type) => {
+ if (!touchState.current.active) {
+ return;
+ }
+
+ const touch = event.touches[0];
+ const currentY = touch.clientY;
+
+ const deltaY =
+ touchState.current.lastY - currentY;
+
+ touchState.current.lastY = currentY;
+
+ touchState.current.accumulated += deltaY;
+
+ const pixelsPerUnit = 18;
+
+ const steps = Math.trunc(
+ touchState.current.accumulated /
+ pixelsPerUnit,
+ );
+
+ if (steps === 0) {
+ return;
+ }
+
+ touchState.current.accumulated -=
+ steps * pixelsPerUnit;
+
+ if (type === 'hours') {
+ changeHour(steps);
+ } else {
+ changeMinute(steps);
+ }
+
+ event.stopPropagation();
+ };
+
+ const handleTouchEnd = () => {
+ touchState.current.active = false;
+ touchState.current.accumulated = 0;
+ };
+
+ const clearTime = () => {
+ onChange('');
+ setOpen(false);
+ };
+
+ return (
+
+
+ }
+ >
+
+
+
+ {value ? (
+ value
+ ) : (
+ <>
+
+ Choisir une heure
+
+
+
+ Choisir
+
+ >
+ )}
+
+
+
+
+
+
+ Choisir une heure
+
+
+
+ {value || '--:--'}
+
+
+
+
+
+
+
handleMouseMove(event, 'hours')}
+ onPointerUp={handleMouseUp}
+ onLostPointerCapture={handleMouseUp}
+ onWheel={(event) =>
+ handleWheel(
+ event,
+ 'hours',
+ )
+ }
+ onTouchStart={handleTouchStart}
+ onTouchMove={(event) =>
+ handleTouchMove(
+ event,
+ 'hours',
+ )
+ }
+ onTouchEnd={handleTouchEnd}
+ onTouchCancel={handleTouchEnd}
+ >
+
+
+ {
+ hourOptions[
+ (Number(hours || 0) +
+ 23) %
+ 24
+ ]
+ }
+
+
+
+ {hours || '00'}
+
+
+
+ {
+ hourOptions[
+ (Number(hours || 0) +
+ 1) %
+ 24
+ ]
+ }
+
+
+
+
+
+
+ :
+
+
+
+
handleMouseMove(event, 'minutes')}
+ onPointerUp={handleMouseUp}
+ onLostPointerCapture={handleMouseUp}
+ onWheel={(event) =>
+ handleWheel(
+ event,
+ 'minutes',
+ )
+ }
+ onTouchStart={handleTouchStart}
+ onTouchMove={(event) =>
+ handleTouchMove(
+ event,
+ 'minutes',
+ )
+ }
+ onTouchEnd={handleTouchEnd}
+ onTouchCancel={handleTouchEnd}
+ >
+
+
+ {
+ minuteOptions[
+ (Number(minutes || 0) +
+ 59) %
+ 60
+ ]
+ }
+
+
+
+ {minutes || '00'}
+
+
+
+ {
+ minuteOptions[
+ (Number(minutes || 0) +
+ 1) %
+ 60
+ ]
+ }
+
+
+
+
+
+
+ Glissez pour modifier
+
+
+
+
+
+
+
+ );
+};
+
+
+// Les champs natifs ouvrent le sélecteur du téléphone.
+// Sur ordinateur, on conserve le calendrier et la molette de TimeClock.
+export const DatePicker = ({ value, onChange, min, placeholder, id }) => (
+ <>
+ onChange(event.target.value)}
+ className="block w-full min-w-0 md:hidden"
+ />
+
+
+
+ >
+);
+
+export const TimePicker = ({ value, onChange, id }) => (
+ <>
+ onChange(event.target.value)}
+ className="block w-full min-w-0 md:hidden"
+ />
+
+
+
+ >
+);
diff --git a/frontend/src/pages/CalendarPage.jsx b/frontend/src/pages/CalendarPage.jsx
index 431c40f..7fa8bff 100644
--- a/frontend/src/pages/CalendarPage.jsx
+++ b/frontend/src/pages/CalendarPage.jsx
@@ -17,6 +17,7 @@ import LoadingCard from '@/components/LoadingCard';
import MainCard from '@/components/MainCard';
import HoverInfo from '@/components/HoverInfo';
+import { DatePicker, TimePicker } from '@/components/ResponsivePickers';
import {
@@ -2358,27 +2359,21 @@ const CalendarPage = () => {
-
+ onChange={(value) =>
handleStartDateChange(
- event.target.value,
+ value,
)
@@ -2396,12 +2391,10 @@ const CalendarPage = () => {
- {
}
- onChange={(
-
- event,
-
- ) =>
+ onChange={(value) =>
handleFormChange(
'end_date',
- event.target.value,
+ value,
)
@@ -2456,29 +2445,23 @@ const CalendarPage = () => {
-
+ onChange={(value) =>
handleFormChange(
'start_time',
- event.target.value,
+ value,
)
@@ -2502,29 +2485,23 @@ const CalendarPage = () => {
-
+ onChange={(value) =>
handleFormChange(
'end_time',
- event.target.value,
+ value,
)
diff --git a/frontend/src/pages/TimeClockPage.jsx b/frontend/src/pages/TimeClockPage.jsx
index 6244c2e..bdb53b2 100644
--- a/frontend/src/pages/TimeClockPage.jsx
+++ b/frontend/src/pages/TimeClockPage.jsx
@@ -31,6 +31,7 @@ import {
import { Badge } from '@/components/ui/badge';
import ConfirmDialog from '@/components/ConfirmDialog';
+import { DatePicker, TimePicker } from '@/components/ResponsivePickers';
import {
Dialog,
@@ -39,16 +40,9 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
-import { Calendar } from '@/components/ui/calendar';
+
import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from '@/components/ui/popover';
-
-import {
- CalendarDays,
Clock,
} from 'lucide-react';
@@ -95,24 +89,6 @@ const parseDateValue = (value) => {
};
-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');
-
- return `${year}-${month}-${day}`;
-};
-
-
const formatDateLabel = (value) => {
const date =
parseDateValue(value);
@@ -183,426 +159,6 @@ const formatDate = (
DATE PICKER
========================================================= */
-const DatePicker = ({
- value,
- onChange,
- min,
- placeholder = 'Choisir une date',
-}) => {
- const [open, setOpen] =
- useState(false);
-
- const selectedDate =
- parseDateValue(value);
-
- const minDate =
- parseDateValue(min);
-
- return (
-
-
- }
- >
-
-
-
- {selectedDate
- ? formatDateLabel(value)
- : placeholder}
-
-
-
-
- {
- if (!date) return;
-
- onChange(
- formatDateValue(date),
- );
-
- setOpen(false);
- }}
- />
-
-
- );
-};
-
-
-/* =========================================================
- TIME PICKER
-========================================================= */
-
-const TimePicker = ({
- value,
- onChange
-}) => {
- const [open, setOpen] = useState(false);
-
- const touchState = useRef({
- active: false,
- lastY: 0,
- accumulated: 0,
- });
-
- const [hours = '', minutes = ''] = value
- ? value.split(':')
- : [];
-
- const hourOptions = Array.from(
- { length: 24 },
- (_, i) => String(i).padStart(2, '0'),
- );
-
- const minuteOptions = Array.from(
- { length: 60 },
- (_, i) => String(i).padStart(2, '0'),
- );
-
- const updateTime = (
- nextHours,
- nextMinutes,
- ) => {
- onChange(
- `${String(nextHours).padStart(2, '0')}:${String(
- nextMinutes,
- ).padStart(2, '0')}`,
- );
- };
-
- const changeHour = (direction) => {
- const currentHour = Number(hours || 0);
- const currentMinute = Number(minutes || 0);
-
- let nextHour =
- currentHour + direction;
-
- if (nextHour < 0) {
- nextHour = 23;
- }
-
- if (nextHour > 23) {
- nextHour = 0;
- }
-
- updateTime(
- nextHour,
- currentMinute,
- );
- };
-
- const changeMinute = (direction) => {
- const currentHour = Number(hours || 0);
- const currentMinute = Number(minutes || 0);
-
- let nextMinute =
- currentMinute + direction;
-
- let nextHour = currentHour;
-
- if (nextMinute < 0) {
- nextMinute = 59;
-
- nextHour =
- currentHour === 0
- ? 23
- : currentHour - 1;
- }
-
- if (nextMinute > 59) {
- nextMinute = 0;
-
- nextHour =
- currentHour === 23
- ? 0
- : currentHour + 1;
- }
-
- updateTime(
- nextHour,
- nextMinute,
- );
- };
-
- const handleWheel = (event, type) => {
- event.stopPropagation();
-
- const direction =
- event.deltaY > 0 ? 1 : -1;
-
- if (type === 'hours') {
- changeHour(direction);
- } else {
- changeMinute(direction);
- }
- };
-
- const handleTouchStart = (event) => {
- const touch = event.touches[0];
-
- touchState.current = {
- active: true,
- lastY: touch.clientY,
- accumulated: 0,
- };
- };
-
- const handleTouchMove = (event, type) => {
- if (!touchState.current.active) {
- return;
- }
-
- const touch = event.touches[0];
- const currentY = touch.clientY;
-
- const deltaY =
- touchState.current.lastY - currentY;
-
- touchState.current.lastY = currentY;
-
- touchState.current.accumulated += deltaY;
-
- const pixelsPerUnit = 18;
-
- const steps = Math.trunc(
- touchState.current.accumulated /
- pixelsPerUnit,
- );
-
- if (steps === 0) {
- return;
- }
-
- touchState.current.accumulated -=
- steps * pixelsPerUnit;
-
- if (type === 'hours') {
- changeHour(steps);
- } else {
- changeMinute(steps);
- }
-
- event.stopPropagation();
- };
-
- const handleTouchEnd = () => {
- touchState.current.active = false;
- touchState.current.accumulated = 0;
- };
-
- const clearTime = () => {
- onChange('');
- setOpen(false);
- };
-
- return (
-
-
- }
- >
-
-
-
- {value ? (
- value
- ) : (
- <>
-
- Choisir une heure
-
-
-
- Choisir
-
- >
- )}
-
-
-
-
-
-
- Choisir une heure
-
-
-
- {value || '--:--'}
-
-
-
-
-
-
-
- handleWheel(
- event,
- 'hours',
- )
- }
- onTouchStart={handleTouchStart}
- onTouchMove={(event) =>
- handleTouchMove(
- event,
- 'hours',
- )
- }
- onTouchEnd={handleTouchEnd}
- onTouchCancel={handleTouchEnd}
- >
-
-
- {
- hourOptions[
- (Number(hours || 0) +
- 23) %
- 24
- ]
- }
-
-
-
- {hours || '00'}
-
-
-
- {
- hourOptions[
- (Number(hours || 0) +
- 1) %
- 24
- ]
- }
-
-
-
-
-
-
- :
-
-
-
-
- handleWheel(
- event,
- 'minutes',
- )
- }
- onTouchStart={handleTouchStart}
- onTouchMove={(event) =>
- handleTouchMove(
- event,
- 'minutes',
- )
- }
- onTouchEnd={handleTouchEnd}
- onTouchCancel={handleTouchEnd}
- >
-
-
- {
- minuteOptions[
- (Number(minutes || 0) +
- 59) %
- 60
- ]
- }
-
-
-
- {minutes || '00'}
-
-
-
- {
- minuteOptions[
- (Number(minutes || 0) +
- 1) %
- 60
- ]
- }
-
-
-
-
-
-
- Glissez pour modifier
-
-
-
-
-
-
-
- );
-};
-
-
/* =========================================================
PAGE
========================================================= */