feat(version): bump to 0.8.104 and enhance combination exercise features
All checks were successful
Deploy Development / deploy (push) Successful in 40s
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 40s
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.104, reflecting recent improvements in combination exercise handling. - Enhanced the CombinationMethodProfileEditor to support structured slot timing profiles without requiring JSON input from trainers. - Introduced quick ratio presets for circuit and interval training methods, improving user experience in setting up training profiles. - Updated documentation and changelog to reflect new features and integration details. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4e654e50c0
commit
435da7f17a
|
|
@ -1,7 +1,7 @@
|
|||
# Trainingsmodule und Kombinationsübungen — fachliche Spezifikation V3
|
||||
|
||||
**Status:** fachlicher Spezifikationsentwurf
|
||||
**Stand:** 2026-05-12 · **Coaching/Archetypen:** § 10.2.1, § 10.4–10.5, **§ 5.4/§ 6.3** Methoden/Archetypen/Zeitschicht · **Anhang A** (Abgleich Code vs. Spec)
|
||||
**Stand:** 2026-05-12 (Anhang A **grob** App **0.8.104**; Zeit‑Pfad **`COMBINATION_TIMING_PROFILE_PLAN.md`**) · **Coaching/Archetypen:** § 10.2.1, § 10.4–10.5, **§ 5.4/§ 6.3** Methoden/Archetypen/Zeitschicht · **Anhang A**
|
||||
**Zweck:** Produkt- und Fachspezifikation für Trainingsmodule, Kombinationsübungen, Trainingsmethodenbezug, Planungsintegration und Coaching-Modus in Shinkan.
|
||||
|
||||
**Wichtige Leitlinie dieser Version:**
|
||||
|
|
@ -417,8 +417,12 @@ Alle diese Angaben sind **Anweisungen an den Trainer** und **Coach‑Assistenz**
|
|||
|
||||
**Nach Einplanung in eine konkrete Trainingseinheit** muss diese Zeitschicht (oder ihr Abgleich mit der Einheitsposition) für den Trainer **bearbeitbar** bleiben, **ohne** die Bibliotheksvorlage still zu überschreiben (kopier-/instanzbasierte Anpassungen — siehe bereits § 2.5 und § 8.3).
|
||||
|
||||
**Umsetzung in der App (Stand 0.8.103):** Pro Übungszeile in einer Trainingseinheit kann optional ein **JSON-Snapshot** des Ablaufprofils gespeichert werden (`planning_method_profile` in der DB). **`null`** bedeutet: es wirkt das Ablaufprofil aus dem **Katalog** (`method_profile` der Übung). Ist ein Snapshot gesetzt, ersetzt er den Katalog **vollständig** für diese Platzierung (kein serverseitiges Zusammenführen). Bearbeitung in der Planungs-UI: aufklappbarer Block **„Ablaufprofil für diese Planung (Kombination)“** mit denselben geführten Feldern wie im Übungsformular.
|
||||
|
||||
**Coach:** soll die wirksamen Werte nach **Übernahme** und **Einheitsübersteuerungen** konsistent nachvollziehen (**§ 10.4**).
|
||||
|
||||
**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.
|
||||
|
||||
### 6.4 Slot- und Pool-Logik
|
||||
|
||||
Slots können fest oder variabel sein.
|
||||
|
|
@ -512,7 +516,7 @@ Produktregel:
|
|||
Nach dem Einfügen muss ein Planungsblock lokal angepasst werden können:
|
||||
|
||||
* Dauer ändern,
|
||||
* **bei Kombinationsübungen:** im Idealfall **`method_profile` (Arbeit, Erholung, Durchläufe)** und Stations-/Slot-Anpassungen des **konkreten Vorkommens**, nicht nur Gesamtzeit,
|
||||
* **bei Kombinationsübungen:** Ablaufprofil **optional nur für diese Platzierung** überschreiben (aktuell: Snapshot parallel zum Katalog-`method_profile`, z. B. Arbeit-, Erholungs- und Runden-/Intervallangaben über die gleichen strukturierten Felder wie im Übungskatalog) — zusätzlich zu den **Gepl.-Min.** am Eintrag; **Stations-/Slot-Austausch** am konkreten Vorkommen weiter über die bestehende Übungs-/Planungslogik, nicht gesondert als „Kombi-Programmierung“ je Zeile,
|
||||
* Übung austauschen,
|
||||
* Station ergänzen,
|
||||
* Hinweise anpassen,
|
||||
|
|
@ -787,7 +791,7 @@ Die Spezifikation ist daher kein technisches Pflichtenheft, sondern ein fachlich
|
|||
|
||||
---
|
||||
|
||||
## Anhang A — Implementierungsabgleich (Stand Code: App **0.8.102**, grob)
|
||||
## Anhang A — Implementierungsabgleich (Stand Code: App **0.8.104**, grob)
|
||||
|
||||
Zweck: dieselbe Tabelle für **Produkt / Architekt / Agent** beim nächsten Schritt; verhindert „wir haben X gebaut, die Spec sagt aber Y“ ohne dass es dokumentiert wird.
|
||||
|
||||
|
|
@ -795,10 +799,10 @@ Zweck: dieselbe Tabelle für **Produkt / Architekt / Agent** beim nächsten Schr
|
|||
|--------------------------------------------|-----------------|---------------------------------|-------------------------------------|
|
||||
| **Trainingsmodule (Bibliothek)** | Wiederverwendbare Blöcke, Kopier-Einfügen in Einheit | Bibliothek, API, Übernahme-Modal, Lineage-Spalte | **Phase 3** des Umsetzungsplans: erweiterter Übernahmemodus |
|
||||
| **Kombinationsübung im Katalog** | `exercise_kind=combination`, Slots, Pools (Kandidaten) | Migration 056, CRUD Übung mit `combination_slots`, GET liefert Slots + Kandidatentitel | Fachbezug Haupt-/Nebenmethoden aus § 4/§ 6 dort umsetzen, wo die Domäne es noch nicht abdeckt |
|
||||
| **Archetyp + Ablaufprofil am Katalogobjekt** | `method_archetype`, JSON `method_profile` | Persistenz; Übungsformular: **geführte Felder** nach Archetyp (`CombinationMethodProfileEditor`, `combinationMethodProfileUi.js`) + eingeklapptes Roh‑JSON | Schema‑Validierung serverseitig noch offen; UI für Pflicht je Archetyp (§ 10.5) weiter schärfen |
|
||||
| **Einplanbarkeit (normale Planung)** | Kombi wie Übung in Sektionen; **Zeitprofil‑Overrides** nach § 8.3 / § 10.5.1 | Picker, `exercise_kind` in Form/PUT, keine Variante bei Kombi; **Overrides von `method_profile` am Platzierungseintrag fehlen** | Planungs-UI/API: kopiertes **`method_profile` pro Einheit/item** bearbeitbar; Planungsblöcke (Phase 3) |
|
||||
| **Zeitphasen (global / pro Slot)** | § 6.3 | Über `method_profile` teilweise (globale Schlüssel im Formular); **keine strukturierten slotgebundenen Zeitlisten** im UI | `slot_timing[]` oder äquivalent definieren und editieren |
|
||||
| **Coaching Stufe A** | Slots + Kandidaten sichtbar, Archetyp‑Hinweis, Profil lesbar | `CombinationCoachSlots` zeigt **Key/Value** aus `method_profile`, sonst wie zuvor | Profilwerte **lesend** benutzerfreundlicher labeln (statt nur Schlüsselnamen) |
|
||||
| **Archetyp + Ablaufprofil am Katalogobjekt** | `method_archetype`, JSON `method_profile` (+ Pilot **`slot_profiles_v1`** je Station in derselben JSON‑Struktur) | Persistenz; Übungsformular: **geführte globale Felder** + **pro Slot** vier Zeitreihen ohne Nutzer‑JSON‑Pflicht; Schnellwahl typische Arbeit/Pause‑Relationen (**Zirkel**, **Intervall**); Reihenfolge UX: Stationen vor Ablaufprofil | JSON‑„Experte“ weiter abschaltbar; Schema‑Pflichtfelder nach Archetyp; Konvergenz flache Schlüssel ↔ `timing_schema` (siehe Arbeitsplan) |
|
||||
| **Einplanbarkeit (normale Planung)** | Kombi in Sektionen; **Zeitprofil‑Overrides** nach § 8.3 / § 10.5.1 | Picker, `exercise_kind` in Form/PUT, keine Variante bei Kombi; **Override:** DB **`planning_method_profile`** je Sektions-Item (Migration **057**), Planungseditor: Details „Ablaufprofil für diese Planung“, **„Planung wie Katalog“** / **„Aus Katalog kopieren“** | Planungsblöcke als Produktkonzept · Phase 3; serverseitige Validierung Snapshot↔Archetyp optional |
|
||||
| **Zeitphasen (global / pro Slot)** | § 6.3 | Über `method_profile` / Planungs‑Snapshot (**gleiche JSON-Struktur** wie Katalogprofil): globale Schlüssel im Übungs- und Planungseditor; weiterhin **keine** eigenständigen slotgebundenen Zeitlisten im UI | `slot_timing[]` oder äquivalent definieren und editieren |
|
||||
| **Coaching Stufe A** | Slots + Kandidaten sichtbar, Archetyp‑Hinweis, Profil lesbar | `CombinationCoachSlots`: wirksames Profil = **Planungs‑Snapshot wenn gesetzt, sonst Katalog**; Anzeige **Key/Value** | Profilwerte **lesend** benutzerfreundlicher labeln (statt nur Schlüsselnamen) |
|
||||
| **Coaching Stufe B** | Zeitleiste archetypnah (z. B. Schritt pro Station) | **Nein** — ein Coach‑Schritt = ein Planungsitem | Designentscheid: virtuelle Substeps vs. DB‑Materialisierung; Auswirkung auf Ist‑Zeit pro Item |
|
||||
| **Coaching Stufe C** | Timer/Wechsel/Abhaken nach Archetyp | Nur **generischer** Coach‑Timer pro Planungsitem | Pro Archetyp UI‑State + Anbindung an `method_profile` |
|
||||
| **Rahmenprogramm** | Gleiche Inhalte wie Einheit | Slot‑Blueprint, `from-framework-slot` | Modul-/Kombi‑UX in Rahmen wie in Einheit konsolidieren (Phase 5) |
|
||||
|
|
|
|||
100
.claude/docs/working/COMBINATION_TIMING_PROFILE_PLAN.md
Normal file
100
.claude/docs/working/COMBINATION_TIMING_PROFILE_PLAN.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Kombinations‑Ablaufprofil — Zeitmodell, Archetyp‑Vorgaben, Umsetzung
|
||||
|
||||
**Zweck:** Fach-/Technik-Brücke zwischen Wunschbild („kein Nutzer‑JSON“, globale und slotbezogene Eckwerte, Archetyp‑Strukturen) und bestehendem Speicher **`method_profile` (JSON)** + **`planning_method_profile`** auf Planungszeilen.
|
||||
|
||||
**Bezüge:** `.claude/docs/functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` (§ 6.3 / § 8.3); Frontend `CombinationMethodProfileEditor`, `combinationMethodProfileUi.js`; Archetyp‑IDs siehe Backend `COMBINATION_ARCHETYPE_IDS` / Frontend `COMBINATION_ARCHETYPE_OPTIONS`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Grundprinzipien
|
||||
|
||||
| Prinzip | Beschreibung |
|
||||
|--------|--------------|
|
||||
| **Kein Pflicht‑JSON für Trainer** | Alle trainertypischen Pflegepfade nur über geführte Felder + Archetyp‑Vorschlagsknöpfe. |
|
||||
| **JSON bleibt Transport** | Persistenz geschieht weiter in `method_profile` / Kopie in `planning_method_profile`; **kanonische Schlüssel** werden hier und in Codekommentaren festgehalten. |
|
||||
| **Archetyp = Struktur + Defaults** | Wechsel des Archetyps soll (optional/togglebar) Grundwerte oder typische Relationen vorbelegen können — keine stillen Überschreibungen ohne Hinweis. |
|
||||
| **`free_method_block` = Maximale Freiheit** | Entspricht „maximaler Konfiguration“: alle relevanten Timing‑Dimensionen über UI, insbesondere **pro Slot**; keine impliziten stationären Constraints. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Kanonisches Zeit‑Schema (`timing_schema`)
|
||||
|
||||
**Empfohlene Versionierung im Objekt:**
|
||||
|
||||
- **`timing_schema: 1`** — sobald neue globale/strukturierte Felder aktiv genutzt werden (Pilot; UI kann ohne Migration starten durch parallele Schlüssel).
|
||||
|
||||
### 2.1 Globalebene (`method_profile`)
|
||||
|
||||
| Feld (Pilot) | Semantik |
|
||||
|----------------|----------|
|
||||
| `timing_schema` | `1` wenn Block unten aktiv |
|
||||
| `intro_sec` oder bestehend `block_intro_sec` | einmalige Einführung/Demo am Block |
|
||||
| `rounds` (bzw. bei Intervallen `interval_rounds` — Angleich später) | komplette Durchläufe des Musters |
|
||||
| *Planned totals* nur **berechnete Anzeige** in UI, optional persistiert z. B. `planned_total_duration_min_hint` später |
|
||||
|
||||
Relationen **Zwischen Arbeit und Pause** können als Schnellwahl gesetzt werden (kein eigener Persist‑Erzwing‑Typ nötig), indem konkrete Sekunden geschrieben werden.
|
||||
|
||||
### 2.2 Slots (`slot_profiles_v1`)
|
||||
|
||||
Array synchron zu `slot_index`; fehlende Einträge = „nicht gefüllt / aus globalen Eckdaten ableiten wo sinnvoll“.
|
||||
|
||||
Objekt‑Shape (Sekunden, ganze Zahlen ≥ 0):
|
||||
|
||||
```json
|
||||
{
|
||||
"slot_index": 0,
|
||||
"load_sec": 40,
|
||||
"consecutive_reps": 1,
|
||||
"intra_rep_rest_sec": 10,
|
||||
"transition_after_sec": 15
|
||||
}
|
||||
```
|
||||
|
||||
| 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. |
|
||||
| `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.
|
||||
|
||||
---
|
||||
|
||||
## 3. Archetyp → typische Schnellwahl (Überblicks‑Matrix)
|
||||
|
||||
| Archetyp | Globale Schnellwahl (Beispiele) | Slots |
|
||||
|----------|---------------------------------|-------|
|
||||
| `circuit_rotate_time` | Arbeit; Rotation „≈ Arbeit“ oder „Pause 2/3 Arbeit“ bezogen auf Rund‑Pausen/Rotation wo im UI dokumentiert | sinnvoll ab **timing_schema** geführt |
|
||||
| `time_domain_interval` | Pause = Arbeit; Pause = 2/3 Arbeit (auf `rest_seconds`↔`work_seconds`) | optional |
|
||||
| `sequence_linear` | Einführung + grobe Sek./Station | **slot_profiles_v1** priorisiert |
|
||||
| `circuit_all_parallel` | Erklärzeit, gemeinsamer Start | Slots optional |
|
||||
| `pair_superset` | Wechsel A↔B, Arbeit je Seite (+ später erweiterbar) | 2‑Slot‑Fokus |
|
||||
| `free_method_block` | alle globalen Slots optional | **Pfad für maximale Flex** |
|
||||
| `station_parcour` | Reihenfolge frei‑Flag bestehend | pro Station Verweilen sinnvoll |
|
||||
|
||||
**Pyramidal (später):** neue Archetyp‑ID **`pyramid_interval`** o. ä. oder Flag `pyramid_recovery_rule` mit Regelwerk „Pause abhängig von letzter Belastung“ — **explizit out of scope** bis Regeln feststehen.
|
||||
|
||||
---
|
||||
|
||||
## 4. UX‑Normen
|
||||
|
||||
- **Trainingsplanung** (`plannerMode`): **keine** Roh‑JSON‑Oberfläche.
|
||||
- **Übungsformular**: Roh‑JSON nur wenn `allowExpertJson === true` (Default false; später z. B. Superadmin/Dev).
|
||||
- **Coaching‑Ansicht**: nur **wirksame** Zahlen aus Snapshot/Katalog darstellen, mittelfristig Labels statt Schlüsseln.
|
||||
|
||||
---
|
||||
|
||||
## 5. Phasen (Implementierung)
|
||||
|
||||
| Phase | Inhalt |
|
||||
|-------|--------|
|
||||
| **1 (jetzt)** | Slot‑Zeilen‑UI über `slot_profiles_v1`; Schnellwahl‑Ratios für `circuit_rotate_time` + `time_domain_interval`; `plannerMode` ohne JSON; `allowExpertJson` default false |
|
||||
| **2** | Beim Archetypwechsel **optionales** Modal „Archetyp‑Vorlage anwenden?“ mit nicht‑destruktivem Merge |
|
||||
| **3** | Geplante **Gesamtzeit** konsistent rechnerisch (Summe Slots × Runden + Global) mit Transparenz in UI |
|
||||
| **4** | Konsolidierung flacher Schlüssel → **`timing_schema`** v1‑only im Editor |
|
||||
| **5** | Pyramide / adaptive Recovery |
|
||||
|
||||
---
|
||||
|
||||
**Pflege:** Änderungen an Schlüsseln oder Phasen hier und in Anhang A der Fachspez mitziehen.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.103"
|
||||
APP_VERSION = "0.8.104"
|
||||
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.24.2", # Kombi: geführtes method_profile im Übungsformular nach Archetyp + Coach zeigt Profil als Key/Wert
|
||||
"exercises": "2.25.0", # Kombi: slot_profiles_v1 + Schnellwahl Belastung/Erholung; keine Nutzer‑JSON‑Pflicht; Übungsform Stationen vor Ablaufprofil
|
||||
"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,14 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.104",
|
||||
"date": "2026-05-12",
|
||||
"changes": [
|
||||
"Kombinations‑Ablaufprofil UX: Stationszeilen (slot_profiles_v1); Schnellwahlen Arbeit↔Pause (Zirkel + Intervall); Planungs‑Override ohne JSON; Übungsformular: Reihenfolge Stationen dann Ablaufprofil.",
|
||||
"Arbeitspapier: `.claude/docs/working/COMBINATION_TIMING_PROFILE_PLAN.md`.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.103",
|
||||
"date": "2026-05-12",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import React, { useMemo, useState } from 'react'
|
||||
import { archetypeCoachHint, combinationArchetypeLabel } from '../constants/combinationArchetypes'
|
||||
import { archetypeCoachHint, combinationArchetypeLabel, sortCombinationSlotsForDisplay } from '../constants/combinationArchetypes'
|
||||
import {
|
||||
METHOD_PROFILE_GUI_FIELDS,
|
||||
parseProfileJson,
|
||||
setFullProfileRawJson,
|
||||
updateProfileGuided,
|
||||
patchMethodProfile,
|
||||
readSlotProfilesV1,
|
||||
patchSlotTimingField,
|
||||
applyCircuitRotateQuickRatio,
|
||||
applyIntervalDomainQuickRatio,
|
||||
} from '../utils/combinationMethodProfileUi'
|
||||
|
||||
function clampInt(n, min, max) {
|
||||
|
|
@ -15,13 +20,31 @@ function clampInt(n, min, max) {
|
|||
return Math.round(x)
|
||||
}
|
||||
|
||||
/** Archetypen mit klar bezifferbarer Stationslogik · alle mit Slot-Liste sinnvoll */
|
||||
const ARCHETYPES_WITH_SLOT_TIMING = new Set([
|
||||
'circuit_rotate_time',
|
||||
'sequence_linear',
|
||||
'station_parcour',
|
||||
'time_domain_interval',
|
||||
'circuit_all_parallel',
|
||||
'pair_superset',
|
||||
'free_method_block',
|
||||
])
|
||||
|
||||
/**
|
||||
* Kombination: geführtes Ablaufprofil + optionales Roh-JSON.
|
||||
* Kombination: geführtes method_profile (+ optional Stationszeilen, ohne JSON für Trainer).
|
||||
*
|
||||
* @param {boolean} [props.plannerMode] — z. B. Planungs‑Override: keine Roh‑JSON‑Sektion.
|
||||
* @param {boolean} [props.allowExpertJson] — wenn true und nicht plannerMode: Roh‑JSON (Support).
|
||||
* @param {{ slot_index?: number|string, title?: string }[]} [props.comboSlotsOutline] — für Slot‑Felder aus der Übung
|
||||
*/
|
||||
export default function CombinationMethodProfileEditor({
|
||||
methodArchetype,
|
||||
methodProfileJson,
|
||||
onChangeMethodProfileJson,
|
||||
plannerMode = false,
|
||||
allowExpertJson = false,
|
||||
comboSlotsOutline = null,
|
||||
}) {
|
||||
const arch = typeof methodArchetype === 'string' ? methodArchetype.trim() : ''
|
||||
const fieldsGui = METHOD_PROFILE_GUI_FIELDS[arch]
|
||||
|
|
@ -29,20 +52,36 @@ export default function CombinationMethodProfileEditor({
|
|||
const parseState = useMemo(() => parseProfileJson(methodProfileJson || '{}'), [methodProfileJson])
|
||||
const [rawOpenError, setRawOpenError] = useState(null)
|
||||
const [rawDraft, setRawDraft] = useState(null)
|
||||
const [presetHint, setPresetHint] = useState(null)
|
||||
|
||||
const profileObj = parseState.ok ? parseState.obj : {}
|
||||
|
||||
const outlineSorted = useMemo(() => {
|
||||
if (!comboSlotsOutline || !Array.isArray(comboSlotsOutline) || comboSlotsOutline.length === 0) return []
|
||||
return sortCombinationSlotsForDisplay(comboSlotsOutline)
|
||||
}, [comboSlotsOutline])
|
||||
|
||||
const showSlotTiming =
|
||||
ARCHETYPES_WITH_SLOT_TIMING.has(arch) && outlineSorted.length > 0
|
||||
|
||||
const slotRowsModel = useMemo(() => readSlotProfilesV1(profileObj), [profileObj])
|
||||
|
||||
const lookupSlotTiming = (slotIndex) =>
|
||||
slotRowsModel.find((r) => Number(r.slot_index) === Number(slotIndex)) || {}
|
||||
|
||||
const applyGuided = (key, value, kind) => {
|
||||
if (kind === 'bool') {
|
||||
const res = updateProfileGuided(arch, methodProfileJson || '{}', key, value, 'bool')
|
||||
if (!res.ok) return
|
||||
onChangeMethodProfileJson(res.json)
|
||||
setPresetHint(null)
|
||||
return
|
||||
}
|
||||
if (value === '' || value === undefined || value === null) {
|
||||
const res = updateProfileGuided(arch, methodProfileJson || '{}', key, '', 'int')
|
||||
if (!res.ok) return
|
||||
onChangeMethodProfileJson(res.json)
|
||||
setPresetHint(null)
|
||||
return
|
||||
}
|
||||
const num = typeof value === 'number' ? value : parseInt(String(value), 10)
|
||||
|
|
@ -53,6 +92,36 @@ export default function CombinationMethodProfileEditor({
|
|||
const res = updateProfileGuided(arch, methodProfileJson || '{}', key, c, 'int')
|
||||
if (!res.ok) return
|
||||
onChangeMethodProfileJson(res.json)
|
||||
setPresetHint(null)
|
||||
}
|
||||
|
||||
const onSlotField = (slotIx, field, rawStr) => {
|
||||
const patched = patchMethodProfile(methodProfileJson || '{}', (d) =>
|
||||
patchSlotTimingField(d, slotIx, field, rawStr)
|
||||
)
|
||||
if (!patched.ok) return
|
||||
onChangeMethodProfileJson(patched.json)
|
||||
setPresetHint(null)
|
||||
}
|
||||
|
||||
const runCircuitPreset = (presetId) => {
|
||||
const r = patchMethodProfile(methodProfileJson || '{}', (draft) => {
|
||||
const pr = applyCircuitRotateQuickRatio(draft, presetId)
|
||||
if (!pr.ok) setPresetHint(pr.error || '')
|
||||
else setPresetHint(null)
|
||||
})
|
||||
if (!r.ok) return
|
||||
onChangeMethodProfileJson(r.json)
|
||||
}
|
||||
|
||||
const runIntervalPreset = (presetId) => {
|
||||
const r = patchMethodProfile(methodProfileJson || '{}', (draft) => {
|
||||
const pr = applyIntervalDomainQuickRatio(draft, presetId)
|
||||
if (!pr.ok) setPresetHint(pr.error || '')
|
||||
else setPresetHint(null)
|
||||
})
|
||||
if (!r.ok) return
|
||||
onChangeMethodProfileJson(r.json)
|
||||
}
|
||||
|
||||
const archeLabel = arch ? combinationArchetypeLabel(arch) : null
|
||||
|
|
@ -63,8 +132,14 @@ export default function CombinationMethodProfileEditor({
|
|||
setRawDraft(p.ok ? JSON.stringify(p.obj, null, 2) : String(methodProfileJson || ''))
|
||||
}
|
||||
|
||||
const showExpertSection = allowExpertJson && !plannerMode
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
{presetHint ? (
|
||||
<p style={{ fontSize: '12px', color: 'var(--danger)', margin: '0 0 8px', lineHeight: 1.4 }}>{presetHint}</p>
|
||||
) : null}
|
||||
|
||||
{arch ? (
|
||||
<p style={{ fontSize: '12px', color: 'var(--text2)', lineHeight: 1.48, margin: '0 0 12px' }}>
|
||||
<strong style={{ color: 'var(--text1)' }}>
|
||||
|
|
@ -75,7 +150,8 @@ export default function CombinationMethodProfileEditor({
|
|||
</p>
|
||||
) : (
|
||||
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: '0 0 12px' }}>
|
||||
Wähle einen Archetyp, um das Ablaufprofil strukturiert zu erfassen — oder nur das JSON weiter unten.
|
||||
Wähle einen Methoden‑Archetyp — besonders beim <strong>freien Methodenblock</strong> stehen alle
|
||||
typischen Stations‑Zeiten zur Verfügung. Ohne Archetyp keine geführten Eingaben.
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
|
@ -85,6 +161,48 @@ export default function CombinationMethodProfileEditor({
|
|||
|
||||
{fields && fields.length > 0 ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '12px' }}>
|
||||
{arch === 'circuit_rotate_time' ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '6px',
|
||||
marginBottom: '4px',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '11px', color: 'var(--text3)', marginRight: '6px' }}>Schnellwahl:</span>
|
||||
<button type="button" className="btn btn-secondary framework-ctrl framework-ctrl--xs" onClick={() => runCircuitPreset('transition_equals_work')}>
|
||||
Wechsel ≈ Arbeit
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary framework-ctrl framework-ctrl--xs" onClick={() => runCircuitPreset('round_rest_equals_work')}>
|
||||
Runden‑Pause ≈ Arbeit
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary framework-ctrl framework-ctrl--xs" onClick={() => runCircuitPreset('round_rest_two_thirds_work')}>
|
||||
Runden‑Pause ≈ ⅔ Arbeit
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{arch === 'time_domain_interval' ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '6px',
|
||||
marginBottom: '4px',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '11px', color: 'var(--text3)', marginRight: '6px' }}>Schnellwahl:</span>
|
||||
<button type="button" className="btn btn-secondary framework-ctrl framework-ctrl--xs" onClick={() => runIntervalPreset('rest_equals_work')}>
|
||||
Erholung = Belastung
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary framework-ctrl framework-ctrl--xs" onClick={() => runIntervalPreset('rest_two_thirds_work')}>
|
||||
Erholung ≈ ⅔ Belastung
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{fields.map((def) => {
|
||||
if (def.kind === 'bool') {
|
||||
const ck = !!profileObj[def.key]
|
||||
|
|
@ -125,26 +243,142 @@ export default function CombinationMethodProfileEditor({
|
|||
})}
|
||||
</div>
|
||||
) : arch && fields && fields.length === 0 ? (
|
||||
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: '0 0 10px' }}>
|
||||
Für diesen Archetyp gibt es keine vorgegebenen Profilfelder — nutze die Freitexte der Kombination oder Roh‑JSON bei Bedarf.
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: '0 0 10px', lineHeight: 1.45 }}>
|
||||
Dieser Archetyp ist für <strong>maximal flexible</strong> Stationsblöcke gedacht — die Zeit‑Eckdaten sind
|
||||
unten je Station möglich. Freitexte der Kombination beschreiben alles Organisatorische, was nicht in
|
||||
Sekunden gefasst wird.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showSlotTiming ? (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '6px',
|
||||
marginBottom: '14px',
|
||||
padding: '12px 14px',
|
||||
borderRadius: '10px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface)',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '0.88rem', fontWeight: 700, marginBottom: '10px', color: 'var(--text1)' }}>
|
||||
Pro Station / Slot (Zeiten in Sekunden)
|
||||
</div>
|
||||
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '0 0 12px', lineHeight: 1.45 }}>
|
||||
Belastungsdauer, wie oft die Übung an der gleichen Station hintereinander, kurze Pause dazwischen, Zeit bis
|
||||
zur nächsten Station. Felder können leer bleiben — z. B. nutzt der Zirkel oben erst die globalen Arbeit‑/
|
||||
Rotations‑Sekunden.
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{outlineSorted.map((slot) => {
|
||||
const siRaw = slot.slot_index
|
||||
const si =
|
||||
siRaw === '' || siRaw == null ? null : typeof siRaw === 'number' ? siRaw : parseInt(String(siRaw), 10)
|
||||
if (!Number.isFinite(si)) return null
|
||||
const row = lookupSlotTiming(si)
|
||||
const ttl = ((slot.title || '').trim() || `Station ${si}`).trim()
|
||||
return (
|
||||
<div
|
||||
key={`slot-timing-${si}`}
|
||||
style={{
|
||||
padding: '10px',
|
||||
borderRadius: '10px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface2)',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: '10px', fontWeight: 700, color: 'var(--text1)', fontSize: '0.9rem' }}>
|
||||
Station {si}
|
||||
<span style={{ fontWeight: 400, marginLeft: 8, color: 'var(--text2)', fontSize: '0.86rem' }}>{ttl}</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
|
||||
gap: '10px',
|
||||
alignItems: 'end',
|
||||
}}
|
||||
>
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label" style={{ fontSize: '11px' }}>
|
||||
Belastung (s)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
className="form-input"
|
||||
placeholder="–"
|
||||
value={row.load_sec != null ? String(row.load_sec) : ''}
|
||||
onChange={(e) => onSlotField(si, 'load_sec', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label" style={{ fontSize: '11px' }}>
|
||||
Wdh. ohne Wechsel
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
className="form-input"
|
||||
placeholder="oft 1"
|
||||
value={row.consecutive_reps != null ? String(row.consecutive_reps) : ''}
|
||||
onChange={(e) => onSlotField(si, 'consecutive_reps', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<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)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
className="form-input"
|
||||
placeholder="–"
|
||||
value={row.transition_after_sec != null ? String(row.transition_after_sec) : ''}
|
||||
onChange={(e) => onSlotField(si, 'transition_after_sec', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showExpertSection ? (
|
||||
<details
|
||||
style={{
|
||||
marginTop: '4px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--border)',
|
||||
padding: '8px 10px',
|
||||
background: 'var(--surface)',
|
||||
background: 'var(--surface2)',
|
||||
}}
|
||||
onToggle={(ev) => {
|
||||
if (ev.target.open) openAdvanced()
|
||||
}}
|
||||
>
|
||||
<summary style={{ cursor: 'pointer', fontSize: '0.82rem', fontWeight: 600 }}>Erweitert: JSON direkt bearbeiten</summary>
|
||||
<summary style={{ cursor: 'pointer', fontSize: '0.82rem', fontWeight: 600 }}>
|
||||
Support / Entwicklung: Rohdaten (JSON)
|
||||
</summary>
|
||||
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '8px 0 6px', lineHeight: 1.4 }}>
|
||||
Zusätzliche Schlüssel (Piloten). Geführte Felder können dieselben Schlüssel beim nächsten Speichern überlagern.
|
||||
Für Migrationen und Sonderfälle. Geführte Felder setzen weiterhin gültige Standardschlüssel.
|
||||
</p>
|
||||
<textarea
|
||||
className="form-input"
|
||||
|
|
@ -168,8 +402,11 @@ export default function CombinationMethodProfileEditor({
|
|||
onChangeMethodProfileJson(res.json)
|
||||
}}
|
||||
/>
|
||||
{rawOpenError ? <p style={{ color: 'var(--danger)', fontSize: '12px', marginTop: '6px' }}>{rawOpenError}</p> : null}
|
||||
{rawOpenError ? (
|
||||
<p style={{ color: 'var(--danger)', fontSize: '12px', marginTop: '6px' }}>{rawOpenError}</p>
|
||||
) : null}
|
||||
</details>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1066,6 +1066,7 @@ export default function TrainingUnitSectionsEditor({
|
|||
</button>
|
||||
</div>
|
||||
<CombinationMethodProfileEditor
|
||||
plannerMode
|
||||
methodArchetype={(it.catalog_method_archetype || '').trim()}
|
||||
methodProfileJson={comboPlanningProfileJsonForEditor(
|
||||
it.catalog_method_profile || {},
|
||||
|
|
|
|||
|
|
@ -1031,19 +1031,10 @@ function ExerciseFormPage() {
|
|||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Ablaufprofil (über Archetyp)</label>
|
||||
<CombinationMethodProfileEditor
|
||||
methodArchetype={formData.method_archetype || ''}
|
||||
methodProfileJson={formData.method_profile_json || '{}'}
|
||||
onChangeMethodProfileJson={(s) => updateFormField('method_profile_json', s)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ paddingTop: '4px', borderTop: '1px dashed var(--border)', marginBottom: '10px' }}>
|
||||
<strong style={{ fontSize: '14px' }}>Stationen</strong>
|
||||
<p style={{ fontSize: '12px', color: 'var(--text2)', margin: '6px 0 10px' }}>
|
||||
Index (Reihenfolge), optional Stationstitel und kommaseparierte IDs von{' '}
|
||||
<strong>Einzelübungen</strong> im Pool (nur Übungen mit Art „Einzelübung“).
|
||||
<p style={{ fontSize: '12px', color: 'var(--text2)', margin: '6px 0 10px', lineHeight: 1.45 }}>
|
||||
Zuerst die Stationen anlegen — dann erscheinen die Zeiten darunter auch <strong>pro Slot</strong> im Ablaufprofil.
|
||||
</p>
|
||||
{(formData.combination_slots || []).map((row, idx) => (
|
||||
<div
|
||||
|
|
@ -1146,6 +1137,15 @@ function ExerciseFormPage() {
|
|||
+ Station
|
||||
</button>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Ablaufprofil (Zeiten & Runden)</label>
|
||||
<CombinationMethodProfileEditor
|
||||
methodArchetype={formData.method_archetype || ''}
|
||||
methodProfileJson={formData.method_profile_json || '{}'}
|
||||
onChangeMethodProfileJson={(s) => updateFormField('method_profile_json', s)}
|
||||
comboSlotsOutline={formData.combination_slots || []}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -162,4 +162,149 @@ export function setFullProfileRawJson(rawEditable) {
|
|||
return { ok: true, obj: parsed.obj, json: j === '{}' ? '{}' : j }
|
||||
}
|
||||
|
||||
/**
|
||||
* Pfad für slot_profiles_v1 und ähnliche strukturierte Erweiterungen.
|
||||
* Ungültiges JSON gibt { ok:false } zurück; mutator erhält geklontes Profil‑Objekt.
|
||||
*/
|
||||
export function patchMethodProfile(rawJson, mutator) {
|
||||
const parsed = parseProfileJson(rawJson || '{}')
|
||||
if (!parsed.ok) return parsed
|
||||
const draft = { ...parsed.obj }
|
||||
mutator(draft)
|
||||
try {
|
||||
const j = JSON.stringify(draft)
|
||||
return { ok: true, obj: draft, json: j === '{}' ? '{}' : j }
|
||||
} catch {
|
||||
return { ok: false, error: 'Ablaufprofil konnte nicht gespeichert werden.' }
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalisiert slot_profiles_v1 aus dem gespeicherten Profil */
|
||||
export function readSlotProfilesV1(profileObj) {
|
||||
if (!profileObj || typeof profileObj !== 'object') return []
|
||||
const raw = profileObj.slot_profiles_v1
|
||||
if (!Array.isArray(raw)) return []
|
||||
return raw.map((row) => {
|
||||
if (!row || typeof row !== 'object') return null
|
||||
const si = Number(row.slot_index)
|
||||
return {
|
||||
slot_index: Number.isFinite(si) ? si : 0,
|
||||
load_sec: normalizeOptionalNonNegInt(row.load_sec),
|
||||
consecutive_reps: normalizeOptionalPositiveInt(row.consecutive_reps),
|
||||
intra_rep_rest_sec: normalizeOptionalNonNegInt(row.intra_rep_rest_sec),
|
||||
transition_after_sec: normalizeOptionalNonNegInt(row.transition_after_sec),
|
||||
}
|
||||
}).filter(Boolean)
|
||||
}
|
||||
|
||||
function normalizeOptionalNonNegInt(v) {
|
||||
if (v === '' || v === undefined || v === null) return undefined
|
||||
const n = typeof v === 'number' ? v : parseInt(String(v), 10)
|
||||
if (!Number.isFinite(n) || n < 0) return undefined
|
||||
return Math.round(n)
|
||||
}
|
||||
|
||||
function normalizeOptionalPositiveInt(v) {
|
||||
const n = normalizeOptionalNonNegInt(v)
|
||||
if (n === undefined) return undefined
|
||||
if (n < 1) return undefined
|
||||
return n
|
||||
}
|
||||
|
||||
const SLOT_TIMING_FIELDS = /** @type {const} */ ([
|
||||
'load_sec',
|
||||
'consecutive_reps',
|
||||
'intra_rep_rest_sec',
|
||||
'transition_after_sec',
|
||||
])
|
||||
|
||||
/** '', null = Feld entfernen; sonst gültige Zahl setzen */
|
||||
export function patchSlotTimingField(profileDraft, slotIndex, field, rawInput) {
|
||||
if (!SLOT_TIMING_FIELDS.includes(field)) return
|
||||
const ix =
|
||||
typeof slotIndex === 'number' && Number.isFinite(slotIndex)
|
||||
? slotIndex
|
||||
: parseInt(String(slotIndex), 10)
|
||||
if (!Number.isFinite(ix)) return
|
||||
|
||||
let arr = Array.isArray(profileDraft.slot_profiles_v1) ? [...profileDraft.slot_profiles_v1] : []
|
||||
let found = arr.findIndex((r) => r && typeof r === 'object' && Number(r.slot_index) === ix)
|
||||
|
||||
const nextRow = {}
|
||||
if (found >= 0 && arr[found] && typeof arr[found] === 'object') {
|
||||
Object.assign(nextRow, arr[found])
|
||||
}
|
||||
nextRow.slot_index = ix
|
||||
|
||||
if (rawInput === null || rawInput === undefined || String(rawInput).trim() === '') {
|
||||
delete nextRow[field]
|
||||
} else if (field === 'consecutive_reps') {
|
||||
const n = normalizeOptionalPositiveInt(rawInput)
|
||||
if (n === undefined) delete nextRow[field]
|
||||
else nextRow[field] = n
|
||||
} else {
|
||||
const n = normalizeOptionalNonNegInt(rawInput)
|
||||
if (n === undefined) delete nextRow[field]
|
||||
else nextRow[field] = n
|
||||
}
|
||||
|
||||
const hasTiming = SLOT_TIMING_FIELDS.some((k) => nextRow[k] !== undefined && nextRow[k] !== null)
|
||||
|
||||
if (found >= 0) {
|
||||
if (!hasTiming) {
|
||||
arr = arr.filter((_, i) => i !== found)
|
||||
} else {
|
||||
arr[found] = nextRow
|
||||
}
|
||||
} else if (hasTiming) {
|
||||
arr.push(nextRow)
|
||||
}
|
||||
|
||||
arr.sort((a, b) => Number(a.slot_index) - Number(b.slot_index))
|
||||
|
||||
if (arr.length === 0) delete profileDraft.slot_profiles_v1
|
||||
else profileDraft.slot_profiles_v1 = arr
|
||||
}
|
||||
|
||||
/** Rotierender Zirkel: typische Ableitungen (setzt Sekunden konkret). */
|
||||
export function applyCircuitRotateQuickRatio(profileDraft, preset) {
|
||||
const wRaw = profileDraft.work_seconds
|
||||
const work =
|
||||
typeof wRaw === 'number' && Number.isFinite(wRaw) ? Math.round(wRaw) : parseInt(String(wRaw), 10)
|
||||
if (!Number.isFinite(work) || work <= 0)
|
||||
return { ok: false, error: 'Zuerst Arbeitszeit pro Station (Sek.) setzen.' }
|
||||
profileDraft.timing_schema = profileDraft.timing_schema ?? 1
|
||||
if (preset === 'transition_equals_work') {
|
||||
profileDraft.transition_seconds = work
|
||||
return { ok: true }
|
||||
}
|
||||
if (preset === 'round_rest_equals_work') {
|
||||
profileDraft.rest_seconds = work
|
||||
return { ok: true }
|
||||
}
|
||||
if (preset === 'round_rest_two_thirds_work') {
|
||||
profileDraft.rest_seconds = Math.round((work * 2) / 3)
|
||||
return { ok: true }
|
||||
}
|
||||
return { ok: false, error: 'Unbekannte Schnellwahl.' }
|
||||
}
|
||||
|
||||
export function applyIntervalDomainQuickRatio(profileDraft, preset) {
|
||||
const wRaw = profileDraft.work_seconds
|
||||
const work =
|
||||
typeof wRaw === 'number' && Number.isFinite(wRaw) ? Math.round(wRaw) : parseInt(String(wRaw), 10)
|
||||
if (!Number.isFinite(work) || work <= 0)
|
||||
return { ok: false, error: 'Zuerst Belastungszeit Intervall (Sek.) setzen.' }
|
||||
profileDraft.timing_schema = profileDraft.timing_schema ?? 1
|
||||
if (preset === 'rest_equals_work') {
|
||||
profileDraft.rest_seconds = work
|
||||
return { ok: true }
|
||||
}
|
||||
if (preset === 'rest_two_thirds_work') {
|
||||
profileDraft.rest_seconds = Math.round((work * 2) / 3)
|
||||
return { ok: true }
|
||||
}
|
||||
return { ok: false, error: 'Unbekannte Schnellwahl.' }
|
||||
}
|
||||
|
||||
export { parseProfileJson, INT_MAX }
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user