From 1923feb5bb8fa318fbb6be13df9e6f3e6a1ea360 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 5 May 2026 15:13:00 +0200 Subject: [PATCH] feat: enhance TrainingPlanningPage with calendar view and date handling - Introduced a calendar view option for training planning, allowing users to switch between list and calendar layouts. - Added functions for managing calendar grid ranges and enumerating ISO days, improving date handling. - Updated state management to include calendar month and view type, enhancing user experience. - Refactored loadUnits function to adapt to the selected view, ensuring accurate data loading based on the chosen layout. --- frontend/src/pages/TrainingPlanningPage.jsx | 380 ++++++++++++++++++-- 1 file changed, 358 insertions(+), 22 deletions(-) diff --git a/frontend/src/pages/TrainingPlanningPage.jsx b/frontend/src/pages/TrainingPlanningPage.jsx index dda19d7..15d7420 100644 --- a/frontend/src/pages/TrainingPlanningPage.jsx +++ b/frontend/src/pages/TrainingPlanningPage.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react' +import React, { useState, useEffect, useCallback, useMemo } from 'react' import { Link } from 'react-router-dom' import api from '../utils/api' import { useAuth } from '../context/AuthContext' @@ -19,6 +19,59 @@ function addDaysIsoDate(isoDay, daysDelta) { return d.toISOString().slice(0, 10) } +function pad2(n) { + return String(n).padStart(2, '0') +} + +function toIsoLocal(d) { + return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}` +} + +/** Montag = erster Wochentag (ISO-Woche UI) */ +function mondayIndex(d) { + return (d.getDay() + 6) % 7 +} + +/** Kalendarische Monatsansicht: erster und letzter Tag des sichtbaren Rasters (Mo–So) */ +function getCalendarGridRange(ym) { + const parts = (ym || '').split('-').map(Number) + const y = parts[0] + const m = parts[1] + if (!y || !m || m < 1 || m > 12) { + const t = new Date() + return { gridStart: toIsoLocal(t), gridEnd: toIsoLocal(t) } + } + const first = new Date(y, m - 1, 1) + const last = new Date(y, m, 0) + const gridStart = new Date(first) + gridStart.setDate(first.getDate() - mondayIndex(first)) + const lastMon = mondayIndex(last) + const gridEnd = new Date(last) + gridEnd.setDate(last.getDate() + (6 - lastMon)) + return { gridStart: toIsoLocal(gridStart), gridEnd: toIsoLocal(gridEnd) } +} + +function shiftCalendarMonth(ym, delta) { + const parts = (ym || '').split('-').map(Number) + const y = parts[0] || new Date().getFullYear() + const m = parts[1] || 1 + const d = new Date(y, m - 1 + delta, 1) + return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}` +} + +function enumerateIsoDays(fromIso, toIso) { + const out = [] + const cur = new Date(`${fromIso}T12:00:00`) + const end = new Date(`${toIso}T12:00:00`) + while (cur <= end) { + out.push(toIsoLocal(cur)) + cur.setDate(cur.getDate() + 1) + } + return out +} + +const WEEKDAYS_DE = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'] + function TrainingPlanningPage() { const { user } = useAuth() const [groups, setGroups] = useState([]) @@ -50,6 +103,8 @@ function TrainingPlanningPage() { const [startDate, setStartDate] = useState(today) const [endDate, setEndDate] = useState(thirtyDaysLater) + const [planView, setPlanView] = useState('list') + const [calendarMonthStr, setCalendarMonthStr] = useState(() => today.slice(0, 7)) const [formData, setFormData] = useState({ group_id: '', @@ -75,7 +130,7 @@ function TrainingPlanningPage() { if (selectedGroupId) { loadUnits() } - }, [selectedGroupId, startDate, endDate]) + }, [selectedGroupId, loadUnits]) useEffect(() => { if (!frameworkImportOpen) return @@ -239,19 +294,26 @@ function TrainingPlanningPage() { } } - const loadUnits = async () => { + const loadUnits = useCallback(async () => { if (!selectedGroupId) return + let start = startDate + let end = endDate + if (planView === 'calendar') { + const r = getCalendarGridRange(calendarMonthStr) + start = r.gridStart + end = r.gridEnd + } try { const unitsData = await api.listTrainingUnits({ group_id: selectedGroupId, - start_date: startDate, - end_date: endDate + start_date: start, + end_date: end }) setUnits(unitsData) } catch (err) { console.error('Failed to load units:', err) } - } + }, [selectedGroupId, startDate, endDate, planView, calendarMonthStr]) const handleQuickCreate = async () => { if (!selectedGroupId) { @@ -301,6 +363,32 @@ function TrainingPlanningPage() { setShowModal(true) } + const handleCreateForDate = (isoDay) => { + if (!selectedGroupId) { + alert('Bitte wähle zuerst eine Trainingsgruppe') + return + } + const group = groups.find((g) => g.id === parseInt(selectedGroupId, 10)) + setEditingUnit(null) + setDraftPlanTemplateId('') + setFormData({ + group_id: selectedGroupId, + planned_date: isoDay, + planned_time_start: group?.time_start?.slice(0, 5) || '', + planned_time_end: group?.time_end?.slice(0, 5) || '', + planned_focus: '', + actual_date: '', + actual_time_start: '', + actual_time_end: '', + attendance_count: '', + status: 'planned', + notes: '', + trainer_notes: '', + sections: [defaultSection('Hauptteil')] + }) + setShowModal(true) + } + const applyTemplateFromSelect = async (templateId) => { setDraftPlanTemplateId(templateId) if (!templateId) return @@ -422,6 +510,31 @@ function TrainingPlanningPage() { setFormData((prev) => ({ ...prev, [field]: value })) } + const calendarGridDays = useMemo(() => { + const r = getCalendarGridRange(calendarMonthStr) + return enumerateIsoDays(r.gridStart, r.gridEnd) + }, [calendarMonthStr]) + + const unitsByPlannedDate = useMemo(() => { + const m = new Map() + for (const u of units) { + const raw = u.planned_date + if (!raw) continue + const key = String(raw).slice(0, 10) + if (!m.has(key)) m.set(key, []) + m.get(key).push(u) + } + return m + }, [units]) + + const calendarMonthTitle = useMemo(() => { + const p = calendarMonthStr.split('-').map(Number) + const y = p[0] + const mo = p[1] + if (!y || !mo) return '' + return new Date(y, mo - 1, 1).toLocaleDateString('de-DE', { month: 'long', year: 'numeric' }) + }, [calendarMonthStr]) + if (loading) { return (
@@ -496,24 +609,110 @@ function TrainingPlanningPage() {
- - setStartDate(e.target.value)} - /> + + Ansicht + +
+ + +
+

+ Liste: Zeitraum filtern · Kalender: Monatsraster aus Gruppenterminen (Mo–So) +

-
- - setEndDate(e.target.value)} - /> -
+ {planView === 'list' ? ( + <> +
+ + setStartDate(e.target.value)} + /> +
+ +
+ + setEndDate(e.target.value)} + /> +
+ + ) : ( +
+ + + {calendarMonthTitle} + + + +
+ )} {selectedGroup && ( @@ -624,6 +823,143 @@ function TrainingPlanningPage() { Wähle oben eine Trainingsgruppe — danach kannst du mit „Neue Trainingseinheit planen“ starten.

+ ) : planView === 'calendar' ? ( +
+ {units.length === 0 ? ( +

+ Im sichtbaren Monatsbereich liegt noch keine Einheit. Über + in einem Tag legst du einen + neuen Termin mit Datum an. +

+ ) : null} +
+ {WEEKDAYS_DE.map((w) => ( +
+ {w} +
+ ))} + {calendarGridDays.map((dayIso) => { + const inMonth = dayIso.slice(0, 7) === calendarMonthStr + const dayNum = parseInt(dayIso.slice(8, 10), 10) + const isTodayMarker = dayIso === today + const dayUnits = unitsByPlannedDate.get(dayIso) || [] + return ( +
+
+ {dayNum} + +
+
+ {dayUnits.slice(0, 3).map((unit) => ( + + ))} +
+ {dayUnits.length > 3 ? ( +

+ +{dayUnits.length - 3} weitere +

+ ) : null} +
+ ) + })} +
+
) : units.length === 0 ? (