diff --git a/backend/migrations/044_training_unit_debrief_completed.sql b/backend/migrations/044_training_unit_debrief_completed.sql
new file mode 100644
index 0000000..b34459f
--- /dev/null
+++ b/backend/migrations/044_training_unit_debrief_completed.sql
@@ -0,0 +1,10 @@
+-- Rückschau / Nachbereitung: explizit abschließbar (Dashboard & Filter)
+ALTER TABLE training_units
+ ADD COLUMN IF NOT EXISTS debrief_completed_at TIMESTAMPTZ NULL;
+
+COMMENT ON COLUMN training_units.debrief_completed_at IS
+ 'Zeitpunkt, zu dem die Trainer-Rückschau (Nachbereitung) bewusst abgeschlossen wurde; NULL = offen';
+
+CREATE INDEX IF NOT EXISTS idx_training_units_debrief_open
+ ON training_units (status, debrief_completed_at)
+ WHERE status = 'completed' AND debrief_completed_at IS NULL;
diff --git a/backend/routers/training_planning.py b/backend/routers/training_planning.py
index 1eddfea..c26c871 100644
--- a/backend/routers/training_planning.py
+++ b/backend/routers/training_planning.py
@@ -962,6 +962,10 @@ def list_training_units(
end_date: Optional[str] = Query(default=None),
status: Optional[str] = Query(default=None),
assigned_to_me: bool = Query(default=False),
+ debrief_pending: bool = Query(
+ default=False,
+ description="Nur abgeschlossene Einheiten ohne gesetzte Rückschau (debrief_completed_at IS NULL)",
+ ),
sort: str = Query(default="desc"),
limit: Optional[int] = Query(default=None),
tenant: TenantContext = Depends(get_tenant_context),
@@ -1081,7 +1085,11 @@ def list_training_units(
where.append("tu.planned_date <= %s")
params.append(end_date)
- if status:
+ if debrief_pending:
+ where.append("tu.status = %s")
+ params.append("completed")
+ where.append("tu.debrief_completed_at IS NULL")
+ elif status:
where.append("tu.status = %s")
params.append(status)
@@ -1384,6 +1392,13 @@ def update_training_unit(unit_id: int, data: dict, tenant: TenantContext = Depen
assist_sql = ", assistant_trainer_profile_ids = %s"
assist_params.append(na)
+ debrief_frag = ""
+ if "debrief_completed" in data and not is_blueprint:
+ if data.get("debrief_completed") is True:
+ debrief_frag = ", debrief_completed_at = NOW()"
+ else:
+ debrief_frag = ", debrief_completed_at = NULL"
+
cur.execute(
f"""
UPDATE training_units SET
@@ -1402,6 +1417,7 @@ def update_training_unit(unit_id: int, data: dict, tenant: TenantContext = Depen
updated_at = NOW()
{lead_sql}
{assist_sql}
+ {debrief_frag}
WHERE id = %s
""",
(
diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx
index 031231a..b393f43 100644
--- a/frontend/src/pages/Dashboard.jsx
+++ b/frontend/src/pages/Dashboard.jsx
@@ -42,7 +42,7 @@ function Dashboard() {
setTrainingHomeErr(null)
try {
const today = new Date().toISOString().slice(0, 10)
- const [upcomingRaw, recentRaw, plannedPool] = await Promise.all([
+ const [upcomingRaw, reviewPendingRaw, plannedPool] = await Promise.all([
api.listTrainingUnits({
assigned_to_me: true,
status: 'planned',
@@ -52,9 +52,9 @@ function Dashboard() {
}),
api.listTrainingUnits({
assigned_to_me: true,
- status: 'completed',
+ debrief_pending: true,
sort: 'desc',
- limit: 6,
+ limit: 8,
}),
api.listTrainingUnits({
assigned_to_me: true,
@@ -72,7 +72,7 @@ function Dashboard() {
if (!cancelled) {
setTrainingHome({
upcoming: Array.isArray(upcomingRaw) ? upcomingRaw : [],
- recent: Array.isArray(recentRaw) ? recentRaw : [],
+ reviewPending: Array.isArray(reviewPendingRaw) ? reviewPendingRaw : [],
plannedWithNotes: noteHits,
})
}
@@ -315,14 +315,14 @@ function Dashboard() {
-
Rückschau
+
Offene Rückschau
{trainingHomeErr ? (
{trainingHomeErr}
- ) : trainingHome?.recent?.length ? (
+ ) : trainingHome?.reviewPending?.length ? (
- {trainingHome.recent.map((u) => (
+ {trainingHome.reviewPending.map((u) => (
-
-
+
{(u.actual_date || u.planned_date || '').toString().slice(0, 10) || 'Datum'}
{u.group_name ? (
@@ -332,7 +332,10 @@ function Dashboard() {
))}
) : (
-
Noch keine Einträge in der Kurzliste.
+
+ Keine durchgeführten Trainings mit offener Nachbereitung. Zum Abschluss der Rückschau in der
+ Planung „Rückschau erledigt“ aktivieren.
+
)}
diff --git a/frontend/src/pages/TrainingPlanningPage.jsx b/frontend/src/pages/TrainingPlanningPage.jsx
index fec3178..793b781 100644
--- a/frontend/src/pages/TrainingPlanningPage.jsx
+++ b/frontend/src/pages/TrainingPlanningPage.jsx
@@ -1,5 +1,5 @@
-import React, { useState, useEffect, useCallback, useMemo } from 'react'
-import { Link } from 'react-router-dom'
+import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'
+import { Link, useSearchParams } from 'react-router-dom'
import api from '../utils/api'
import { useAuth } from '../context/AuthContext'
import ExercisePickerModal from '../components/ExercisePickerModal'
@@ -112,6 +112,8 @@ function filterDirectoryExcludingLead(directory, excludeLeadPid) {
}
function TrainingPlanningPage() {
const { user } = useAuth()
+ const [searchParams, setSearchParams] = useSearchParams()
+ const unitDeepLinkHandledRef = useRef(null)
const [groups, setGroups] = useState([])
const [selectedGroupId, setSelectedGroupId] = useState('')
const [units, setUnits] = useState([])
@@ -169,6 +171,7 @@ function TrainingPlanningPage() {
status: 'planned',
notes: '',
trainer_notes: '',
+ debrief_completed: false,
sections: [defaultSection()],
...sessionAssignDefaults()
})
@@ -482,6 +485,7 @@ function TrainingPlanningPage() {
status: 'planned',
notes: '',
trainer_notes: '',
+ debrief_completed: false,
sections: [defaultSection('Hauptteil')],
...sessionAssignDefaults()
})
@@ -510,6 +514,7 @@ function TrainingPlanningPage() {
status: 'planned',
notes: '',
trainer_notes: '',
+ debrief_completed: false,
sections: [defaultSection('Hauptteil')],
...sessionAssignDefaults()
})
@@ -537,7 +542,7 @@ function TrainingPlanningPage() {
}
}
- const handleEdit = async (unit) => {
+ const handleEdit = useCallback(async (unit) => {
try {
const fullUnit = await api.getTrainingUnit(unit.id)
setEditingUnit(fullUnit)
@@ -557,6 +562,7 @@ function TrainingPlanningPage() {
status: fullUnit.status || 'planned',
notes: fullUnit.notes || '',
trainer_notes: fullUnit.trainer_notes || '',
+ debrief_completed: Boolean(fullUnit.debrief_completed_at),
sections,
lead_trainer_profile_id:
fullUnit.lead_trainer_profile_id != null && fullUnit.lead_trainer_profile_id !== ''
@@ -579,8 +585,46 @@ function TrainingPlanningPage() {
setShowModal(true)
} catch (err) {
alert('Fehler beim Laden: ' + err.message)
+ throw err
}
- }
+ }, [])
+
+ useEffect(() => {
+ if (!user?.id || loading) return
+ const uid = searchParams.get('unit')
+ if (!uid) {
+ unitDeepLinkHandledRef.current = null
+ return
+ }
+ if (unitDeepLinkHandledRef.current === uid) return
+ const idNum = parseInt(uid, 10)
+ if (!Number.isFinite(idNum)) return
+ unitDeepLinkHandledRef.current = uid
+ handleEdit({ id: idNum })
+ .then(() => {
+ setSearchParams(
+ (prev) => {
+ const next = new URLSearchParams(prev)
+ next.delete('unit')
+ next.delete('debrief')
+ return next
+ },
+ { replace: true }
+ )
+ })
+ .catch(() => {
+ unitDeepLinkHandledRef.current = null
+ setSearchParams(
+ (prev) => {
+ const next = new URLSearchParams(prev)
+ next.delete('unit')
+ next.delete('debrief')
+ return next
+ },
+ { replace: true }
+ )
+ })
+ }, [user?.id, loading, searchParams, handleEdit, setSearchParams])
const handleSaveAsTemplate = async () => {
const name = window.prompt('Name für die neue Trainingsvorlage (nur Abschnitts-Gliederung):')
@@ -691,6 +735,10 @@ function TrainingPlanningPage() {
trainer_notes: formData.trainer_notes || null,
sections: sectionsPayload
}
+ if (editingUnit) {
+ payload.debrief_completed =
+ (formData.status || '') === 'completed' ? !!formData.debrief_completed : false
+ }
const leadStr = String(formData.lead_trainer_profile_id || '').trim()
if (leadStr) {
payload.lead_trainer_profile_id = parseInt(leadStr, 10)
@@ -725,7 +773,13 @@ function TrainingPlanningPage() {
const updateFormField = (field, value) => {
setFormData((prev) => {
- if (field !== 'lead_trainer_profile_id') return { ...prev, [field]: value }
+ if (field !== 'lead_trainer_profile_id') {
+ const patch = { ...prev, [field]: value }
+ if (field === 'status' && value !== 'completed') {
+ patch.debrief_completed = false
+ }
+ return patch
+ }
const ts = typeof value === 'string' ? value.trim() : String(value ?? '').trim()
const strip = new Set()
if (ts !== '') {
@@ -2298,6 +2352,34 @@ function TrainingPlanningPage() {
+
+ {formData.status === 'completed' ? (
+
+
+
+ ) : null}
>
)}
diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js
index 447a7d1..b44b5b7 100644
--- a/frontend/src/utils/api.js
+++ b/frontend/src/utils/api.js
@@ -996,6 +996,7 @@ export async function listTrainingUnits(filters = {}) {
if (filters.start_date) q.set('start_date', filters.start_date)
if (filters.end_date) q.set('end_date', filters.end_date)
if (filters.status) q.set('status', filters.status)
+ if (filters.debrief_pending === true) q.set('debrief_pending', 'true')
if (filters.assigned_to_me === true) q.set('assigned_to_me', 'true')
if (filters.sort) q.set('sort', String(filters.sort))
if (filters.limit != null && filters.limit !== '') q.set('limit', String(filters.limit))