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 (
+ Liste: Zeitraum filtern · Kalender: Monatsraster aus Gruppenterminen (Mo–So) +
+ Im sichtbaren Monatsbereich liegt noch keine Einheit. Über + in einem Tag legst du einen + neuen Termin mit Datum an. +
+ ) : null} ++ +{dayUnits.length - 3} weitere +
+ ) : null} +