Files
boehmitools-training/plugins/trainingsplan/static/index.html
T
2026-09-01 16:16:57 +02:00

667 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Trainingsplan-Editor</title>
<link rel="stylesheet" href="/shared/boehmi.css">
<script src="/shared/boehmi.js"></script>
<style>
.tp-top { display: grid; grid-template-columns: minmax(260px, 1fr) auto; gap: 12px; align-items: end; }
.tp-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.tp-actions button, .tp-actions select { width: auto; }
.tp-layout { display: grid; grid-template-columns: 320px minmax(0, 1fr); gap: 14px; align-items: start; }
.tp-side { position: sticky; top: calc(var(--bt-appbar-h) + 12px); }
.tp-statline { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
.tp-day { border: 1px solid var(--bt-line); border-radius: var(--bt-r-lg); background: var(--bt-surface); margin-bottom: 14px; box-shadow: var(--bt-shadow); overflow: hidden; }
.tp-day-head { display: grid; grid-template-columns: 86px minmax(180px, 1fr) auto; gap: 10px; align-items: end; padding: 14px 16px; background: var(--bt-surface-2); border-bottom: 1px solid var(--bt-line); }
.tp-day-body { padding: 14px 16px 16px; }
.tp-day-actions { display: flex; gap: 6px; justify-content: flex-end; align-items: center; flex-wrap: wrap; }
.tp-textareas { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; }
.tp-exercise-row { display: grid; grid-template-columns: minmax(220px, 1.2fr) minmax(180px, 1fr) auto; gap: 8px; align-items: end; padding: 9px 0; border-bottom: 1px solid var(--bt-line-soft); }
.tp-exercise-row:last-child { border-bottom: none; }
.tp-exercise-actions { display: flex; gap: 5px; justify-content: flex-end; }
.tp-library { display: grid; gap: 12px; }
.tp-library-row { border-bottom: 1px solid var(--bt-line); padding: 14px 16px; }
.tp-library-row:last-child { border-bottom: none; }
.tp-library-main { display: grid; grid-template-columns: minmax(220px, 1fr) minmax(220px, 1fr) auto; gap: 8px; align-items: end; }
.tp-progressions { display: grid; gap: 8px; margin-top: 10px; }
.tp-progression-row { display: grid; grid-template-columns: 34px minmax(180px, 1fr) auto; gap: 8px; align-items: center; }
.tp-progression-number { color: var(--bt-muted); font-size: 12px; text-align: center; }
.tp-addline, .tp-formline { display: grid; grid-template-columns: minmax(220px, 1fr) auto; gap: 8px; align-items: end; margin-top: 12px; }
.tp-table-input { width: 100%; min-width: 0; }
.tp-code { min-height: 520px; font-family: var(--bt-mono); font-size: 12.5px; }
.tp-warn { color: var(--bt-warn); }
@media (max-width: 980px) {
.tp-top, .tp-layout, .tp-day-head, .tp-textareas, .tp-addline, .tp-formline { grid-template-columns: 1fr; }
.tp-side { position: static; }
.tp-actions, .tp-day-actions { justify-content: flex-start; }
.tp-exercise-row, .tp-library-main, .tp-progression-row { grid-template-columns: 1fr; }
.tp-exercise-actions { justify-content: flex-start; }
.tp-progression-number { text-align: left; }
}
</style>
</head>
<body data-bt-title="Trainingsplan-Editor" data-bt-icon="🏋">
<main class="bt-main wide">
<div class="bt-pagehead">
<div class="tp-top">
<div>
<h1>Trainingsplan-Editor</h1>
<p id="headline">Pläne, Tage, Warm-up, Cool-down und Übungen pflegen.</p>
</div>
<div class="tp-actions">
<select id="planSelect" aria-label="Plan auswählen"></select>
<button id="btnNew" class="ghost">Neu</button>
<button id="btnRename" class="ghost">Umbenennen</button>
<button id="btnDelete" class="danger">Löschen</button>
<button id="btnSave">Speichern</button>
<button id="btnPublish" class="primary">Veröffentlichen</button>
</div>
</div>
</div>
<div id="warn" class="bt-notice err bt-hidden"></div>
<div class="bt-tabs" id="tabs">
<button class="bt-tab active" data-tab="tage">Tage</button>
<button class="bt-tab" data-tab="uebungen">Übungen</button>
<button class="bt-tab" data-tab="json">JSON</button>
</div>
<div class="tp-layout" style="margin-top:14px">
<aside class="tp-side">
<div class="bt-card">
<h2>Plan</h2>
<label for="metaTitle">Titel</label>
<input id="metaTitle" type="text">
<label for="metaSubtitle">Untertitel</label>
<input id="metaSubtitle" type="text">
<div class="tp-statline" id="statusPills"></div>
</div>
</aside>
<section id="tab-tage" class="tab"></section>
<section id="tab-uebungen" class="tab bt-hidden"></section>
<section id="tab-json" class="tab bt-hidden"></section>
</div>
</main>
<script>
"use strict";
const $ = (id) => document.getElementById(id);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
const api = (path) => window.BT ? BT.api(path) : fetch(path).then((r) => r.json());
const esc = (value) => window.BT ? BT.escape(value) : String(value ?? "").replace(/[&<>"']/g, (ch) => ({"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"}[ch]));
const uid = (prefix) => `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
let state = {
plans: [],
activePlanId: "",
wrapper: null,
cfg: null,
tab: "tage",
dirty: false,
};
function toast(message, kind = "") {
if (window.BT) BT.toast(message, kind);
else alert(message);
}
function showError(message) {
const warn = $("warn");
warn.textContent = message;
warn.classList.remove("bt-hidden");
}
function clearError() {
$("warn").classList.add("bt-hidden");
}
function markDirty() {
state.dirty = true;
renderStatus();
}
function textItems(obj, key) {
return (obj?.[key]?.items || []).join("\n");
}
function setTextItems(obj, key, text) {
obj[key] = {items: String(text || "").split("\n").map((item) => item.trim()).filter(Boolean)};
markDirty();
}
function exerciseName(id) {
return (state.cfg?.exercises || []).find((exercise) => exercise.id === id)?.name || id || "Übung";
}
function progressionOptions(exercise, selected = "") {
return (exercise.progressions || []).map((progression) =>
`<option value="${esc(progression.id)}"${progression.id === selected ? " selected" : ""}>${esc(progression.name || progression.id)}</option>`
).join("");
}
function dayProgressionSelect(exerciseId) {
const exercise = (state.cfg?.exercises || []).find((item) => item.id === exerciseId);
const progressions = exercise?.progressions || [];
if (!progressions.length) {
return `<select data-day-current-progression disabled><option value="">Keine Progression</option></select>`;
}
const selected = exercise.current_progression_id || progressions[0].id;
return `<select data-day-current-progression>${progressionOptions(exercise, selected)}</select>`;
}
function optionList(selected = "") {
return (state.cfg?.exercises || []).map((exercise) =>
`<option value="${esc(exercise.id)}"${exercise.id === selected ? " selected" : ""}>${esc(exercise.name || exercise.id)}</option>`
).join("");
}
async function loadPlans(selectId = "") {
clearError();
const data = await api("/api/plans");
state.plans = data.plans || [];
state.activePlanId = selectId || data.active || state.plans[0]?.id || "";
renderPlanSelect();
if (state.activePlanId) await loadPlan(state.activePlanId);
else {
state.wrapper = null;
state.cfg = null;
renderAll();
}
}
async function loadPlan(pid) {
clearError();
state.wrapper = await api(`/api/plans/${encodeURIComponent(pid)}`);
state.activePlanId = pid;
state.cfg = state.wrapper.config || {meta: {}, exercises: [], days: []};
state.cfg.exercises = state.cfg.exercises || [];
state.cfg.days = state.cfg.days || [];
state.dirty = false;
renderAll();
}
function renderPlanSelect() {
$("planSelect").innerHTML = state.plans.map((plan) =>
`<option value="${esc(plan.id)}"${plan.id === state.activePlanId ? " selected" : ""}>${esc(plan.name)}</option>`
).join("");
}
function renderStatus() {
const cfg = state.cfg || {days: [], exercises: []};
const wrapper = state.wrapper || {};
$("headline").textContent = state.activePlanId
? `${state.activePlanId} · Bearbeitung R${wrapper.revision || 1} · veröffentlicht R${wrapper.published_revision || 1}`
: "Kein Trainingsplan gefunden.";
$("statusPills").innerHTML = [
`<span class="bt-badge">Schema ${cfg.schema_version || 5}/${cfg.contract_version || 4}</span>`,
`<span class="bt-badge">${(cfg.days || []).length} Tage</span>`,
`<span class="bt-badge">${(cfg.exercises || []).length} Übungen</span>`,
state.dirty || wrapper.has_unpublished_changes
? `<span class="bt-badge warn">Ungespeichert</span>`
: `<span class="bt-badge ok">Aktuell</span>`,
].join("");
}
function renderAll() {
if (!state.cfg) {
$("metaTitle").value = "";
$("metaSubtitle").value = "";
$("tab-tage").innerHTML = `<div class="bt-empty">Kein Plan vorhanden.</div>`;
$("tab-uebungen").innerHTML = "";
$("tab-json").innerHTML = "";
renderStatus();
return;
}
$("metaTitle").value = state.cfg.meta?.title || "";
$("metaSubtitle").value = state.cfg.meta?.subtitle || "";
renderTabs();
renderStatus();
renderDays();
renderExercises();
renderJson();
}
function renderTabs() {
$$("#tabs button").forEach((button) => button.classList.toggle("active", button.dataset.tab === state.tab));
$$(".tp-layout > .tab").forEach((section) => section.classList.add("bt-hidden"));
$(`tab-${state.tab}`).classList.remove("bt-hidden");
}
function renderDays() {
const days = state.cfg.days || [];
$("tab-tage").innerHTML = `
<div class="bt-card">
<div class="bt-row" style="align-items:center">
<div style="flex:1">
<h2 style="margin:0">Trainingstage</h2>
<p class="bt-hint" style="margin:4px 0 0">Warm-up und Cool-down bleiben am Tag, Übungen werden aus der Bibliothek hinterlegt.</p>
</div>
<button class="primary" data-add-day>Tag hinzufügen</button>
</div>
</div>
${days.length ? days.map((day, index) => dayHtml(day, index)).join("") : `<div class="bt-empty">Noch keine Trainingstage.</div>`}
`;
}
function dayHtml(day, index) {
const items = (day.exercises || []).map((item, itemIndex) => `
<div class="tp-exercise-row" data-day-item="${esc(item.id)}">
<label>Übung<select data-day-field="exercise_id">${optionList(item.exercise_id)}</select></label>
<label>Aktuell${dayProgressionSelect(item.exercise_id)}</label>
<div class="tp-exercise-actions">
<button class="mini ghost" data-move-day-item="-1" title="Nach oben">↑</button>
<button class="mini ghost" data-move-day-item="1" title="Nach unten">↓</button>
<button class="mini danger" data-remove-day-item title="Entfernen">×</button>
</div>
</div>
`).join("");
return `
<article class="tp-day" data-day-id="${esc(day.id)}">
<div class="tp-day-head">
<label>Nr.<input type="number" min="1" data-day-prop="num" value="${esc(day.num || index + 1)}"></label>
<label>Name<input type="text" data-day-prop="name" value="${esc(day.name || day.focus || `Tag ${index + 1}`)}"></label>
<div class="tp-day-actions">
<button class="mini ghost" data-move-day="-1">↑</button>
<button class="mini ghost" data-move-day="1">↓</button>
<button class="mini danger" data-remove-day>Tag löschen</button>
</div>
</div>
<div class="tp-day-body">
<div class="tp-textareas">
<label>Warm-up<textarea data-day-text="warmup">${esc(textItems(day, "warmup"))}</textarea></label>
<label>Cool-down<textarea data-day-text="cooldown">${esc(textItems(day, "cooldown"))}</textarea></label>
</div>
<h3>Übungen</h3>
${items || `<div class="bt-empty">Keine Übungen an diesem Tag.</div>`}
<div class="tp-addline">
<label>Übung hinzufügen<select data-new-day-exercise>${optionList()}</select></label>
<button data-add-day-exercise>Hinzufügen</button>
</div>
</div>
</article>
`;
}
function renderExercises() {
const rows = (state.cfg.exercises || []).map((exercise) => `
<div class="tp-library-row" data-exercise-id="${esc(exercise.id)}">
<div class="tp-library-main">
<label>Name<input class="tp-table-input" data-exercise-field="name" value="${esc(exercise.name || "")}"></label>
<label>Aktuelle Progression<select data-exercise-field="current_progression_id">
<option value="">Keine Progression</option>
${progressionOptions(exercise, exercise.current_progression_id)}
</select></label>
<div class="tp-exercise-actions">
<button class="mini ghost" data-add-progression>Progression</button>
<button class="mini danger" data-remove-exercise>×</button>
</div>
</div>
<div class="tp-progressions">
${(exercise.progressions || []).map((progression, index) => `
<div class="tp-progression-row" data-progression-id="${esc(progression.id)}">
<div class="tp-progression-number">${index + 1}</div>
<label>Progression<input class="tp-table-input" data-progression-field="name" value="${esc(progression.name || "")}"></label>
<div class="tp-exercise-actions">
<button class="mini ghost" data-move-progression="-1" title="Nach oben">↑</button>
<button class="mini ghost" data-move-progression="1" title="Nach unten">↓</button>
<button class="mini danger" data-remove-progression title="Entfernen">×</button>
</div>
</div>
`).join("") || `<div class="bt-empty">Keine Progressionen hinterlegt.</div>`}
</div>
<div class="bt-mono bt-muted" style="margin-top:8px">${esc(exercise.id)}</div>
</div>
`).join("");
$("tab-uebungen").innerHTML = `
<div class="bt-card">
<h2>Übung anlegen</h2>
<div class="tp-formline">
<label>Name<input id="newExerciseName" type="text"></label>
<button class="primary" data-add-exercise>Hinzufügen</button>
</div>
</div>
<div class="bt-card bt-card-flush">
${rows ? `<div class="tp-library">${rows}</div>` : `<div class="bt-empty" style="margin:16px">Noch keine Übungen.</div>`}
</div>
`;
}
function renderJson() {
$("tab-json").innerHTML = `
<div class="bt-card">
<div class="bt-row" style="align-items:center">
<div style="flex:1">
<h2 style="margin:0">JSON</h2>
<p class="bt-hint" style="margin:4px 0 0">Direktbearbeitung des aktuellen Entwurfs.</p>
</div>
<button data-apply-json>Übernehmen</button>
</div>
<textarea id="jsonEditor" class="tp-code">${esc(JSON.stringify(state.cfg, null, 2))}</textarea>
</div>
`;
}
function addExerciseFromForm() {
const name = $("newExerciseName")?.value.trim();
if (!name) return toast("Name fehlt.", "err");
state.cfg.exercises.push({
id: uid("exercise"),
name,
progressions: [],
current_progression_id: "",
});
markDirty();
renderAll();
}
function addDay() {
const next = Math.max(0, ...(state.cfg.days || []).map((day) => Number(day.num || 0))) + 1;
state.cfg.days.push({id: uid("day"), num: next, name: `Tag ${next}`, warmup: {items: []}, cooldown: {items: []}, exercises: []});
markDirty();
renderAll();
}
function move(list, index, delta) {
const next = index + delta;
if (!list || index < 0 || next < 0 || next >= list.length) return;
const [item] = list.splice(index, 1);
list.splice(next, 0, item);
markDirty();
renderAll();
}
function syncFormStateFromDom() {
if (!state.cfg) return;
state.cfg.meta = state.cfg.meta || {};
state.cfg.meta.title = $("metaTitle").value;
if ($("metaSubtitle").value.trim()) state.cfg.meta.subtitle = $("metaSubtitle").value;
else delete state.cfg.meta.subtitle;
document.querySelectorAll("[data-exercise-id]").forEach((row) => {
const exercise = state.cfg.exercises.find((item) => item.id === row.dataset.exerciseId);
if (!exercise) return;
row.querySelectorAll("[data-exercise-field]").forEach((input) => {
const key = input.dataset.exerciseField;
if (key === "name") exercise.name = input.value;
else if (input.value.trim()) exercise[key] = input.value;
else delete exercise[key];
});
row.querySelectorAll("[data-progression-id]").forEach((progressionRow) => {
const progression = (exercise.progressions || []).find((item) => item.id === progressionRow.dataset.progressionId);
const input = progressionRow.querySelector('[data-progression-field="name"]');
if (progression && input) progression.name = input.value;
});
});
document.querySelectorAll("[data-day-id]").forEach((shell) => {
const day = state.cfg.days.find((item) => item.id === shell.dataset.dayId);
if (!day) return;
const num = shell.querySelector('[data-day-prop="num"]');
const name = shell.querySelector('[data-day-prop="name"]');
const warmup = shell.querySelector('[data-day-text="warmup"]');
const cooldown = shell.querySelector('[data-day-text="cooldown"]');
if (num) day.num = Number(num.value || 1);
if (name) {
day.name = name.value;
delete day.focus;
}
if (warmup) day.warmup = {items: warmup.value.split("\n").map((item) => item.trim()).filter(Boolean)};
if (cooldown) day.cooldown = {items: cooldown.value.split("\n").map((item) => item.trim()).filter(Boolean)};
shell.querySelectorAll("[data-day-item]").forEach((row) => {
const item = (day.exercises || []).find((entry) => entry.id === row.dataset.dayItem);
if (!item) return;
const exercise = row.querySelector('[data-day-field="exercise_id"]');
if (exercise) item.exercise_id = exercise.value;
const current = row.querySelector("[data-day-current-progression]");
const selectedExercise = state.cfg.exercises.find((entry) => entry.id === item.exercise_id);
if (selectedExercise && current && current.value) {
selectedExercise.current_progression_id = current.value;
}
});
});
}
async function savePlan() {
if (!state.activePlanId || !state.cfg) return;
syncFormStateFromDom();
const saved = await api(`/api/plans/${encodeURIComponent(state.activePlanId)}`, {
method: "POST",
body: JSON.stringify({expected_revision: state.wrapper.revision, config: state.cfg}),
});
state.wrapper = saved;
state.cfg = saved.config;
state.dirty = false;
renderPlanSelect();
renderAll();
const errors = saved.validation?.errors?.length || 0;
toast(errors ? `${errors} Fehler beim Speichern.` : "Gespeichert.", errors ? "err" : "ok");
}
async function publishPlan() {
if (!state.activePlanId) return;
syncFormStateFromDom();
if (state.dirty) await savePlan();
const saved = await api(`/api/plans/${encodeURIComponent(state.activePlanId)}/publish`, {
method: "POST",
body: JSON.stringify({expected_revision: state.wrapper.revision}),
});
state.wrapper = saved;
state.cfg = saved.config;
state.dirty = false;
await loadPlans(state.activePlanId);
toast("Veröffentlicht.", "ok");
}
$("tabs").addEventListener("click", (event) => {
const button = event.target.closest("button[data-tab]");
if (!button) return;
state.tab = button.dataset.tab;
renderTabs();
if (state.tab === "json") renderJson();
});
function handleFormEdit(event) {
const target = event.target;
if (!state.cfg) return;
if (target.id === "metaTitle") {
state.cfg.meta = state.cfg.meta || {};
state.cfg.meta.title = target.value;
markDirty();
} else if (target.id === "metaSubtitle") {
state.cfg.meta = state.cfg.meta || {};
if (target.value.trim()) state.cfg.meta.subtitle = target.value;
else delete state.cfg.meta.subtitle;
markDirty();
}
const exerciseRow = target.closest("[data-exercise-id]");
if (exerciseRow && target.dataset.exerciseField) {
const exercise = state.cfg.exercises.find((item) => item.id === exerciseRow.dataset.exerciseId);
if (!exercise) return;
const key = target.dataset.exerciseField;
if (target.value.trim() || key === "name") exercise[key] = target.value;
else delete exercise[key];
markDirty();
}
const progressionRow = target.closest("[data-progression-id]");
if (exerciseRow && progressionRow && target.dataset.progressionField) {
const exercise = state.cfg.exercises.find((item) => item.id === exerciseRow.dataset.exerciseId);
const progression = (exercise?.progressions || []).find((item) => item.id === progressionRow.dataset.progressionId);
if (!progression) return;
progression[target.dataset.progressionField] = target.value;
markDirty();
}
const dayShell = target.closest("[data-day-id]");
if (dayShell) {
const day = state.cfg.days.find((item) => item.id === dayShell.dataset.dayId);
if (!day) return;
const itemRow = target.closest("[data-day-item]");
if (target.dataset.dayProp) {
day[target.dataset.dayProp] = target.dataset.dayProp === "num" ? Number(target.value || 1) : target.value;
markDirty();
} else if (target.dataset.dayText) {
setTextItems(day, target.dataset.dayText, target.value);
} else if (itemRow && target.dataset.dayField) {
const item = (day.exercises || []).find((entry) => entry.id === itemRow.dataset.dayItem);
if (!item) return;
const key = target.dataset.dayField;
if (target.value.trim()) item[key] = target.value;
else delete item[key];
markDirty();
if (key === "exercise_id") renderAll();
} else if (itemRow && target.dataset.dayCurrentProgression !== undefined) {
const item = (day.exercises || []).find((entry) => entry.id === itemRow.dataset.dayItem);
const exercise = state.cfg.exercises.find((entry) => entry.id === item?.exercise_id);
if (!exercise) return;
exercise.current_progression_id = target.value;
markDirty();
renderAll();
}
}
}
document.addEventListener("input", handleFormEdit);
document.addEventListener("change", handleFormEdit);
document.addEventListener("click", async (event) => {
const button = event.target.closest("button");
if (!button) return;
if (!state.cfg && button.id !== "btnNew") return;
try {
if (button.id === "btnNew") {
const name = prompt("Name des neuen Plans:", "Neuer Trainingsplan");
if (!name) return;
const created = await api("/api/plans", {method: "POST", body: JSON.stringify({name})});
await loadPlans(created.id);
} else if (button.id === "btnRename") {
const name = prompt("Neuer Name:", state.wrapper?.name || "");
if (!name) return;
await api(`/api/plans/${encodeURIComponent(state.activePlanId)}/rename`, {method: "POST", body: JSON.stringify({name})});
await loadPlans(state.activePlanId);
} else if (button.id === "btnDelete") {
if (!confirm("Plan löschen?")) return;
await api(`/api/plans/${encodeURIComponent(state.activePlanId)}`, {method: "DELETE"});
await loadPlans();
} else if (button.id === "btnSave") {
await savePlan();
} else if (button.id === "btnPublish") {
await publishPlan();
} else if (button.dataset.addDay !== undefined) {
addDay();
} else if (button.dataset.addExercise !== undefined) {
addExerciseFromForm();
} else if (button.dataset.applyJson !== undefined) {
state.cfg = JSON.parse($("jsonEditor").value);
state.cfg.exercises = state.cfg.exercises || [];
state.cfg.days = state.cfg.days || [];
markDirty();
renderAll();
}
const dayShell = button.closest("[data-day-id]");
if (dayShell) {
const dayIndex = state.cfg.days.findIndex((day) => day.id === dayShell.dataset.dayId);
const day = state.cfg.days[dayIndex];
if (!day) return;
if (button.dataset.moveDay) {
move(state.cfg.days, dayIndex, Number(button.dataset.moveDay));
} else if (button.dataset.removeDay !== undefined) {
if (!confirm("Tag löschen?")) return;
state.cfg.days.splice(dayIndex, 1);
markDirty();
renderAll();
} else if (button.dataset.addDayExercise !== undefined) {
const exerciseId = dayShell.querySelector("[data-new-day-exercise]")?.value;
if (!exerciseId) return toast("Keine Übung ausgewählt.", "err");
day.exercises = day.exercises || [];
day.exercises.push({id: uid("day-exercise"), exercise_id: exerciseId});
markDirty();
renderAll();
}
const itemRow = button.closest("[data-day-item]");
if (itemRow) {
const itemIndex = (day.exercises || []).findIndex((item) => item.id === itemRow.dataset.dayItem);
if (button.dataset.moveDayItem) {
move(day.exercises, itemIndex, Number(button.dataset.moveDayItem));
} else if (button.dataset.removeDayItem !== undefined) {
day.exercises.splice(itemIndex, 1);
markDirty();
renderAll();
}
}
}
const exerciseRow = button.closest("[data-exercise-id]");
if (exerciseRow && button.dataset.addProgression !== undefined) {
const exercise = state.cfg.exercises.find((item) => item.id === exerciseRow.dataset.exerciseId);
if (!exercise) return;
exercise.progressions = exercise.progressions || [];
const progression = {id: uid("progression"), name: "Neue Progression"};
exercise.progressions.push(progression);
if (!exercise.current_progression_id) exercise.current_progression_id = progression.id;
markDirty();
renderAll();
} else if (exerciseRow && button.dataset.removeExercise !== undefined) {
const id = exerciseRow.dataset.exerciseId;
const used = state.cfg.days.some((day) => (day.exercises || []).some((item) => item.exercise_id === id));
if (used && !confirm("Übung ist in Tagen hinterlegt. Trotzdem entfernen?")) return;
state.cfg.exercises = state.cfg.exercises.filter((exercise) => exercise.id !== id);
state.cfg.days.forEach((day) => {
day.exercises = (day.exercises || []).filter((item) => item.exercise_id !== id);
});
markDirty();
renderAll();
}
if (exerciseRow) {
const exercise = state.cfg.exercises.find((item) => item.id === exerciseRow.dataset.exerciseId);
const progressionRow = button.closest("[data-progression-id]");
if (exercise && progressionRow) {
exercise.progressions = exercise.progressions || [];
const progressionIndex = exercise.progressions.findIndex((item) => item.id === progressionRow.dataset.progressionId);
if (button.dataset.moveProgression) {
move(exercise.progressions, progressionIndex, Number(button.dataset.moveProgression));
} else if (button.dataset.removeProgression !== undefined && progressionIndex >= 0) {
const [removed] = exercise.progressions.splice(progressionIndex, 1);
if (removed?.id === exercise.current_progression_id) {
exercise.current_progression_id = exercise.progressions[0]?.id || "";
}
markDirty();
renderAll();
}
}
}
} catch (err) {
toast(err.message, "err");
}
});
$("planSelect").addEventListener("change", async (event) => {
if (state.dirty && !confirm("Ungespeicherte Änderungen verwerfen?")) {
renderPlanSelect();
return;
}
try {
await api(`/api/plans/${encodeURIComponent(event.target.value)}/select`, {method: "POST"});
await loadPlan(event.target.value);
} catch (err) {
toast(err.message, "err");
}
});
window.addEventListener("beforeunload", (event) => {
if (!state.dirty) return;
event.preventDefault();
event.returnValue = "";
});
const ready = window.BT?.ready || Promise.resolve();
ready.then(() => loadPlans()).catch((err) => showError(err.message));
</script>
</body>
</html>