diff --git a/backend/time_tracking/views.py b/backend/time_tracking/views.py
index 01568f6..73fe21c 100644
--- a/backend/time_tracking/views.py
+++ b/backend/time_tracking/views.py
@@ -282,8 +282,8 @@ class TimeStatsView(APIView):
def get(self, request):
period = request.query_params.get('period', 'month')
- if period not in ('day', 'month', 'year'):
- raise ValidationError({'period': 'Vue inconnue. Choisissez jour, mois ou année.'})
+ if period not in ('day', 'week', 'month', 'year'):
+ raise ValidationError({'period': 'Vue inconnue. Choisissez jour, semaine, mois ou année.'})
anchor_raw = request.query_params.get('anchor') or timezone.localdate().isoformat()
try:
@@ -307,6 +307,9 @@ class TimeStatsView(APIView):
if period == 'day':
start = end = anchor
+ elif period == 'week':
+ start = anchor - timedelta(days=anchor.weekday())
+ end = start + timedelta(days=6)
elif period == 'month':
start = anchor.replace(day=1)
end = anchor.replace(day=monthrange(anchor.year, anchor.month)[1])
diff --git a/frontend/src/components/statistics/PersonalStatistics.jsx b/frontend/src/components/statistics/PersonalStatistics.jsx
index a9ad7da..ac04e2c 100644
--- a/frontend/src/components/statistics/PersonalStatistics.jsx
+++ b/frontend/src/components/statistics/PersonalStatistics.jsx
@@ -2,15 +2,9 @@ import { useEffect, useState } from 'react';
import { BarChart3, CalendarDays, CarFront, Clock3, RefreshCw, Sun } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
-import { DatePicker } from '@/components/ResponsivePickers';
import { timeApi } from '@/api/time';
import { dateKey, formatMinutes } from '@/components/planning/utils';
-
-const PERIODS = [
- { key: 'day', label: 'Jour' },
- { key: 'month', label: 'Mois' },
- { key: 'year', label: 'Année' },
-];
+import StatisticsPeriodSelector from '@/components/statistics/StatisticsPeriodSelector';
const StatCard = ({ icon: Icon, label, value }) => (
@@ -37,6 +31,12 @@ const rangeLabel = (stats) => {
year: 'numeric',
});
}
+ if (stats.period === 'week') {
+ const end = new Date(`${stats.end}T12:00:00`);
+ const startText = start.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' });
+ const endText = end.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' });
+ return `Du ${startText} au ${endText}`;
+ }
if (stats.period === 'month') {
return start.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
}
@@ -72,8 +72,6 @@ export default function PersonalStatistics() {
return () => controller.abort();
}, [period, anchor, reload]);
- const monthValue = anchor.slice(0, 7);
- const yearValue = anchor.slice(0, 4);
return (
@@ -94,29 +92,12 @@ export default function PersonalStatistics() {
-
- Période
-
- {PERIODS.map((item) => (
-
- ))}
-
-
-
-
+
{error && (
diff --git a/frontend/src/components/statistics/StatisticsPeriodSelector.jsx b/frontend/src/components/statistics/StatisticsPeriodSelector.jsx
new file mode 100644
index 0000000..9e21926
--- /dev/null
+++ b/frontend/src/components/statistics/StatisticsPeriodSelector.jsx
@@ -0,0 +1,158 @@
+import { ChevronLeft, ChevronRight } from 'lucide-react';
+import { DatePicker } from '@/components/ResponsivePickers';
+import { Button } from '@/components/ui/button';
+
+const PERIODS = [
+ { key: 'day', label: 'Jour' },
+ { key: 'week', label: 'Semaine' },
+ { key: 'month', label: 'Mois' },
+ { key: 'year', label: 'Année' },
+];
+
+const parseAnchor = (value) => {
+ const [year, month, day] = String(value || '').split('-').map(Number);
+ if (!year || !month || !day) return new Date();
+ return new Date(year, month - 1, day, 12, 0, 0, 0);
+};
+
+const dateKey = (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 startOfWeek = (value) => {
+ const date = parseAnchor(value);
+ const weekday = (date.getDay() + 6) % 7;
+ date.setDate(date.getDate() - weekday);
+ return dateKey(date);
+};
+
+const normalizedAnchorForPeriod = (value, period) => {
+ const date = parseAnchor(value);
+ if (period === 'week') return startOfWeek(value);
+ if (period === 'month') {
+ date.setDate(1);
+ return dateKey(date);
+ }
+ if (period === 'year') {
+ date.setMonth(0, 1);
+ return dateKey(date);
+ }
+ return dateKey(date);
+};
+
+const shiftAnchor = (value, period, direction) => {
+ const date = parseAnchor(normalizedAnchorForPeriod(value, period));
+
+ if (period === 'day') {
+ date.setDate(date.getDate() + direction);
+ } else if (period === 'week') {
+ date.setDate(date.getDate() + (7 * direction));
+ } else if (period === 'month') {
+ date.setMonth(date.getMonth() + direction, 1);
+ } else {
+ date.setFullYear(date.getFullYear() + direction, 0, 1);
+ }
+
+ return dateKey(date);
+};
+
+export default function StatisticsPeriodSelector({ period, anchor, onPeriodChange, onAnchorChange, className = '' }) {
+ const monthValue = anchor.slice(0, 7);
+ const yearValue = anchor.slice(0, 4);
+
+ const changePeriod = (nextPeriod) => {
+ onPeriodChange(nextPeriod);
+ onAnchorChange(normalizedAnchorForPeriod(anchor, nextPeriod));
+ };
+
+ const move = (direction) => {
+ onAnchorChange(shiftAnchor(anchor, period, direction));
+ };
+
+ const picker = (() => {
+ if (period === 'month') {
+ return (
+ onAnchorChange(`${value}-01`)}
+ />
+ );
+ }
+
+ if (period === 'year') {
+ return (
+ onAnchorChange(`${value}-01-01`)}
+ />
+ );
+ }
+
+ return (
+ onAnchorChange(period === 'week' ? startOfWeek(value) : value)}
+ />
+ );
+ })();
+
+ return (
+
+
+ Période
+
+ {PERIODS.map((item) => (
+
+ ))}
+
+
+
+
+ {period === 'day' ? 'Date' : period === 'week' ? 'Semaine' : period === 'month' ? 'Mois' : 'Année'}
+
+
+
+
{picker}
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/UserStatisticsPage.jsx b/frontend/src/pages/UserStatisticsPage.jsx
index 6c97dc2..7b58390 100644
--- a/frontend/src/pages/UserStatisticsPage.jsx
+++ b/frontend/src/pages/UserStatisticsPage.jsx
@@ -2,19 +2,13 @@ import { useEffect, useMemo, useState } from 'react';
import { BarChart3, CalendarDays, CarFront, Clock3, RefreshCw, Sun } from 'lucide-react';
import MainCard from '@/components/MainCard';
import LoadingCard from '@/components/LoadingCard';
-import { DatePicker } from '@/components/ResponsivePickers';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { timeApi } from '@/api/time';
import { planningApi } from '@/api/planning';
import { useAuth } from '@/context/useAuth';
import { dateKey, formatMinutes, personLabel, inputClass } from '@/components/planning/utils';
-
-const PERIODS = [
- { key: 'day', label: 'Jour' },
- { key: 'month', label: 'Mois' },
- { key: 'year', label: 'Année' },
-];
+import StatisticsPeriodSelector from '@/components/statistics/StatisticsPeriodSelector';
const StatCard = ({ icon: Icon, label, value, detail }) => (
@@ -38,6 +32,11 @@ const rangeLabel = (stats) => {
if (stats.period === 'day') {
return start.toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
}
+ if (stats.period === 'week') {
+ const startText = start.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' });
+ const endText = end.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' });
+ return `Du ${startText} au ${endText}`;
+ }
if (stats.period === 'month') {
return start.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
}
@@ -102,8 +101,6 @@ export default function UserStatisticsPage() {
return
;
}
- const monthValue = anchor.slice(0, 7);
- const yearValue = anchor.slice(0, 4);
return (
@@ -119,29 +116,12 @@ export default function UserStatisticsPage() {
-
- Période
-
- {PERIODS.map((item) => (
-
- ))}
-
-
-
-
+