2026-07-28 13:00:06 +02:00
|
|
|
|
"""A1 — Stage-bound practice exercises (Activity Set)."""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
from typing import Any, Optional
|
|
|
|
|
|
|
|
|
|
|
|
from services.recurring import (
|
|
|
|
|
|
list_recurring_for_initiative,
|
|
|
|
|
|
update_recurring_element,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Starter exercise sets per stage title (Spagat reference path).
|
|
|
|
|
|
STAGE_PRACTICE_SETS: dict[str, list[tuple[str, str]]] = {
|
|
|
|
|
|
"Stufe 1 — Basis": [
|
2026-08-17 18:06:41 +02:00
|
|
|
|
("Vorbeuge-Dehnung", "3×30 Sek. — Hüfte und Beinrückseite"),
|
|
|
|
|
|
("Hüftöffner (Schmetterling)", "Sitzend, leichter Druck"),
|
2026-07-28 13:00:06 +02:00
|
|
|
|
],
|
|
|
|
|
|
"Stufe 2 — Aufbau": [
|
2026-08-17 18:06:41 +02:00
|
|
|
|
("Seitlicher Spagat — links", "Halten, optional Messwert (Grad)"),
|
|
|
|
|
|
("Seitlicher Spagat — rechts", "Halten, optional Messwert (Grad)"),
|
|
|
|
|
|
("Aktive Vorbeuge", "3× kurz halten"),
|
2026-07-28 13:00:06 +02:00
|
|
|
|
],
|
|
|
|
|
|
"Stufe 3 — Ziel": [
|
2026-08-17 18:06:41 +02:00
|
|
|
|
("Voller Spagat", "Zielposition — Messwert dokumentieren"),
|
2026-07-28 13:00:06 +02:00
|
|
|
|
],
|
2026-07-28 16:28:08 +02:00
|
|
|
|
"Dehnung — Einstieg": [
|
2026-08-17 18:06:41 +02:00
|
|
|
|
("Vorbeuge halten", "3×30 Sek."),
|
2026-07-28 16:28:08 +02:00
|
|
|
|
],
|
|
|
|
|
|
"Hüftrotation — Einstieg": [
|
2026-08-17 18:06:41 +02:00
|
|
|
|
("Hüftkreisen im Stand", "10 Wiederholungen je Richtung"),
|
2026-07-28 16:28:08 +02:00
|
|
|
|
],
|
|
|
|
|
|
"Dehnung — nach Join": [
|
2026-08-17 18:06:41 +02:00
|
|
|
|
("Enddehnung Mawashi-Vorbereitung", "2×45 Sek."),
|
2026-07-28 16:28:08 +02:00
|
|
|
|
],
|
|
|
|
|
|
"Hüftrotation — nach Join": [
|
2026-08-17 18:06:41 +02:00
|
|
|
|
("Explosive Hüftrotation", "3×8 Wiederholungen"),
|
2026-07-28 16:28:08 +02:00
|
|
|
|
],
|
2026-07-28 13:00:06 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 16:28:08 +02:00
|
|
|
|
def get_active_maturity_stages(
|
2026-07-28 13:00:06 +02:00
|
|
|
|
*, tenant_id: str, initiative_id: str
|
2026-07-28 16:28:08 +02:00
|
|
|
|
) -> list[dict[str, Any]]:
|
2026-07-28 13:00:06 +02:00
|
|
|
|
from services.roadmap import list_roadmap_items_for_initiative
|
|
|
|
|
|
|
|
|
|
|
|
stages = [
|
|
|
|
|
|
item
|
|
|
|
|
|
for item in list_roadmap_items_for_initiative(
|
|
|
|
|
|
tenant_id=tenant_id, initiative_id=initiative_id
|
|
|
|
|
|
)
|
|
|
|
|
|
if item.get("item_type") == "maturity_stage" and item.get("status") == "active"
|
|
|
|
|
|
]
|
|
|
|
|
|
stages.sort(key=lambda s: (s.get("sort_order", 0), s.get("title", "")))
|
2026-07-28 16:28:08 +02:00
|
|
|
|
return stages
|
2026-07-28 13:00:06 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 16:28:08 +02:00
|
|
|
|
def get_active_maturity_stage(
|
|
|
|
|
|
*, tenant_id: str, initiative_id: str
|
|
|
|
|
|
) -> Optional[dict[str, Any]]:
|
|
|
|
|
|
stages = get_active_maturity_stages(
|
|
|
|
|
|
tenant_id=tenant_id, initiative_id=initiative_id
|
2026-07-28 13:00:06 +02:00
|
|
|
|
)
|
2026-07-28 16:28:08 +02:00
|
|
|
|
return stages[0] if stages else None
|
2026-07-28 13:00:06 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def list_practices_for_stage(
|
|
|
|
|
|
*,
|
|
|
|
|
|
tenant_id: str,
|
|
|
|
|
|
initiative_id: str,
|
|
|
|
|
|
stage_id: str,
|
|
|
|
|
|
) -> list[dict[str, Any]]:
|
|
|
|
|
|
items = list_recurring_for_initiative(
|
|
|
|
|
|
tenant_id=tenant_id, initiative_id=initiative_id
|
|
|
|
|
|
)
|
|
|
|
|
|
return [
|
|
|
|
|
|
item
|
|
|
|
|
|
for item in items
|
|
|
|
|
|
if item.get("roadmap_item_id") == stage_id
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pause_all_practices(
|
|
|
|
|
|
*,
|
|
|
|
|
|
tenant_id: str,
|
|
|
|
|
|
initiative_id: str,
|
|
|
|
|
|
user_id: Optional[str] = None,
|
|
|
|
|
|
) -> list[str]:
|
|
|
|
|
|
paused: list[str] = []
|
|
|
|
|
|
for item in list_recurring_for_initiative(
|
|
|
|
|
|
tenant_id=tenant_id, initiative_id=initiative_id
|
|
|
|
|
|
):
|
|
|
|
|
|
if item.get("status") == "active":
|
|
|
|
|
|
update_recurring_element(
|
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
|
recurring_id=item["id"],
|
|
|
|
|
|
status="paused",
|
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
paused.append(item["title"])
|
|
|
|
|
|
return paused
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-03 18:51:17 +02:00
|
|
|
|
def pause_practices_on_same_lane_except_stage(
|
2026-07-28 16:28:08 +02:00
|
|
|
|
*,
|
|
|
|
|
|
tenant_id: str,
|
|
|
|
|
|
initiative_id: str,
|
|
|
|
|
|
keep_stage_id: str,
|
|
|
|
|
|
user_id: Optional[str] = None,
|
|
|
|
|
|
) -> list[str]:
|
2026-08-03 18:51:17 +02:00
|
|
|
|
from services.progression_graph import progression_lanes_for_initiative
|
|
|
|
|
|
|
|
|
|
|
|
lanes = progression_lanes_for_initiative(
|
|
|
|
|
|
tenant_id=tenant_id, initiative_id=initiative_id
|
|
|
|
|
|
)
|
|
|
|
|
|
keep_lane = lanes.get(str(keep_stage_id))
|
|
|
|
|
|
if not keep_lane:
|
|
|
|
|
|
return pause_all_practices(
|
|
|
|
|
|
tenant_id=tenant_id, initiative_id=initiative_id, user_id=user_id
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
lane_gate_ids = {
|
|
|
|
|
|
gate_id for gate_id, lane in lanes.items() if lane == keep_lane
|
|
|
|
|
|
}
|
2026-07-28 16:28:08 +02:00
|
|
|
|
paused: list[str] = []
|
|
|
|
|
|
for item in list_recurring_for_initiative(
|
|
|
|
|
|
tenant_id=tenant_id, initiative_id=initiative_id
|
|
|
|
|
|
):
|
|
|
|
|
|
if item.get("status") != "active":
|
|
|
|
|
|
continue
|
|
|
|
|
|
if item.get("roadmap_item_id") == keep_stage_id:
|
|
|
|
|
|
continue
|
2026-08-03 18:51:17 +02:00
|
|
|
|
if str(item.get("roadmap_item_id") or "") not in lane_gate_ids:
|
2026-07-28 16:28:08 +02:00
|
|
|
|
continue
|
|
|
|
|
|
update_recurring_element(
|
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
|
recurring_id=item["id"],
|
|
|
|
|
|
status="paused",
|
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
paused.append(item["title"])
|
|
|
|
|
|
return paused
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-03 18:51:17 +02:00
|
|
|
|
def pause_practices_except_stage(
|
|
|
|
|
|
*,
|
|
|
|
|
|
tenant_id: str,
|
|
|
|
|
|
initiative_id: str,
|
|
|
|
|
|
keep_stage_id: str,
|
|
|
|
|
|
strand_project_id: Optional[str] = None,
|
|
|
|
|
|
user_id: Optional[str] = None,
|
|
|
|
|
|
) -> list[str]:
|
|
|
|
|
|
del strand_project_id
|
|
|
|
|
|
return pause_practices_on_same_lane_except_stage(
|
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
|
initiative_id=initiative_id,
|
|
|
|
|
|
keep_stage_id=keep_stage_id,
|
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-28 13:00:06 +02:00
|
|
|
|
def seed_practices_for_stage(
|
|
|
|
|
|
*,
|
|
|
|
|
|
tenant_id: str,
|
|
|
|
|
|
initiative_id: str,
|
|
|
|
|
|
stage: dict[str, Any],
|
|
|
|
|
|
user_id: Optional[str] = None,
|
|
|
|
|
|
) -> list[dict[str, Any]]:
|
|
|
|
|
|
"""Idempotent: create missing exercises for a stage; activate existing."""
|
|
|
|
|
|
stage_id = stage["id"]
|
|
|
|
|
|
stage_title = stage.get("title") or ""
|
|
|
|
|
|
defs = STAGE_PRACTICE_SETS.get(stage_title)
|
|
|
|
|
|
if not defs:
|
|
|
|
|
|
defs = [
|
|
|
|
|
|
(
|
|
|
|
|
|
f"Übung — {stage_title}",
|
2026-08-17 18:06:41 +02:00
|
|
|
|
f"Routine für {stage_title}",
|
2026-07-28 13:00:06 +02:00
|
|
|
|
)
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
existing = list_practices_for_stage(
|
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
|
initiative_id=initiative_id,
|
|
|
|
|
|
stage_id=stage_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
existing_by_title = {item["title"]: item for item in existing}
|
|
|
|
|
|
created: list[dict[str, Any]] = []
|
|
|
|
|
|
now = datetime.now(timezone.utc)
|
|
|
|
|
|
|
2026-08-17 18:06:41 +02:00
|
|
|
|
from services.scheduled_work import create_scheduled_action_at_gate
|
|
|
|
|
|
|
2026-07-28 13:00:06 +02:00
|
|
|
|
for title, description in defs:
|
|
|
|
|
|
if title in existing_by_title:
|
|
|
|
|
|
item = existing_by_title[title]
|
|
|
|
|
|
if item.get("status") != "active":
|
|
|
|
|
|
updated = update_recurring_element(
|
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
|
recurring_id=item["id"],
|
|
|
|
|
|
status="active",
|
|
|
|
|
|
next_due_at=now,
|
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if updated:
|
|
|
|
|
|
created.append(updated)
|
|
|
|
|
|
else:
|
|
|
|
|
|
created.append(item)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-08-17 18:06:41 +02:00
|
|
|
|
row = create_scheduled_action_at_gate(
|
2026-07-28 13:00:06 +02:00
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
|
initiative_id=initiative_id,
|
2026-08-17 18:06:41 +02:00
|
|
|
|
gate_id=stage_id,
|
2026-07-28 13:00:06 +02:00
|
|
|
|
title=title,
|
|
|
|
|
|
description=description,
|
|
|
|
|
|
user_id=user_id,
|
2026-08-17 18:06:41 +02:00
|
|
|
|
schedule_kind="weekdays",
|
2026-07-28 13:00:06 +02:00
|
|
|
|
)
|
|
|
|
|
|
created.append(row)
|
|
|
|
|
|
|
|
|
|
|
|
return created
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def activate_stage_practice_set(
|
|
|
|
|
|
*,
|
|
|
|
|
|
tenant_id: str,
|
|
|
|
|
|
initiative_id: str,
|
|
|
|
|
|
stage: dict[str, Any],
|
|
|
|
|
|
user_id: Optional[str] = None,
|
|
|
|
|
|
) -> dict[str, Any]:
|
2026-08-03 18:51:17 +02:00
|
|
|
|
paused = pause_practices_on_same_lane_except_stage(
|
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
|
initiative_id=initiative_id,
|
|
|
|
|
|
keep_stage_id=stage["id"],
|
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
|
)
|
2026-07-28 13:00:06 +02:00
|
|
|
|
exercises = seed_practices_for_stage(
|
|
|
|
|
|
tenant_id=tenant_id,
|
|
|
|
|
|
initiative_id=initiative_id,
|
|
|
|
|
|
stage=stage,
|
|
|
|
|
|
user_id=user_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
return {
|
|
|
|
|
|
"stage_id": stage["id"],
|
|
|
|
|
|
"stage_title": stage.get("title"),
|
|
|
|
|
|
"paused_practices": paused,
|
|
|
|
|
|
"active_practices": [e["title"] for e in exercises],
|
|
|
|
|
|
"practice_ids": [e["id"] for e in exercises],
|
|
|
|
|
|
}
|