541 lines
21 KiB
HTML
541 lines
21 KiB
HTML
<!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(180px, 2fr) 72px minmax(80px, 1fr) 86px minmax(110px, 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-addline { display: grid; grid-template-columns: minmax(200px, 1fr) auto; gap: 8px; align-items: end; margin-top: 12px; }
|
||
.tp-table-input { min-width: 130px; }
|
||
.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 { grid-template-columns: 1fr; }
|
||
.tp-side { position: static; }
|
||
.tp-actions, .tp-day-actions { justify-content: flex-start; }
|
||
.tp-exercise-row { grid-template-columns: 1fr 86px 1fr 86px; }
|
||
.tp-exercise-row label:first-child, .tp-exercise-row label:nth-child(5), .tp-exercise-actions { grid-column: 1 / -1; }
|
||
}
|
||
@media (max-width: 620px) {
|
||
.tp-exercise-row { grid-template-columns: 1fr; }
|
||
}
|
||
</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) => ({"&": "&", "<": "<", ">": ">", '"': """, "'": "'"}[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 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 || 4}/${cfg.contract_version || 3}</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>Sätze<input type="number" min="1" max="40" data-day-field="sets" value="${esc(item.sets || "")}"></label>
|
||
<label>Wdh.<input type="text" data-day-field="reps" value="${esc(item.reps || "")}"></label>
|
||
<label>Sek.<input type="number" min="1" data-day-field="duration_seconds" value="${esc(item.duration_seconds || "")}"></label>
|
||
<label>Notiz<input type="text" data-day-field="notes" value="${esc(item.notes || "")}"></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) => `
|
||
<tr data-exercise-id="${esc(exercise.id)}">
|
||
<td><input class="tp-table-input" data-exercise-field="name" value="${esc(exercise.name || "")}"></td>
|
||
<td><input class="tp-table-input" data-exercise-field="cue" value="${esc(exercise.cue || "")}"></td>
|
||
<td><input class="tp-table-input" data-exercise-field="equipment" value="${esc(exercise.equipment || "")}"></td>
|
||
<td><input class="tp-table-input" data-exercise-field="description" value="${esc(exercise.description || "")}"></td>
|
||
<td class="bt-mono bt-muted">${esc(exercise.id)}</td>
|
||
<td><button class="mini danger" data-remove-exercise>×</button></td>
|
||
</tr>
|
||
`).join("");
|
||
$("tab-uebungen").innerHTML = `
|
||
<div class="bt-card">
|
||
<h2>Übung anlegen</h2>
|
||
<div class="bt-grid4">
|
||
<label>Name<input id="newExerciseName" type="text"></label>
|
||
<label>Cue<input id="newExerciseCue" type="text"></label>
|
||
<label>Equipment<input id="newExerciseEquipment" type="text"></label>
|
||
<div style="display:flex;align-items:end"><button class="primary" data-add-exercise>Hinzufügen</button></div>
|
||
</div>
|
||
</div>
|
||
<div class="bt-card bt-card-flush">
|
||
${rows ? `
|
||
<table class="bt-table">
|
||
<thead><tr><th>Name</th><th>Cue</th><th>Equipment</th><th>Beschreibung</th><th>ID</th><th></th></tr></thead>
|
||
<tbody>${rows}</tbody>
|
||
</table>
|
||
` : `<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,
|
||
cue: $("newExerciseCue")?.value.trim() || "",
|
||
equipment: $("newExerciseEquipment")?.value.trim() || "",
|
||
});
|
||
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();
|
||
}
|
||
|
||
async function savePlan() {
|
||
if (!state.activePlanId || !state.cfg) return;
|
||
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;
|
||
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();
|
||
});
|
||
|
||
document.addEventListener("input", (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 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 (key === "sets" || key === "duration_seconds") {
|
||
const value = Number(target.value || 0);
|
||
if (value > 0) item[key] = value;
|
||
else delete item[key];
|
||
} else if (target.value.trim()) item[key] = target.value;
|
||
else delete item[key];
|
||
markDirty();
|
||
}
|
||
}
|
||
});
|
||
|
||
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.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();
|
||
}
|
||
} 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>
|