feat(version): bump to 0.8.110 and enhance combination exercise features
Some checks failed
Some checks failed
- Updated app version to 0.8.110, reflecting recent improvements in combination exercise handling. - Introduced `load_combination_slots_for_exercise` function to streamline fetching combination slots for exercises. - Enhanced `TrainingPlanningPage` and `ExercisePeekModal` to utilize the new combination slots functionality, improving user experience. - Updated changelog to document the latest changes and feature enhancements. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5dc93d9a8c
commit
a8942a9e4e
|
|
@ -974,6 +974,43 @@ def assert_exercise_not_combination(cur, exercise_id: int) -> None:
|
|||
)
|
||||
|
||||
|
||||
def load_combination_slots_for_exercise(cur, exercise_id: int) -> List[dict]:
|
||||
"""Stationsliste einer Kombinationsübung (gleiches Format wie GET /api/exercises/:id)."""
|
||||
cur.execute(
|
||||
"""SELECT id, slot_index, title FROM combination_exercise_slots
|
||||
WHERE exercise_id = %s ORDER BY slot_index ASC, id ASC""",
|
||||
(exercise_id,),
|
||||
)
|
||||
slot_rows = [r2d(r) for r in cur.fetchall()]
|
||||
slots_out: List[dict] = []
|
||||
for sr in slot_rows:
|
||||
slot_pk = sr["id"]
|
||||
cur.execute(
|
||||
"""SELECT candidate_exercise_id FROM combination_slot_candidates
|
||||
WHERE slot_id = %s ORDER BY sort_order ASC, id ASC""",
|
||||
(slot_pk,),
|
||||
)
|
||||
crows = cur.fetchall()
|
||||
cids = [int(r2d(c)["candidate_exercise_id"]) for c in crows]
|
||||
cand_meta: Dict[int, Optional[str]] = {}
|
||||
if cids:
|
||||
ph = ",".join(["%s"] * len(cids))
|
||||
cur.execute(
|
||||
f"SELECT id, title FROM exercises WHERE id IN ({ph})",
|
||||
tuple(cids),
|
||||
)
|
||||
cand_meta = {int(r2d(x)["id"]): r2d(x).get("title") for x in cur.fetchall()}
|
||||
slots_out.append(
|
||||
{
|
||||
"slot_index": sr["slot_index"],
|
||||
"title": sr.get("title"),
|
||||
"candidate_exercise_ids": cids,
|
||||
"candidates": [{"exercise_id": cid, "title": cand_meta.get(cid)} for cid in cids],
|
||||
}
|
||||
)
|
||||
return slots_out
|
||||
|
||||
|
||||
def enrich_exercise_detail(exercise_id: int, cur) -> dict:
|
||||
"""
|
||||
Lädt alle M:N Relations für eine Übung und gibt ein vollständiges
|
||||
|
|
@ -1102,41 +1139,7 @@ def enrich_exercise_detail(exercise_id: int, cur) -> dict:
|
|||
|
||||
exercise["combination_slots"] = []
|
||||
if exercise["exercise_kind"] == "combination":
|
||||
cur.execute(
|
||||
"""SELECT id, slot_index, title FROM combination_exercise_slots
|
||||
WHERE exercise_id = %s ORDER BY slot_index ASC, id ASC""",
|
||||
(exercise_id,),
|
||||
)
|
||||
slot_rows = [r2d(r) for r in cur.fetchall()]
|
||||
slots_out: List[dict] = []
|
||||
for sr in slot_rows:
|
||||
slot_pk = sr["id"]
|
||||
cur.execute(
|
||||
"""SELECT candidate_exercise_id FROM combination_slot_candidates
|
||||
WHERE slot_id = %s ORDER BY sort_order ASC, id ASC""",
|
||||
(slot_pk,),
|
||||
)
|
||||
crows = cur.fetchall()
|
||||
cids = [int(r2d(c)["candidate_exercise_id"]) for c in crows]
|
||||
cand_meta: Dict[int, Optional[str]] = {}
|
||||
if cids:
|
||||
ph = ",".join(["%s"] * len(cids))
|
||||
cur.execute(
|
||||
f"SELECT id, title FROM exercises WHERE id IN ({ph})",
|
||||
tuple(cids),
|
||||
)
|
||||
cand_meta = {int(r2d(x)["id"]): r2d(x).get("title") for x in cur.fetchall()}
|
||||
slots_out.append(
|
||||
{
|
||||
"slot_index": sr["slot_index"],
|
||||
"title": sr.get("title"),
|
||||
"candidate_exercise_ids": cids,
|
||||
"candidates": [
|
||||
{"exercise_id": cid, "title": cand_meta.get(cid)} for cid in cids
|
||||
],
|
||||
}
|
||||
)
|
||||
exercise["combination_slots"] = slots_out
|
||||
exercise["combination_slots"] = load_combination_slots_for_exercise(cur, exercise_id)
|
||||
|
||||
return exercise
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ from club_tenancy import (
|
|||
)
|
||||
from routers.training_modules import load_training_module_for_apply
|
||||
|
||||
from routers.exercises import load_combination_slots_for_exercise
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["training_planning"])
|
||||
|
||||
|
||||
|
|
@ -493,6 +495,14 @@ def _fetch_sections(cur, unit_id: int) -> List[Dict[str, Any]]:
|
|||
it["catalog_method_profile"] = {}
|
||||
else:
|
||||
it["catalog_method_profile"] = dict(cmp_raw)
|
||||
ek = str(it.get("exercise_kind") or "simple").strip().lower()
|
||||
if ek == "combination" and it.get("exercise_id"):
|
||||
try:
|
||||
it["combination_slots"] = load_combination_slots_for_exercise(cur, int(it["exercise_id"]))
|
||||
except (TypeError, ValueError):
|
||||
it["combination_slots"] = []
|
||||
else:
|
||||
it["combination_slots"] = []
|
||||
secs.append(sec)
|
||||
return secs
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.109"
|
||||
APP_VERSION = "0.8.110"
|
||||
BUILD_DATE = "2026-05-12"
|
||||
DB_SCHEMA_VERSION = "20260512057"
|
||||
|
||||
|
|
@ -21,10 +21,10 @@ MODULE_VERSIONS = {
|
|||
"groups": "0.1.0",
|
||||
"skills": "0.1.0",
|
||||
"methods": "0.1.0",
|
||||
"exercises": "2.27.2", # Kombi: Serien‑Standard 1 + Archetyp‑Map ARCHETYPE_DEFAULT_REP_SERIES_COUNT; Payload rep_series_count ab 1
|
||||
"exercises": "2.27.3", # load_combination_slots_for_exercise (gemeinsam mit GET Übung); Hydrate für Planung
|
||||
"training_units": "0.2.0",
|
||||
"training_programs": "0.1.0",
|
||||
"planning": "0.9.2", # Kombi: planning_method_profile auf Sektions-Items (Migration 057); Form-Payload + Coach-PUT
|
||||
"planning": "0.9.3", # GET training-units/:id Sektions-Items: combination_slots + Kandidaten-Titel für Druck/Run
|
||||
"training_modules": "1.0.0",
|
||||
"import_wiki": "1.0.0",
|
||||
"admin": "1.0.0",
|
||||
|
|
@ -35,6 +35,14 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.110",
|
||||
"date": "2026-05-12",
|
||||
"changes": [
|
||||
"GET /api/training-units/:id: Bei Kombinationsübungen werden `combination_slots` inkl. Kandidaten-Titel mitgeliefert (für Plan & Ablauf / Druck).",
|
||||
"Hilfsfunktion `load_combination_slots_for_exercise` im exercises-Router; GET Übung nutzt dieselbe Ladelogik.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.109",
|
||||
"date": "2026-05-12",
|
||||
|
|
|
|||
|
|
@ -6210,6 +6210,178 @@ a.analysis-split__nav-item {
|
|||
line-height: 1.48;
|
||||
}
|
||||
|
||||
/* Kombinationsplan — Klammer (Vorschau, Plan & Ablauf, Druck) */
|
||||
.combo-plan-bracket {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
align-items: stretch;
|
||||
margin: 0.35rem 0 0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
.combo-plan-bracket__accent {
|
||||
width: 6px;
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(180deg, var(--accent) 0%, var(--accent-dark) 100%);
|
||||
}
|
||||
.combo-plan-bracket__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 10px 12px 12px;
|
||||
}
|
||||
.combo-plan-bracket__head {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.combo-plan-bracket__head-main {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px 12px;
|
||||
}
|
||||
.combo-plan-bracket__kicker {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text3);
|
||||
}
|
||||
.combo-plan-bracket__archetype {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: var(--text1);
|
||||
}
|
||||
.combo-plan-bracket__archetype-id {
|
||||
font-weight: 500;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text3);
|
||||
}
|
||||
.combo-plan-bracket__badge {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent-soft, hsla(160, 42%, 90%, 1));
|
||||
color: var(--accent-dark);
|
||||
}
|
||||
.combo-plan-bracket__hint {
|
||||
margin: 6px 0 0;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.42;
|
||||
color: var(--text2);
|
||||
}
|
||||
.combo-plan-bracket__globals {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.combo-plan-bracket__globals-title {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text3);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.combo-plan-bracket__globals-empty {
|
||||
margin: 0 0 12px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text3);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.combo-plan-bracket__chip-row {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.combo-plan-bracket__chip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 6.5rem;
|
||||
max-width: 14rem;
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface2);
|
||||
}
|
||||
.combo-plan-bracket__chip-cap {
|
||||
font-size: 0.68rem;
|
||||
color: var(--text3);
|
||||
line-height: 1.25;
|
||||
}
|
||||
.combo-plan-bracket__chip-val {
|
||||
font-size: 0.88rem;
|
||||
font-weight: 700;
|
||||
color: var(--text1);
|
||||
}
|
||||
.combo-plan-bracket__stations {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.combo-plan-bracket__station {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
padding: 10px 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface2);
|
||||
}
|
||||
.combo-plan-bracket__station-index {
|
||||
flex-shrink: 0;
|
||||
min-width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--accent-dark);
|
||||
}
|
||||
.combo-plan-bracket__station-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.combo-plan-bracket__station-title {
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text1);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.combo-plan-bracket__station-exercises {
|
||||
font-size: 0.84rem;
|
||||
color: var(--text2);
|
||||
line-height: 1.38;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.combo-plan-bracket__station-timing {
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.42;
|
||||
color: var(--text1);
|
||||
}
|
||||
.combo-plan-bracket__station-timing--muted {
|
||||
color: var(--text3);
|
||||
font-style: italic;
|
||||
}
|
||||
.combo-plan-bracket__timing-label {
|
||||
font-weight: 700;
|
||||
color: var(--text3);
|
||||
margin-right: 6px;
|
||||
}
|
||||
.training-run-combo-embed {
|
||||
margin-top: 0.65rem;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.desktop-sidebar,
|
||||
.bottom-nav,
|
||||
|
|
@ -6236,6 +6408,24 @@ a.analysis-split__nav-item {
|
|||
break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.combo-plan-bracket {
|
||||
border-color: #222 !important;
|
||||
background: #fff !important;
|
||||
break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.combo-plan-bracket__accent {
|
||||
background: #085041 !important;
|
||||
}
|
||||
.combo-plan-bracket__chip,
|
||||
.combo-plan-bracket__station {
|
||||
border-color: #444 !important;
|
||||
background: #f4f6f8 !important;
|
||||
}
|
||||
.combo-plan-bracket__station-index {
|
||||
border-color: #444 !important;
|
||||
color: #06352a !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Coach — volle Übung, Nur-Mittelbereich scrollt; Steuerung oben/unten sichtbar */
|
||||
|
|
|
|||
128
frontend/src/components/CombinationPlanBracket.jsx
Normal file
128
frontend/src/components/CombinationPlanBracket.jsx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* Kombination: konsolidierte Darstellung globales Profil + Stationen mit Zeiten (Vorschau, Plan-Ansicht, Druck).
|
||||
*/
|
||||
import React, { useMemo } from 'react'
|
||||
import {
|
||||
archetypeCoachHint,
|
||||
combinationArchetypeLabel,
|
||||
sortCombinationSlotsForDisplay,
|
||||
} from '../constants/combinationArchetypes'
|
||||
import { describeGlobalComboProfile, readSlotProfilesV1, summarizeSlotProfileBrief } from '../utils/combinationMethodProfileUi'
|
||||
|
||||
function candidateLine(slot) {
|
||||
const cands = slot.candidates
|
||||
if (Array.isArray(cands) && cands.length > 0) {
|
||||
return cands
|
||||
.map((c) =>
|
||||
((c.title || '').trim() || (c.exercise_id != null ? `Übung #${c.exercise_id}` : '')).trim(),
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join(' ↔ ')
|
||||
}
|
||||
const ids = slot.candidate_exercise_ids || []
|
||||
return ids
|
||||
.map((raw) => {
|
||||
const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10)
|
||||
return Number.isFinite(n) ? `Übung #${n}` : ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(' ↔ ')
|
||||
}
|
||||
|
||||
export default function CombinationPlanBracket({
|
||||
methodArchetype,
|
||||
methodProfile,
|
||||
combinationSlots,
|
||||
planningAdjusted = false,
|
||||
}) {
|
||||
const arch = typeof methodArchetype === 'string' ? methodArchetype.trim() : ''
|
||||
const archLabel = arch ? combinationArchetypeLabel(arch) : null
|
||||
const globals = describeGlobalComboProfile(arch, methodProfile || {})
|
||||
const slotsSorted = useMemo(() => sortCombinationSlotsForDisplay(combinationSlots || []), [combinationSlots])
|
||||
const timingByIx = useMemo(() => {
|
||||
const mp = methodProfile || {}
|
||||
const rows = readSlotProfilesV1(mp)
|
||||
const m = new Map()
|
||||
for (const r of rows) {
|
||||
m.set(Number(r.slot_index), r)
|
||||
}
|
||||
return m
|
||||
}, [methodProfile])
|
||||
|
||||
const coachHint = arch ? archetypeCoachHint(arch) : ''
|
||||
|
||||
return (
|
||||
<div className="combo-plan-bracket">
|
||||
<div className="combo-plan-bracket__accent" aria-hidden />
|
||||
<div className="combo-plan-bracket__body">
|
||||
<header className="combo-plan-bracket__head">
|
||||
<div className="combo-plan-bracket__head-main">
|
||||
<span className="combo-plan-bracket__kicker">Kombinations‑Plan</span>
|
||||
<span className="combo-plan-bracket__archetype">
|
||||
{archLabel || arch || 'Archetyp'}
|
||||
{arch && archLabel && archLabel !== arch ? (
|
||||
<span className="combo-plan-bracket__archetype-id"> ({arch})</span>
|
||||
) : null}
|
||||
</span>
|
||||
{planningAdjusted ? (
|
||||
<span className="combo-plan-bracket__badge">Planung angepasst</span>
|
||||
) : null}
|
||||
</div>
|
||||
{coachHint ? <p className="combo-plan-bracket__hint">{coachHint}</p> : null}
|
||||
</header>
|
||||
|
||||
{globals.length > 0 ? (
|
||||
<section className="combo-plan-bracket__globals" aria-label="Globale Eckdaten">
|
||||
<div className="combo-plan-bracket__globals-title">Runden · Zeiten · Pausen (global)</div>
|
||||
<ul className="combo-plan-bracket__chip-row">
|
||||
{globals.map((g) => (
|
||||
<li key={g.key} className="combo-plan-bracket__chip" title={g.detailLabel}>
|
||||
<span className="combo-plan-bracket__chip-cap">{g.caption}</span>
|
||||
<span className="combo-plan-bracket__chip-val">{g.value}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : (
|
||||
<p className="combo-plan-bracket__globals-empty">
|
||||
Keine globalen Zahlenfelder gesetzt — Steuerung erfolgt nur je Station oder über den Freitext der Kombination.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<ol className="combo-plan-bracket__stations">
|
||||
{slotsSorted.map((slot, si) => {
|
||||
const siRaw = slot.slot_index
|
||||
const ixParsed =
|
||||
siRaw === '' || siRaw == null ? si : typeof siRaw === 'number' ? siRaw : parseInt(String(siRaw), 10)
|
||||
const stationIx = Number.isFinite(ixParsed) ? ixParsed : si
|
||||
const stationTitle = ((slot.title || '').trim() || `Station ${stationIx}`).trim()
|
||||
const names = candidateLine(slot)
|
||||
const timing = summarizeSlotProfileBrief(timingByIx.get(stationIx))
|
||||
|
||||
return (
|
||||
<li key={`slot-${stationIx}-${si}`} className="combo-plan-bracket__station">
|
||||
<div className="combo-plan-bracket__station-index" title={`slot_index ${stationIx}`}>
|
||||
S{stationIx}
|
||||
</div>
|
||||
<div className="combo-plan-bracket__station-main">
|
||||
<div className="combo-plan-bracket__station-title">{stationTitle}</div>
|
||||
<div className="combo-plan-bracket__station-exercises">{names || '(keine Einzelübung)'}</div>
|
||||
{timing ? (
|
||||
<div className="combo-plan-bracket__station-timing">
|
||||
<span className="combo-plan-bracket__timing-label">Zeit / Steuerung</span>
|
||||
<span>{timing}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="combo-plan-bracket__station-timing combo-plan-bracket__station-timing--muted">
|
||||
Keine eigene Stations‑Zeit im Profil — ggf. nur globale Vorgaben oder Freitext.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -5,8 +5,7 @@ import React, { useEffect, useMemo, useState } from 'react'
|
|||
import { Link } from 'react-router-dom'
|
||||
import api from '../utils/api'
|
||||
import ExerciseRichTextBlock from './ExerciseRichTextBlock'
|
||||
import CombinationCoachSlots from './CombinationCoachSlots'
|
||||
import { combinationArchetypeLabel } from '../constants/combinationArchetypes'
|
||||
import CombinationPlanBracket from './CombinationPlanBracket'
|
||||
import { effectiveComboMethodProfile } from '../utils/comboPlanningMethodProfile'
|
||||
|
||||
function TagMini({ exercise }) {
|
||||
|
|
@ -100,7 +99,7 @@ export default function ExercisePeekModal({
|
|||
aria-modal="true"
|
||||
aria-labelledby="exercise-peek-title"
|
||||
style={{
|
||||
maxWidth: sheetWide ? 'min(760px, 96vw)' : '620px',
|
||||
maxWidth: sheetWide ? 'min(840px, 96vw)' : '620px',
|
||||
width: '100%',
|
||||
maxHeight: '88vh',
|
||||
display: 'flex',
|
||||
|
|
@ -128,42 +127,17 @@ export default function ExercisePeekModal({
|
|||
<>
|
||||
{isCombination ? (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '12px',
|
||||
padding: '8px 10px',
|
||||
borderRadius: '8px',
|
||||
background: 'var(--surface2)',
|
||||
border: '1px solid var(--border)',
|
||||
fontSize: '0.82rem',
|
||||
color: 'var(--text2)',
|
||||
lineHeight: 1.45,
|
||||
}}
|
||||
>
|
||||
<strong style={{ color: 'var(--text1)' }}>Kombination</strong>
|
||||
<span style={{ marginLeft: 8 }}>
|
||||
{(() => {
|
||||
const ak = String(exercise.method_archetype || '').trim()
|
||||
const lbl = ak ? combinationArchetypeLabel(ak) : null
|
||||
return lbl || ak || 'Archetyp nicht gesetzt'
|
||||
})()}
|
||||
</span>
|
||||
{peekExtras?.planning_method_profile != null &&
|
||||
typeof peekExtras.planning_method_profile === 'object' &&
|
||||
!Array.isArray(peekExtras.planning_method_profile) ? (
|
||||
<span style={{ marginLeft: 8, color: 'var(--accent-dark)', fontWeight: 600 }}>
|
||||
· Planung angepasst
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<CombinationCoachSlots
|
||||
<CombinationPlanBracket
|
||||
methodArchetype={exercise.method_archetype || ''}
|
||||
methodProfile={comboMethodProfileEffective}
|
||||
combinationSlots={
|
||||
Array.isArray(exercise.combination_slots) ? exercise.combination_slots : []
|
||||
}
|
||||
methodArchetype={exercise.method_archetype || ''}
|
||||
methodProfile={comboMethodProfileEffective}
|
||||
compactPlanningView
|
||||
omitGlobalKeyValueBlock={false}
|
||||
planningAdjusted={
|
||||
peekExtras?.planning_method_profile != null &&
|
||||
typeof peekExtras.planning_method_profile === 'object' &&
|
||||
!Array.isArray(peekExtras.planning_method_profile)
|
||||
}
|
||||
/>
|
||||
<hr style={{ border: 'none', borderTop: '1px solid var(--border)', margin: '1rem 0' }} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { GripVertical, Pencil } from 'lucide-react'
|
||||
import CombinationMethodProfileEditor from './CombinationMethodProfileEditor'
|
||||
import CombinationCoachSlots from './CombinationCoachSlots'
|
||||
import CombinationPlanBracket from './CombinationPlanBracket'
|
||||
import { comboPlanningProfileJsonForEditor, effectiveComboMethodProfile } from '../utils/comboPlanningMethodProfile'
|
||||
import { combinationArchetypeLabel, sortCombinationSlotsForDisplay } from '../constants/combinationArchetypes'
|
||||
import {
|
||||
|
|
@ -1611,12 +1611,15 @@ export default function TrainingUnitSectionsEditor({
|
|||
</p>
|
||||
{comboPlanningResolvedSlots.length > 0 ? (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<CombinationCoachSlots
|
||||
combinationSlots={comboPlanningResolvedSlots}
|
||||
<CombinationPlanBracket
|
||||
methodArchetype={(comboPlanningModalItem.catalog_method_archetype || '').trim()}
|
||||
methodProfile={comboPlanningEffectiveProfile}
|
||||
compactPlanningView
|
||||
omitGlobalKeyValueBlock
|
||||
combinationSlots={comboPlanningResolvedSlots}
|
||||
planningAdjusted={
|
||||
comboPlanningModalItem.planning_method_profile != null &&
|
||||
typeof comboPlanningModalItem.planning_method_profile === 'object' &&
|
||||
!Array.isArray(comboPlanningModalItem.planning_method_profile)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
|||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import api from '../utils/api'
|
||||
import ExercisePeekModal from '../components/ExercisePeekModal'
|
||||
import CombinationPlanBracket from '../components/CombinationPlanBracket'
|
||||
import { itemStableKey, sortedSections, sortedItems } from '../utils/trainingPlanUtils'
|
||||
import { effectiveComboMethodProfile } from '../utils/comboPlanningMethodProfile'
|
||||
|
||||
function storageKey(unitId) {
|
||||
return `sj_training_run_checked_${unitId}`
|
||||
|
|
@ -146,6 +148,7 @@ export default function TrainingUnitRunPage() {
|
|||
open={peekCtx != null}
|
||||
exerciseId={peekCtx?.exerciseId}
|
||||
variantId={peekCtx?.variantId ?? undefined}
|
||||
peekExtras={peekCtx?.peekExtras ?? undefined}
|
||||
onClose={() => setPeekCtx(null)}
|
||||
/>
|
||||
|
||||
|
|
@ -271,7 +274,15 @@ export default function TrainingUnitRunPage() {
|
|||
const plan = formatMin(it.planned_duration_min)
|
||||
const extras = []
|
||||
if (it.exercise_focus_area) extras.push(it.exercise_focus_area)
|
||||
const metaParts = [...extras, plan].filter(Boolean)
|
||||
const exKind = String(it.exercise_kind || 'simple').toLowerCase().trim()
|
||||
const isComboRow = exKind === 'combination'
|
||||
const metaParts = [...extras, isComboRow ? 'Kombination' : null, plan].filter(Boolean)
|
||||
const comboEffectiveProfile = isComboRow
|
||||
? effectiveComboMethodProfile(
|
||||
it.catalog_method_profile || {},
|
||||
it.planning_method_profile ?? null,
|
||||
)
|
||||
: null
|
||||
|
||||
return (
|
||||
<li key={ck} className={`training-run-item training-run-item--exercise${done ? ' training-run-item--done' : ''}`}>
|
||||
|
|
@ -309,6 +320,22 @@ export default function TrainingUnitRunPage() {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{isComboRow && it.exercise_id ? (
|
||||
<div className="training-run-combo-embed">
|
||||
<CombinationPlanBracket
|
||||
methodArchetype={String(it.catalog_method_archetype || '').trim()}
|
||||
methodProfile={comboEffectiveProfile || {}}
|
||||
combinationSlots={
|
||||
Array.isArray(it.combination_slots) ? it.combination_slots : []
|
||||
}
|
||||
planningAdjusted={
|
||||
it.planning_method_profile != null &&
|
||||
typeof it.planning_method_profile === 'object' &&
|
||||
!Array.isArray(it.planning_method_profile)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{it.exercise_id && (
|
||||
<div className="no-print" style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', marginTop: '0.55rem', alignItems: 'center' }}>
|
||||
<button
|
||||
|
|
@ -318,7 +345,16 @@ export default function TrainingUnitRunPage() {
|
|||
onClick={() =>
|
||||
setPeekCtx({
|
||||
exerciseId: it.exercise_id,
|
||||
variantId: it.exercise_variant_id != null ? Number(it.exercise_variant_id) : null,
|
||||
variantId:
|
||||
it.exercise_variant_id != null
|
||||
? Number(it.exercise_variant_id)
|
||||
: null,
|
||||
peekExtras: isComboRow
|
||||
? {
|
||||
catalog_method_profile: it.catalog_method_profile,
|
||||
planning_method_profile: it.planning_method_profile,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -170,6 +170,45 @@ export const METHOD_PROFILE_GUI_FIELDS = Object.freeze({
|
|||
free_method_block: [],
|
||||
})
|
||||
|
||||
function shortenComboGuiCaption(label) {
|
||||
const t = (label || '').trim()
|
||||
if (!t) return ''
|
||||
const cut = t.split('(')[0].trim()
|
||||
return cut.length > 52 ? `${cut.slice(0, 50)}…` : cut
|
||||
}
|
||||
|
||||
/**
|
||||
* Globale Archetyp-Felder aus method_profile für Lesetext (Vorschau, Druck).
|
||||
* Ignoriert slot_profiles_v1 (kommt separat je Station).
|
||||
*/
|
||||
export function describeGlobalComboProfile(archetypeKey, profileObj) {
|
||||
const arch = typeof archetypeKey === 'string' ? archetypeKey.trim() : ''
|
||||
if (!profileObj || typeof profileObj !== 'object' || Array.isArray(profileObj)) return []
|
||||
const defs = METHOD_PROFILE_GUI_FIELDS[arch] || []
|
||||
const rows = []
|
||||
for (const def of defs) {
|
||||
const val = profileObj[def.key]
|
||||
if (val === undefined || val === null || val === '') continue
|
||||
if (def.kind === 'bool') {
|
||||
const on = val === true || val === 'true' || val === 1 || val === '1'
|
||||
rows.push({
|
||||
key: def.key,
|
||||
caption: shortenComboGuiCaption(def.label),
|
||||
detailLabel: def.label,
|
||||
value: on ? 'Ja' : 'Nein',
|
||||
})
|
||||
} else {
|
||||
rows.push({
|
||||
key: def.key,
|
||||
caption: shortenComboGuiCaption(def.label),
|
||||
detailLabel: def.label,
|
||||
value: String(val),
|
||||
})
|
||||
}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert method_profile unter Beibehaltung nicht-GUI Schlüssel.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Shinkan Jinkendo Frontend Version
|
||||
|
||||
export const APP_VERSION = "0.8.94"
|
||||
export const BUILD_DATE = "2026-05-11"
|
||||
export const APP_VERSION = "0.8.110"
|
||||
export const BUILD_DATE = "2026-05-12"
|
||||
|
||||
export const PAGE_VERSIONS = {
|
||||
LoginPage: "1.0.2",
|
||||
|
|
@ -16,7 +16,7 @@ export const PAGE_VERSIONS = {
|
|||
TrainingPlanningPage: "1.4.0",
|
||||
TrainingFrameworkProgramsListPage: "1.1.0",
|
||||
TrainingFrameworkProgramEditPage: "1.5.0",
|
||||
TrainingUnitRunPage: "1.1.0",
|
||||
TrainingUnitRunPage: "1.2.0",
|
||||
TrainingCoachPage: "1.0.0",
|
||||
AdminCatalogsPage: "2.2.0",
|
||||
TrainerContextsPage: "1.0.0",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user