feat(version): bump to 0.8.109 and enhance combination exercise features
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / playwright-tests (push) Successful in 56s
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / playwright-tests (push) Successful in 56s
- Updated app version to 0.8.109, reflecting recent improvements in combination exercise handling. - Introduced `rep_series_count` for slot profiles, allowing for multiple series in `rep` and `manual` modes, enhancing flexibility in exercise configurations. - Updated the CombinationMethodProfileEditor and CombinationCoachSlots components to support and display the new series count feature. - Enhanced ExerciseFormPage to manage series count and intra-series pauses effectively, improving user experience. - Documented changes in the changelog for better tracking of feature enhancements. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
38d84ecdf6
commit
cf9932990e
|
|
@ -423,7 +423,7 @@ Alle diese Angaben sind **Anweisungen an den Trainer** und **Coach‑Assistenz**
|
|||
|
||||
**Geplantes kanonisches Zeitmodell:** Globale Eckwerte (z. B. Anzahl der Durchläufe / Runden, optionale Gesamt-/Einführungszeit als Ziel oder Rechenhilfe) und **pro Platz (Slot)** die Dimensionen „Belastung“, „wie viele gleiche Übung hintereinander“, „kurze Pause dazwischen“, „Übergangszeit zur nächsten Übung/arbeitstation“ — dokumentiert für die technische Angleichung in **`.claude/docs/working/COMBINATION_TIMING_PROFILE_PLAN.md`** (Felder **`slot_profiles_v1`**, `timing_schema`). Archetypen können **Strukturen und typische Schnellwahlen** vorgeben (z. B. Zirkel: Relation Belastungszeit = Übergangszeit oder Erholungsanteil ≈ 2/3 der Belastung); der Archetyp **Freier Methodenblock** bildet den **Maximal‑Pfad** ohne stärkere stille Annahmen. **Pyramidale/abhängige Pausen** (Pause abhängig von vorheriger Belastung) sind **nicht Teil des aktuellen Umsetzungspfads**, können später als eigener Untertyp ergänzt werden.
|
||||
|
||||
**Fortschritt pro Slot (Stand 0.8.106):** optional **`advance_mode`** je Eintrag in **`slot_profiles_v1`**: `timed` — Standard (`load_sec` = geplante Arbeitsdauer für Timer im Coach; fehlende Angabe entspricht `timed` ohne Sekundenfeld), **`rep`** — mengenorientiert (Zielzahl über **`consecutive_reps`**; keine verbindliche Arbeitsuhr), **`manual`** — coachgeführt (Fortschritt bewusst per Schritt später im Coach, optional Richtwert über **`consecutive_reps`**). **`intra_rep_rest_sec`** und **`transition_after_sec`** bleiben unabhängig vom Modus nutzbar. **`load_sec`** wird nur im Modus `timed` persistiert.
|
||||
**Fortschritt pro Slot (Stand 0.8.109):** optional **`advance_mode`** je Eintrag in **`slot_profiles_v1`**: `timed` — Standard (`load_sec` = geplante Arbeitsdauer für Timer im Coach; fehlende Angabe entspricht `timed` ohne Sekundenfeld), **`rep`** — mengenorientiert (Zielzahl über **`consecutive_reps`**; keine verbindliche Arbeitsuhr), **`manual`** — coachgeführt (Fortschritt bewusst per Schritt später im Coach, optional Richtwert über **`consecutive_reps`**). Optional **`rep_series_count`**: Standard **1** (wird im Formular/API explizit geführt); Ausnahmen nur, wenn der **Methoden‑Archetyp** in `ARCHETYPE_DEFAULT_REP_SERIES_COUNT` eine andere Vorgabe definiert oder der Nutzer eine andere Zahl setzt. ≥ 2 ermöglicht Pause **zwischen Serien** (`intra_rep_rest_sec`). Bei nur **einer** Serie: kein **`intra_rep_rest_sec`** in UI und Payload; **`transition_after_sec`** = Wechsel zur nächsten Station.
|
||||
|
||||
### 6.4 Slot- und Pool-Logik
|
||||
|
||||
|
|
|
|||
|
|
@ -53,8 +53,9 @@ Objekt‑Shape (Sekunden, ganze Zahlen ≥ 0):
|
|||
| Feld | Bedeutung |
|
||||
|------|------------|
|
||||
| `load_sec` | Belastungsdauer „an der Station“. |
|
||||
| `consecutive_reps` | Übungen hintereinander ohne Wechsel zu **neuem** Stationsinhalt („oft 1“). |
|
||||
| `intra_rep_rest_sec` | Pause zwischen diesen Folge‑Wiederholungen. |
|
||||
| `consecutive_reps` | Wiederholungen pro „Serie“ bzw. ohne Wechsel zu **neuem** Stationsinhalt (oft 1). |
|
||||
| `rep_series_count` | Anzahl Serien à `consecutive_reps` bei rep/manual; Standard **1**, Archetyp‑Vorgabe möglich (**`ARCHETYPE_DEFAULT_REP_SERIES_COUNT`**). Persistiert für rep/manual ab 1. |
|
||||
| `intra_rep_rest_sec` | Pause zwischen den Folge‑Wiederholungen bzw. **zwischen Serien** (nur sinnvoll, wenn `rep_series_count` ≥ 2 im Modus `rep`/`manual`; sonst Wechselzeit `transition_after_sec` nutzen). |
|
||||
| `transition_after_sec` | Pause / Wechsel **zur nächsten** Station oder zum nächsten logischen Block. |
|
||||
|
||||
**Hinweis:** Bestehende Archetyp‑„flachen“ Schlüssel (`work_seconds`, `transition_seconds`, …) werden schrittweise **nicht zerstört**, sondern Slots ergänzen; Konvergenz (eine Darstellung zu v1) kann Phase 4 sein.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.106"
|
||||
APP_VERSION = "0.8.109"
|
||||
BUILD_DATE = "2026-05-12"
|
||||
DB_SCHEMA_VERSION = "20260512057"
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ MODULE_VERSIONS = {
|
|||
"groups": "0.1.0",
|
||||
"skills": "0.1.0",
|
||||
"methods": "0.1.0",
|
||||
"exercises": "2.26.0", # Kombi: advance_mode je Station (slot_profiles_v1: timed|rep|manual); Payload/Coach-Lesetext
|
||||
"exercises": "2.27.2", # Kombi: Serien‑Standard 1 + Archetyp‑Map ARCHETYPE_DEFAULT_REP_SERIES_COUNT; Payload rep_series_count ab 1
|
||||
"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
|
||||
|
|
@ -35,6 +35,27 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.109",
|
||||
"date": "2026-05-12",
|
||||
"changes": [
|
||||
"Kombination: „Serien“ standardmäßig 1 (Formular/API); Archetyp kann via `ARCHETYPE_DEFAULT_REP_SERIES_COUNT` andere Vorgaben setzen; Profil‑Editor zeigt Fallback.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.108",
|
||||
"date": "2026-05-12",
|
||||
"changes": [
|
||||
"Kombination rep/manual: Feld „Pause zw. Serien“ nur ab 2 Serien sichtbar und speicherbar; Hinweis unterscheidet Wechsel zur nächsten Station; API verwirft intra_rep_rest_sec bei nur einer Serie.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.107",
|
||||
"date": "2026-05-12",
|
||||
"changes": [
|
||||
"Kombination Wiederholungsziel: `rep_series_count` in `slot_profiles_v1` (mehrere Serien à Ziel‑Wdh.); Formular‑ und Profil‑Editor‑Felder; Pause als „zwischen Serien“ beschriftet; Coach‑Zusammenfassung angepasst.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.106",
|
||||
"date": "2026-05-12",
|
||||
|
|
|
|||
|
|
@ -21,17 +21,39 @@ function summarizeSlotProfilesRow(r) {
|
|||
if (r.load_sec != null) bits.push(`${r.load_sec}s Arbeit`)
|
||||
} else if (adv === 'rep') {
|
||||
bits.push('Ziel‑Wdh.')
|
||||
if (r.consecutive_reps != null) bits.push(`${r.consecutive_reps}×`)
|
||||
const nSer = r.rep_series_count != null && r.rep_series_count >= 1 ? r.rep_series_count : 1
|
||||
if (r.consecutive_reps != null) {
|
||||
if (nSer >= 2) bits.push(`${nSer} Serien à ${r.consecutive_reps}×`)
|
||||
else bits.push(`${r.consecutive_reps}×`)
|
||||
}
|
||||
} else {
|
||||
bits.push('Coach')
|
||||
if (r.consecutive_reps != null) bits.push(`Richtwert ${r.consecutive_reps}×`)
|
||||
const nSerMan = r.rep_series_count != null && r.rep_series_count >= 2 ? r.rep_series_count : 1
|
||||
if (r.consecutive_reps != null) {
|
||||
if (nSerMan >= 2) bits.push(`${nSerMan} Serien à Richtwert ${r.consecutive_reps}×`)
|
||||
else bits.push(`Richtwert ${r.consecutive_reps}×`)
|
||||
} else if (r.rep_series_count != null && r.rep_series_count >= 2) {
|
||||
bits.push(`${r.rep_series_count} Serien`)
|
||||
}
|
||||
}
|
||||
if (r.intra_rep_rest_sec != null) {
|
||||
if (adv === 'timed') bits.push(`Pause ${r.intra_rep_rest_sec}s`)
|
||||
else if (adv === 'rep' && r.rep_series_count != null && r.rep_series_count >= 2)
|
||||
bits.push(`Pause zw. Serien ${r.intra_rep_rest_sec}s`)
|
||||
else if (
|
||||
adv === 'manual' &&
|
||||
r.rep_series_count != null &&
|
||||
r.rep_series_count >= 2
|
||||
) {
|
||||
bits.push(`Pause zw. Serien ${r.intra_rep_rest_sec}s`)
|
||||
}
|
||||
}
|
||||
if (r.intra_rep_rest_sec != null) bits.push(`Pause ${r.intra_rep_rest_sec}s`)
|
||||
if (r.transition_after_sec != null) bits.push(`Wechsel ${r.transition_after_sec}s`)
|
||||
return bits.join(' · ')
|
||||
}
|
||||
|
||||
export default function CombinationCoachSlots({ combinationSlots, methodArchetype, methodProfile }) {
|
||||
const slots = useMemo(() => sortCombinationSlotsForDisplay(combinationSlots), [combinationSlots])
|
||||
|
||||
const candidateIds = useMemo(() => {
|
||||
const set = new Set()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useMemo, useState } from 'react'
|
||||
import { archetypeCoachHint, combinationArchetypeLabel, sortCombinationSlotsForDisplay } from '../constants/combinationArchetypes'
|
||||
import { archetypeCoachHint, combinationArchetypeLabel, sortCombinationSlotsForDisplay, defaultRepSeriesCountForArchetype } from '../constants/combinationArchetypes'
|
||||
import {
|
||||
METHOD_PROFILE_GUI_FIELDS,
|
||||
parseProfileJson,
|
||||
|
|
@ -10,6 +10,7 @@ import {
|
|||
patchSlotTimingField,
|
||||
patchSlotAdvanceMode,
|
||||
normalizeAdvanceMode,
|
||||
parseComboRepSeriesCountUi,
|
||||
applyCircuitRotateQuickRatio,
|
||||
applyIntervalDomainQuickRatio,
|
||||
} from '../utils/combinationMethodProfileUi'
|
||||
|
|
@ -116,6 +117,20 @@ export default function CombinationMethodProfileEditor({
|
|||
setPresetHint(null)
|
||||
}
|
||||
|
||||
const onSlotRepSeriesCount = (slotIx, rawStr) => {
|
||||
const trimmed = String(rawStr ?? '').trim()
|
||||
const effective = trimmed === '' ? '1' : trimmed
|
||||
const pn = parseInt(effective, 10)
|
||||
const clearIntra = !Number.isFinite(pn) || pn < 2
|
||||
const patched = patchMethodProfile(methodProfileJson || '{}', (d) => {
|
||||
patchSlotTimingField(d, slotIx, 'rep_series_count', effective)
|
||||
if (clearIntra) patchSlotTimingField(d, slotIx, 'intra_rep_rest_sec', '')
|
||||
})
|
||||
if (!patched.ok) return
|
||||
onChangeMethodProfileJson(patched.json)
|
||||
setPresetHint(null)
|
||||
}
|
||||
|
||||
const runCircuitPreset = (presetId) => {
|
||||
const r = patchMethodProfile(methodProfileJson || '{}', (draft) => {
|
||||
const pr = applyCircuitRotateQuickRatio(draft, presetId)
|
||||
|
|
@ -293,7 +308,11 @@ export default function CombinationMethodProfileEditor({
|
|||
const ttl = ((slot.title || '').trim() || `Station ${si}`).trim()
|
||||
const slotAdv = normalizeAdvanceMode(row.advance_mode)
|
||||
const serieLabel =
|
||||
slotAdv === 'timed' ? 'Wdh. ohne Wechsel' : slotAdv === 'rep' ? 'Ziel‑Wdh.' : 'Richtwert'
|
||||
slotAdv === 'timed' ? 'Wdh. ohne Wechsel' : slotAdv === 'rep' ? 'Wdh. / Serie' : 'Richtwert'
|
||||
const showMultiSeries = slotAdv === 'rep' || slotAdv === 'manual'
|
||||
const serienUi = parseComboRepSeriesCountUi(row.rep_series_count)
|
||||
const showInterSeriesPause = showMultiSeries && serienUi >= 2
|
||||
const intraLabel = slotAdv === 'timed' ? 'Pause zwischen Wdh.' : 'Pause zw. Serien'
|
||||
return (
|
||||
<div
|
||||
key={`slot-timing-${si}`}
|
||||
|
|
@ -359,22 +378,47 @@ export default function CombinationMethodProfileEditor({
|
|||
onChange={(e) => onSlotField(si, 'consecutive_reps', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{showMultiSeries ? (
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label
|
||||
className="form-label"
|
||||
style={{ fontSize: '11px' }}
|
||||
title="Wie oft die Wdh.-Zahl pro Serie hintereinander (mit Pause zwischen den Serien)?"
|
||||
>
|
||||
Serien
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={slotAdv === 'rep' ? 1 : undefined}
|
||||
className="form-input"
|
||||
placeholder="1"
|
||||
value={
|
||||
row.rep_series_count != null && String(row.rep_series_count) !== ''
|
||||
? String(row.rep_series_count)
|
||||
: String(defaultRepSeriesCountForArchetype(methodArchetype))
|
||||
}
|
||||
onChange={(e) => onSlotRepSeriesCount(si, e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{slotAdv === 'timed' || showInterSeriesPause ? (
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label" style={{ fontSize: '11px' }}>
|
||||
{intraLabel} (s)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
className="form-input"
|
||||
placeholder="–"
|
||||
value={row.intra_rep_rest_sec != null ? String(row.intra_rep_rest_sec) : ''}
|
||||
onChange={(e) => onSlotField(si, 'intra_rep_rest_sec', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label" style={{ fontSize: '11px' }}>
|
||||
Pause zwischen Wdh. (s)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
className="form-input"
|
||||
placeholder="–"
|
||||
value={row.intra_rep_rest_sec != null ? String(row.intra_rep_rest_sec) : ''}
|
||||
onChange={(e) => onSlotField(si, 'intra_rep_rest_sec', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label" style={{ fontSize: '11px' }}>
|
||||
Pause / Wechsel (s)
|
||||
Wechsel (s)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
|
|
@ -386,6 +430,12 @@ export default function CombinationMethodProfileEditor({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{showMultiSeries && serienUi < 2 ? (
|
||||
<p style={{ fontSize: '10px', color: 'var(--text3)', margin: '6px 0 0', lineHeight: 1.38 }}>
|
||||
<strong>Wechsel (s)</strong> zur <strong>nächsten Station</strong>. „Pause zw. Serien“ nur ab 2
|
||||
Serien.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -59,3 +59,17 @@ export function sortCombinationSlotsForDisplay(slotsRaw) {
|
|||
return String(a.title || '').localeCompare(String(b.title || ''), 'de')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Vorgabe „Serien“ pro Station bei Steuerung rep/manual, wenn kein Wert in `slot_profiles_v1` steht.
|
||||
* Nur Archetypen eintragen, die fachlich ≠ 1 verlangen; sonst Standard 1.
|
||||
*/
|
||||
export const ARCHETYPE_DEFAULT_REP_SERIES_COUNT = Object.freeze({})
|
||||
|
||||
export function defaultRepSeriesCountForArchetype(archetypeId) {
|
||||
const key = archetypeId != null ? String(archetypeId).trim() : ''
|
||||
const raw = key ? ARCHETYPE_DEFAULT_REP_SERIES_COUNT[key] : undefined
|
||||
const n = typeof raw === 'number' ? raw : raw != null ? parseInt(String(raw), 10) : NaN
|
||||
if (!Number.isFinite(n) || n < 1) return 1
|
||||
return Math.round(n)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ import {
|
|||
import { autoScrollForDragNearEdges } from '../utils/dragAutoScroll'
|
||||
import { SKILL_LEVEL_OPTIONS, normalizeSkillLevelSlug } from '../constants/skillLevels'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { COMBINATION_ARCHETYPE_OPTIONS } from '../constants/combinationArchetypes'
|
||||
import { readSlotProfilesV1, normalizeAdvanceMode } from '../utils/combinationMethodProfileUi'
|
||||
import { COMBINATION_ARCHETYPE_OPTIONS, ARCHETYPE_DEFAULT_REP_SERIES_COUNT, defaultRepSeriesCountForArchetype } from '../constants/combinationArchetypes'
|
||||
import { readSlotProfilesV1, normalizeAdvanceMode, parseComboRepSeriesCountUi } from '../utils/combinationMethodProfileUi'
|
||||
import { GripVertical } from 'lucide-react'
|
||||
|
||||
const INTENSITY_OPTIONS = [
|
||||
|
|
@ -57,6 +57,7 @@ function emptyComboSlotRow() {
|
|||
advance_mode: 'timed',
|
||||
load_sec: '',
|
||||
consecutive_reps: '',
|
||||
rep_series_count: '1',
|
||||
intra_rep_rest_sec: '',
|
||||
transition_after_sec: '',
|
||||
}
|
||||
|
|
@ -64,6 +65,8 @@ function emptyComboSlotRow() {
|
|||
|
||||
function comboSlotsFromDetail(exercise) {
|
||||
const raw = exercise?.combination_slots
|
||||
const arch = exercise?.method_archetype != null ? String(exercise.method_archetype).trim() : ''
|
||||
const serienFallback = defaultRepSeriesCountForArchetype(arch)
|
||||
const mp =
|
||||
exercise?.method_profile &&
|
||||
typeof exercise.method_profile === 'object' &&
|
||||
|
|
@ -83,13 +86,19 @@ function comboSlotsFromDetail(exercise) {
|
|||
const cands = Array.isArray(s.candidate_exercise_ids)
|
||||
? s.candidate_exercise_ids.map((x) => Number(x)).filter((n) => Number.isFinite(n))
|
||||
: []
|
||||
const mode = normalizeAdvanceMode(st.advance_mode)
|
||||
let repSer = ''
|
||||
if (st.rep_series_count != null) repSer = String(st.rep_series_count)
|
||||
else if (mode === 'rep' || mode === 'manual') repSer = String(serienFallback)
|
||||
else repSer = '1'
|
||||
return {
|
||||
title: s.title != null ? String(s.title) : '',
|
||||
candidate_exercise_ids: cands,
|
||||
exercise_title_by_id: {},
|
||||
advance_mode: normalizeAdvanceMode(st.advance_mode),
|
||||
advance_mode: mode,
|
||||
load_sec: st.load_sec != null ? String(st.load_sec) : '',
|
||||
consecutive_reps: st.consecutive_reps != null ? String(st.consecutive_reps) : '',
|
||||
rep_series_count: repSer,
|
||||
intra_rep_rest_sec: st.intra_rep_rest_sec != null ? String(st.intra_rep_rest_sec) : '',
|
||||
transition_after_sec: st.transition_after_sec != null ? String(st.transition_after_sec) : '',
|
||||
}
|
||||
|
|
@ -1152,7 +1161,26 @@ function ExerciseFormPage() {
|
|||
<select
|
||||
className="form-input"
|
||||
value={formData.method_archetype || ''}
|
||||
onChange={(e) => updateFormField('method_archetype', e.target.value)}
|
||||
onChange={(e) => {
|
||||
const arch = (e.target.value || '').trim()
|
||||
const forced = ARCHETYPE_DEFAULT_REP_SERIES_COUNT[arch]
|
||||
setFormDirty(true)
|
||||
setFormData((prev) => {
|
||||
const slots = prev.combination_slots || []
|
||||
const nextSlots =
|
||||
forced !== undefined && forced !== null
|
||||
? slots.map((row) =>
|
||||
normalizeAdvanceMode(row.advance_mode) !== 'timed'
|
||||
? {
|
||||
...row,
|
||||
rep_series_count: String(Math.max(1, Math.round(Number(forced)))),
|
||||
}
|
||||
: row,
|
||||
)
|
||||
: slots
|
||||
return { ...prev, method_archetype: arch, combination_slots: nextSlots }
|
||||
})
|
||||
}}
|
||||
>
|
||||
<option value="">— noch nicht festgelegt —</option>
|
||||
{COMBINATION_ARCHETYPE_OPTIONS.map((o) => (
|
||||
|
|
@ -1197,8 +1225,12 @@ function ExerciseFormPage() {
|
|||
const comboPoolFull = candIds.length >= MAX_COMBO_CANDIDATES_PER_STATION
|
||||
const slotAdv = normalizeAdvanceMode(row.advance_mode)
|
||||
const serieLabel =
|
||||
slotAdv === 'timed' ? 'Serie' : slotAdv === 'rep' ? 'Ziel‑Wdh.' : 'Richtwert'
|
||||
slotAdv === 'timed' ? 'Serie' : slotAdv === 'rep' ? 'Wdh. / Serie' : 'Richtwert'
|
||||
const seriePlaceholder = slotAdv === 'rep' ? '10' : slotAdv === 'manual' ? '–' : '1'
|
||||
const showMultiSeries = slotAdv === 'rep' || slotAdv === 'manual'
|
||||
const serienCountUi = parseComboRepSeriesCountUi(row.rep_series_count)
|
||||
const showInterSeriesPause = showMultiSeries && serienCountUi >= 2
|
||||
const intraLabel = slotAdv === 'timed' ? 'Pause (s)' : 'Pause zw. Serien'
|
||||
const lbl =
|
||||
row.exercise_title_by_id && typeof row.exercise_title_by_id === 'object'
|
||||
? row.exercise_title_by_id
|
||||
|
|
@ -1361,6 +1393,14 @@ function ExerciseFormPage() {
|
|||
const m = normalizeAdvanceMode(e.target.value)
|
||||
const patch = { advance_mode: m }
|
||||
if (m !== 'timed') patch.load_sec = ''
|
||||
if (m === 'rep' || m === 'manual') {
|
||||
const curSer = String(row.rep_series_count ?? '').trim()
|
||||
if (!curSer) {
|
||||
patch.rep_series_count = String(
|
||||
defaultRepSeriesCountForArchetype(formData.method_archetype || ''),
|
||||
)
|
||||
}
|
||||
}
|
||||
patchComboSlotRow(idx, patch)
|
||||
}}
|
||||
>
|
||||
|
|
@ -1373,8 +1413,8 @@ function ExerciseFormPage() {
|
|||
{slotAdv === 'timed'
|
||||
? 'Arbeit (s): geplantes Ende nach Countdown möglich. Serie: Wiederholungen ohne Stationswechsel innerhalb einer Phase.'
|
||||
: slotAdv === 'rep'
|
||||
? 'Ohne Pflicht-Arbeits-Timer: Ziel über Wiederholungen; Pause/Wechsel können weiter automatisch unterstützt werden.'
|
||||
: 'Ohne feste Arbeitsuhr auf dieser Station — Fortschritt im Coach später per Tippschritt; Pause/Wechsel optional weiter mit Sekunden.'}
|
||||
? 'Ohne Pflicht-Arbeits-Timer: Ziel über Wiederholungen. Ab zwei Serien: Pause zwischen diesen Serien; sonst nur Wechsel zur nächsten Station.'
|
||||
: 'Coach: keine feste Arbeitsuhr — Fortschritt später per Tipp. Ab 2 Serien: Pause zwischen Serien; sonst nur Wechsel zur nächsten Station zeitlich planen.'}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -1414,20 +1454,45 @@ function ExerciseFormPage() {
|
|||
onChange={(e) => patchComboSlotRow(idx, { consecutive_reps: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label" style={{ fontSize: '10px' }}>
|
||||
Pause (s)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
className="form-input"
|
||||
style={comboTinyNumberInputSx}
|
||||
placeholder="–"
|
||||
value={row.intra_rep_rest_sec || ''}
|
||||
onChange={(e) => patchComboSlotRow(idx, { intra_rep_rest_sec: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
{showMultiSeries ? (
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label" style={{ fontSize: '10px' }} title="Wie oft die angegebene Wdh.-Zahl hintereinander (mit Pause zw. Serien)?">
|
||||
Serien
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={slotAdv === 'rep' ? 1 : undefined}
|
||||
className="form-input"
|
||||
style={comboTinyNumberInputSx}
|
||||
placeholder="1"
|
||||
value={row.rep_series_count || ''}
|
||||
onChange={(e) => {
|
||||
let rawSer = e.target.value.trim()
|
||||
if (rawSer === '') rawSer = '1'
|
||||
const pn = parseInt(String(rawSer).trim(), 10)
|
||||
const patch = { rep_series_count: rawSer }
|
||||
if (!Number.isFinite(pn) || pn < 2) patch.intra_rep_rest_sec = ''
|
||||
patchComboSlotRow(idx, patch)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{slotAdv === 'timed' || showInterSeriesPause ? (
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label" style={{ fontSize: '10px' }}>
|
||||
{intraLabel}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
className="form-input"
|
||||
style={comboTinyNumberInputSx}
|
||||
placeholder="–"
|
||||
value={row.intra_rep_rest_sec || ''}
|
||||
onChange={(e) => patchComboSlotRow(idx, { intra_rep_rest_sec: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label" style={{ fontSize: '10px' }}>
|
||||
Wechsel (s)
|
||||
|
|
@ -1443,6 +1508,12 @@ function ExerciseFormPage() {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{showMultiSeries && serienCountUi < 2 ? (
|
||||
<p style={{ fontSize: '10px', color: 'var(--text3)', margin: '6px 0 0', lineHeight: 1.38 }}>
|
||||
<strong>Wechsel (s)</strong> = Pause bis zur <strong>nächsten Station</strong>. Feld „Pause zw.
|
||||
Serien“ erscheint erst ab 2 Serien (sonst keine Pause zwischen zwei Blöcken nötig).
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { stripHtmlToText } from './htmlUtils'
|
||||
import { normalizeAdvanceMode } from './combinationMethodProfileUi'
|
||||
import { normalizeAdvanceMode, parseComboRepSeriesCountUi } from './combinationMethodProfileUi'
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || ''
|
||||
|
||||
|
|
@ -546,11 +546,24 @@ export function buildExerciseApiPayload(formData, extras = {}) {
|
|||
if (advanceMode !== 'timed') o.advance_mode = advanceMode
|
||||
const load = parseTimingField(row.load_sec)
|
||||
const crs = parseTimingField(row.consecutive_reps)
|
||||
const rsc = parseTimingField(row.rep_series_count)
|
||||
const intra = parseTimingField(row.intra_rep_rest_sec)
|
||||
const tran = parseTimingField(row.transition_after_sec)
|
||||
const serienUi = parseComboRepSeriesCountUi(row.rep_series_count)
|
||||
const allowInterSeriesPause =
|
||||
advanceMode === 'timed' ||
|
||||
((advanceMode === 'rep' || advanceMode === 'manual') && serienUi >= 2)
|
||||
|
||||
if (advanceMode === 'timed' && load !== undefined && load >= 0) o.load_sec = Math.round(load)
|
||||
if (crs !== undefined && crs >= 1) o.consecutive_reps = Math.round(crs)
|
||||
if (intra !== undefined && intra >= 0) o.intra_rep_rest_sec = Math.round(intra)
|
||||
if (
|
||||
rsc !== undefined &&
|
||||
rsc >= 1 &&
|
||||
(advanceMode === 'rep' || advanceMode === 'manual')
|
||||
) {
|
||||
o.rep_series_count = Math.round(rsc)
|
||||
}
|
||||
if (intra !== undefined && intra >= 0 && allowInterSeriesPause) o.intra_rep_rest_sec = Math.round(intra)
|
||||
if (tran !== undefined && tran >= 0) o.transition_after_sec = Math.round(tran)
|
||||
if (Object.keys(o).length > 1) slot_profiles_v1_next.push(o)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,15 @@ export function normalizeAdvanceMode(v) {
|
|||
return 'timed'
|
||||
}
|
||||
|
||||
/** UI: Serien-Anzahl aus Formularfeld; leer/ungültig ⇒ 1 (eine Serie). */
|
||||
export function parseComboRepSeriesCountUi(raw) {
|
||||
if (raw === '' || raw === undefined || raw === null) return 1
|
||||
const n =
|
||||
typeof raw === 'number' && Number.isFinite(raw) ? Math.round(raw) : parseInt(String(raw).trim(), 10)
|
||||
if (!Number.isFinite(n) || n < 1) return 1
|
||||
return n
|
||||
}
|
||||
|
||||
function parseProfileJson(raw) {
|
||||
if (typeof raw !== 'string' || !raw.trim()) return { ok: true, obj: {} }
|
||||
try {
|
||||
|
|
@ -225,6 +234,7 @@ export function readSlotProfilesV1(profileObj) {
|
|||
advance_mode: mode,
|
||||
load_sec: normalizeOptionalNonNegInt(row.load_sec),
|
||||
consecutive_reps: normalizeOptionalPositiveInt(row.consecutive_reps),
|
||||
rep_series_count: normalizeOptionalPositiveInt(row.rep_series_count),
|
||||
intra_rep_rest_sec: normalizeOptionalNonNegInt(row.intra_rep_rest_sec),
|
||||
transition_after_sec: normalizeOptionalNonNegInt(row.transition_after_sec),
|
||||
}
|
||||
|
|
@ -249,6 +259,7 @@ function normalizeOptionalPositiveInt(v) {
|
|||
const SLOT_TIMING_FIELDS = /** @type {const} */ ([
|
||||
'load_sec',
|
||||
'consecutive_reps',
|
||||
'rep_series_count',
|
||||
'intra_rep_rest_sec',
|
||||
'transition_after_sec',
|
||||
])
|
||||
|
|
@ -322,7 +333,7 @@ export function patchSlotTimingField(profileDraft, slotIndex, field, rawInput) {
|
|||
|
||||
if (rawInput === null || rawInput === undefined || String(rawInput).trim() === '') {
|
||||
delete nextRow[field]
|
||||
} else if (field === 'consecutive_reps') {
|
||||
} else if (field === 'consecutive_reps' || field === 'rep_series_count') {
|
||||
const n = normalizeOptionalPositiveInt(rawInput)
|
||||
if (n === undefined) delete nextRow[field]
|
||||
else nextRow[field] = n
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user