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) => ( - - ))} -
-
- - +