Files
boehmitools-training/plugins/trainingsplan/static/index.html
T
2026-09-01 15:10:52 +02:00

851 lines
29 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>
<style>
:root {
--bg: #f5f7fa;
--surface: #ffffff;
--line: #d8dee8;
--line-strong: #aeb8c8;
--ink: #17202e;
--muted: #657184;
--accent: #0f766e;
--accent-ink: #ffffff;
--warn: #a16207;
--danger: #b42318;
--focus: #2563eb;
--shadow: 0 1px 2px rgba(15, 23, 42, .07);
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font: 14px/1.4 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
button, input, textarea, select {
font: inherit;
color: inherit;
}
button {
border: 1px solid var(--line-strong);
background: var(--surface);
min-height: 34px;
padding: 6px 10px;
border-radius: 6px;
cursor: pointer;
}
button:hover { border-color: var(--focus); }
button.primary {
background: var(--accent);
border-color: var(--accent);
color: var(--accent-ink);
}
button.danger {
color: var(--danger);
border-color: #e4aaa4;
}
button.icon {
width: 34px;
min-width: 34px;
padding: 0;
}
input, textarea, select {
width: 100%;
border: 1px solid var(--line);
background: #fff;
border-radius: 6px;
padding: 8px 9px;
min-height: 34px;
}
textarea {
min-height: 74px;
resize: vertical;
}
label {
display: grid;
gap: 5px;
min-width: 0;
font-size: 12px;
color: var(--muted);
}
label > span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border-bottom: 1px solid var(--line);
padding: 8px;
vertical-align: top;
text-align: left;
}
th {
color: var(--muted);
font-size: 12px;
font-weight: 700;
}
.topbar {
position: sticky;
top: 0;
z-index: 20;
display: grid;
grid-template-columns: minmax(220px, 1fr) auto;
gap: 12px;
align-items: center;
padding: 12px 18px;
background: #17202e;
color: #fff;
box-shadow: var(--shadow);
}
.brand {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.mark {
display: grid;
place-items: center;
width: 36px;
height: 36px;
border-radius: 8px;
background: #0f766e;
font-weight: 800;
}
.title {
min-width: 0;
}
.title h1 {
margin: 0;
font-size: 18px;
line-height: 1.2;
letter-spacing: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.title .sub {
color: #b8c2d2;
font-size: 12px;
margin-top: 2px;
}
.actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}
.actions button,
.actions select {
width: auto;
}
.layout {
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
min-height: calc(100vh - 61px);
}
.sidebar {
border-right: 1px solid var(--line);
background: #eef2f7;
padding: 14px;
}
.main {
padding: 16px;
min-width: 0;
}
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
margin-bottom: 14px;
}
.panel-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-bottom: 1px solid var(--line);
}
.panel-head h2,
.panel-head h3 {
margin: 0;
font-size: 15px;
letter-spacing: 0;
}
.panel-body {
padding: 14px;
}
.grid {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 10px;
}
.span-2 { grid-column: span 2; }
.span-3 { grid-column: span 3; }
.span-4 { grid-column: span 4; }
.span-5 { grid-column: span 5; }
.span-6 { grid-column: span 6; }
.span-8 { grid-column: span 8; }
.span-12 { grid-column: span 12; }
.tabs {
display: grid;
gap: 6px;
margin-bottom: 14px;
}
.tab {
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid transparent;
background: transparent;
text-align: left;
width: 100%;
min-height: 38px;
}
.tab.active {
background: #fff;
border-color: var(--line);
box-shadow: var(--shadow);
}
.count {
color: var(--muted);
font-size: 12px;
}
.muted { color: var(--muted); }
.tiny { font-size: 12px; }
.status {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 12px;
}
.pill {
border: 1px solid var(--line);
border-radius: 999px;
padding: 3px 8px;
background: #fff;
color: var(--muted);
font-size: 12px;
}
.pill.warn {
border-color: #f1d395;
color: var(--warn);
background: #fff8e7;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.day-list {
display: grid;
gap: 12px;
}
.day-shell {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
}
.day-head {
display: grid;
grid-template-columns: 64px minmax(160px, 1fr) auto;
gap: 10px;
align-items: end;
padding: 12px;
border-bottom: 1px solid var(--line);
}
.day-body {
padding: 12px;
display: grid;
gap: 12px;
}
.exercise-row {
display: grid;
grid-template-columns: minmax(180px, 2fr) 78px minmax(90px, 1fr) 90px minmax(120px, 1fr) 84px;
gap: 8px;
align-items: end;
padding: 10px 0;
border-bottom: 1px solid var(--line);
}
.exercise-row:last-child { border-bottom: 0; }
.empty {
border: 1px dashed var(--line-strong);
border-radius: 8px;
padding: 18px;
text-align: center;
color: var(--muted);
background: #fbfcfe;
}
.toast {
position: fixed;
right: 16px;
bottom: 16px;
z-index: 50;
max-width: min(420px, calc(100vw - 32px));
border: 1px solid var(--line);
background: #17202e;
color: #fff;
border-radius: 8px;
padding: 10px 12px;
box-shadow: 0 12px 30px rgba(15, 23, 42, .22);
display: none;
}
.toast.show { display: block; }
.toast.error { background: #7a271a; }
@media (max-width: 940px) {
.topbar,
.layout {
grid-template-columns: 1fr;
}
.sidebar {
border-right: 0;
border-bottom: 1px solid var(--line);
}
.tabs {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.exercise-row,
.day-head {
grid-template-columns: 1fr;
}
.actions {
justify-content: flex-start;
}
}
@media (max-width: 700px) {
.grid { grid-template-columns: 1fr; }
.span-2, .span-3, .span-4, .span-5, .span-6, .span-8, .span-12 { grid-column: auto; }
.tabs { grid-template-columns: 1fr; }
.main, .sidebar { padding: 10px; }
}
</style>
</head>
<body>
<header class="topbar">
<div class="brand">
<div class="mark">TP</div>
<div class="title">
<h1 id="pageTitle">Trainingsplan-Editor</h1>
<div class="sub" id="pageSub">Lade...</div>
</div>
</div>
<div class="actions">
<select id="planSelect" aria-label="Plan"></select>
<button id="btnNew" type="button">Neu</button>
<button id="btnRename" type="button">Umbenennen</button>
<button id="btnDelete" type="button" class="danger">Löschen</button>
<button id="btnSave" type="button">Speichern</button>
<button id="btnPublish" type="button" class="primary">Veröffentlichen</button>
</div>
</header>
<div class="layout">
<aside class="sidebar">
<div class="tabs">
<button class="tab active" type="button" data-tab="days"><span>Tage</span><span class="count" id="dayCount">0</span></button>
<button class="tab" type="button" data-tab="exercises"><span>Übungen</span><span class="count" id="exerciseCount">0</span></button>
<button class="tab" type="button" data-tab="json"><span>JSON</span><span class="count">Draft</span></button>
</div>
<section class="panel">
<div class="panel-head"><h2>Plan</h2></div>
<div class="panel-body">
<div class="grid">
<label class="span-12"><span>Titel</span><input id="metaTitle"></label>
<label class="span-12"><span>Untertitel</span><input id="metaSubtitle"></label>
</div>
<div class="status" id="statusPills"></div>
</div>
</section>
</aside>
<main class="main">
<section id="tabDays"></section>
<section id="tabExercises" hidden></section>
<section id="tabJson" hidden></section>
</main>
</div>
<div class="toast" id="toast"></div>
<script>
const state = {
plans: [],
activePlanId: "",
wrapper: null,
cfg: null,
tab: "days",
dirty: false,
};
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
const api = (path) => window.BT && BT.url ? BT.url(path) : path;
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (ch) => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"
}[ch]));
const rid = (prefix) => `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
function toast(message, error = false) {
const box = $("#toast");
box.textContent = message;
box.className = `toast show${error ? " error" : ""}`;
clearTimeout(window.__toastTimer);
window.__toastTimer = setTimeout(() => box.className = "toast", 2600);
}
async function request(path, options = {}) {
const res = await fetch(api(path), options);
const type = res.headers.get("content-type") || "";
const body = type.includes("application/json") ? await res.json() : await res.text();
if (!res.ok) {
throw new Error((body && body.error) || `HTTP ${res.status}`);
}
return body;
}
function markDirty() {
state.dirty = true;
renderStatus();
}
function exerciseName(id) {
return (state.cfg.exercises || []).find((item) => item.id === id)?.name || id || "Übung";
}
function textValue(obj, key) {
return (obj?.[key]?.items || []).join("\n");
}
function setTextItems(obj, key, value) {
obj[key] = {items: String(value || "").split("\n").map((item) => item.trim()).filter(Boolean)};
markDirty();
}
async function loadPlans(selectId = "") {
const data = await request("/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) {
state.wrapper = await request(`/api/plans/${encodeURIComponent(pid)}`);
state.activePlanId = pid;
state.cfg = state.wrapper.config || {meta: {}, exercises: [], 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 wrapper = state.wrapper || {};
const cfg = state.cfg || {days: [], exercises: []};
$("#pageTitle").textContent = cfg.meta?.title || wrapper.name || "Trainingsplan-Editor";
$("#pageSub").textContent = state.activePlanId ? `${state.activePlanId} · R${wrapper.revision || 1} / P${wrapper.published_revision || 1}` : "Kein Plan";
$("#dayCount").textContent = String((cfg.days || []).length);
$("#exerciseCount").textContent = String((cfg.exercises || []).length);
$("#statusPills").innerHTML = [
`<span class="pill">Schema ${cfg.schema_version || 4}/${cfg.contract_version || 3}</span>`,
`<span class="pill">Revision ${wrapper.revision || 1}</span>`,
`<span class="pill">Veröffentlicht ${wrapper.published_revision || 1}</span>`,
state.dirty || wrapper.has_unpublished_changes ? `<span class="pill warn">Ungespeichert</span>` : `<span class="pill">Aktuell</span>`,
].join("");
}
function renderAll() {
if (!state.cfg) {
$(".main").innerHTML = `<div class="empty">Kein Plan vorhanden.</div>`;
renderStatus();
return;
}
$("#metaTitle").value = state.cfg.meta?.title || "";
$("#metaSubtitle").value = state.cfg.meta?.subtitle || "";
renderStatus();
renderTabs();
renderDays();
renderExercises();
renderJson();
}
function renderTabs() {
$$(".tab").forEach((button) => button.classList.toggle("active", button.dataset.tab === state.tab));
$("#tabDays").hidden = state.tab !== "days";
$("#tabExercises").hidden = state.tab !== "exercises";
$("#tabJson").hidden = state.tab !== "json";
}
function renderDays() {
const host = $("#tabDays");
const days = state.cfg.days || [];
const options = (state.cfg.exercises || []).map((exercise) =>
`<option value="${esc(exercise.id)}">${esc(exercise.name || exercise.id)}</option>`
).join("");
host.innerHTML = `
<section class="panel">
<div class="panel-head">
<h2>Trainingstage</h2>
<div class="toolbar"><button type="button" data-add-day>Tag hinzufügen</button></div>
</div>
</section>
<div class="day-list">
${days.length ? days.map((day, index) => renderDay(day, index, options)).join("") : `<div class="empty">Noch keine Trainingstage.</div>`}
</div>
`;
days.forEach((day) => {
(day.exercises || []).forEach((item) => {
const row = host.querySelector(`[data-day-item="${CSS.escape(item.id)}"]`);
const select = row?.querySelector('[data-day-field="exercise_id"]');
if (select) select.value = item.exercise_id || "";
});
});
}
function renderDay(day, index, options) {
const rows = (day.exercises || []).map((item, itemIndex) => `
<div class="exercise-row" data-day-item="${esc(item.id)}">
<label><span>Übung</span><select data-day-field="exercise_id">${options}</select></label>
<label><span>Sätze</span><input type="number" min="1" max="40" data-day-field="sets" value="${esc(item.sets || "")}"></label>
<label><span>Wdh.</span><input data-day-field="reps" value="${esc(item.reps || "")}"></label>
<label><span>Sek.</span><input type="number" min="1" data-day-field="duration_seconds" value="${esc(item.duration_seconds || "")}"></label>
<label><span>Notiz</span><input data-day-field="notes" value="${esc(item.notes || "")}"></label>
<div class="toolbar">
<button class="icon" type="button" title="Nach oben" data-move-day-item="-1">↑</button>
<button class="icon" type="button" title="Nach unten" data-move-day-item="1">↓</button>
<button class="icon danger" type="button" title="Entfernen" data-remove-day-item>×</button>
</div>
</div>
`).join("");
return `
<section class="day-shell" data-day-id="${esc(day.id)}">
<div class="day-head">
<label><span>Nr.</span><input type="number" min="1" data-day-prop="num" value="${esc(day.num || index + 1)}"></label>
<label><span>Name</span><input data-day-prop="name" value="${esc(day.name || day.focus || `Tag ${index + 1}`)}"></label>
<div class="toolbar">
<button class="icon" type="button" title="Tag nach oben" data-move-day="-1">↑</button>
<button class="icon" type="button" title="Tag nach unten" data-move-day="1">↓</button>
<button class="danger" type="button" data-remove-day>Tag löschen</button>
</div>
</div>
<div class="day-body">
<div class="grid">
<label class="span-6"><span>Warm-up</span><textarea data-day-text="warmup">${esc(textValue(day, "warmup"))}</textarea></label>
<label class="span-6"><span>Cool-down</span><textarea data-day-text="cooldown">${esc(textValue(day, "cooldown"))}</textarea></label>
</div>
<div>
<table>
<thead><tr><th>Übung</th><th>Sätze</th><th>Wdh.</th><th>Sek.</th><th>Notiz</th><th></th></tr></thead>
</table>
${rows || `<div class="empty">Keine Übungen an diesem Tag.</div>`}
<div class="toolbar" style="margin-top:10px">
<select data-new-day-exercise>${options}</select>
<button type="button" data-add-day-exercise>Übung hinzufügen</button>
</div>
</div>
</div>
</section>
`;
}
function renderExercises() {
const host = $("#tabExercises");
host.innerHTML = `
<section class="panel">
<div class="panel-head"><h2>Übungen</h2></div>
<div class="panel-body">
<div class="grid">
<label class="span-4"><span>Name</span><input id="newExerciseName"></label>
<label class="span-4"><span>Cue</span><input id="newExerciseCue"></label>
<label class="span-3"><span>Equipment</span><input id="newExerciseEquipment"></label>
<div class="span-1 toolbar" style="align-items:end"><button class="icon primary" type="button" title="Übung hinzufügen" data-add-exercise>+</button></div>
</div>
</div>
</section>
<section class="panel">
<div class="panel-head"><h2>Bibliothek</h2></div>
<div class="panel-body">
${(state.cfg.exercises || []).length ? `
<table>
<thead><tr><th>Name</th><th>Cue</th><th>Equipment</th><th>Beschreibung</th><th></th></tr></thead>
<tbody>
${(state.cfg.exercises || []).map((exercise) => `
<tr data-exercise-id="${esc(exercise.id)}">
<td><input data-exercise-field="name" value="${esc(exercise.name || "")}"></td>
<td><input data-exercise-field="cue" value="${esc(exercise.cue || "")}"></td>
<td><input data-exercise-field="equipment" value="${esc(exercise.equipment || "")}"></td>
<td><input data-exercise-field="description" value="${esc(exercise.description || "")}"></td>
<td><button class="icon danger" type="button" title="Übung löschen" data-remove-exercise>×</button></td>
</tr>
`).join("")}
</tbody>
</table>` : `<div class="empty">Noch keine Übungen.</div>`}
</div>
</section>
`;
}
function renderJson() {
$("#tabJson").innerHTML = `
<section class="panel">
<div class="panel-head">
<h2>JSON</h2>
<div class="toolbar"><button type="button" data-apply-json>Übernehmen</button></div>
</div>
<div class="panel-body">
<textarea id="jsonEditor" style="min-height:520px;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">${esc(JSON.stringify(state.cfg, null, 2))}</textarea>
</div>
</section>
`;
}
function addExerciseFromForm() {
const name = $("#newExerciseName").value.trim();
if (!name) return toast("Name fehlt.", true);
state.cfg.exercises = state.cfg.exercises || [];
state.cfg.exercises.push({
id: rid("exercise"),
name,
cue: $("#newExerciseCue").value.trim(),
equipment: $("#newExerciseEquipment").value.trim(),
});
markDirty();
renderAll();
}
function addDay() {
const nums = (state.cfg.days || []).map((day) => Number(day.num || 0));
const num = Math.max(0, ...nums) + 1;
state.cfg.days = state.cfg.days || [];
state.cfg.days.push({
id: rid("day"),
num,
name: `Tag ${num}`,
warmup: {items: []},
cooldown: {items: []},
exercises: [],
});
markDirty();
renderAll();
}
function move(list, index, delta) {
const next = index + delta;
if (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 body = {expected_revision: state.wrapper.revision, config: state.cfg};
const saved = await request(`/api/plans/${encodeURIComponent(state.activePlanId)}`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(body),
});
state.wrapper = saved;
state.cfg = saved.config;
state.dirty = false;
renderAll();
const errors = saved.validation?.errors?.length || 0;
toast(errors ? `${errors} Fehler beim Speichern.` : "Gespeichert.", Boolean(errors));
}
async function publishPlan() {
if (state.dirty) await savePlan();
const saved = await request(`/api/plans/${encodeURIComponent(state.activePlanId)}/publish`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({expected_revision: state.wrapper.revision}),
});
state.wrapper = saved;
state.cfg = saved.config;
state.dirty = false;
renderAll();
await loadPlans(state.activePlanId);
toast("Veröffentlicht.");
}
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();
}
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) {
const key = target.dataset.exerciseField;
if (target.value.trim()) exercise[key] = target.value;
else if (key !== "name") 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 target = event.target.closest("button");
if (!target) return;
if (target.dataset.tab) {
state.tab = target.dataset.tab;
renderTabs();
if (state.tab === "json") renderJson();
} else if (target.id === "btnNew") {
const name = prompt("Name des neuen Plans:", "Neuer Trainingsplan");
if (!name) return;
const created = await request("/api/plans", {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({name})});
await loadPlans(created.id);
} else if (target.id === "btnRename") {
const name = prompt("Neuer Name:", state.wrapper?.name || "");
if (!name) return;
await request(`/api/plans/${encodeURIComponent(state.activePlanId)}/rename`, {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({name})});
await loadPlans(state.activePlanId);
} else if (target.id === "btnDelete") {
if (!confirm("Plan löschen?")) return;
await request(`/api/plans/${encodeURIComponent(state.activePlanId)}`, {method: "DELETE"});
await loadPlans();
} else if (target.id === "btnSave") {
await savePlan();
} else if (target.id === "btnPublish") {
await publishPlan();
} else if (target.dataset.addDay !== undefined) {
addDay();
} else if (target.dataset.addExercise !== undefined) {
addExerciseFromForm();
} else if (target.dataset.applyJson !== undefined) {
try {
state.cfg = JSON.parse($("#jsonEditor").value);
markDirty();
renderAll();
} catch (err) {
toast(err.message, true);
}
}
const dayShell = target.closest("[data-day-id]");
if (dayShell) {
const days = state.cfg.days || [];
const dayIndex = days.findIndex((day) => day.id === dayShell.dataset.dayId);
const day = days[dayIndex];
if (!day) return;
if (target.dataset.moveDay) {
move(days, dayIndex, Number(target.dataset.moveDay));
} else if (target.dataset.removeDay !== undefined) {
if (!confirm("Tag löschen?")) return;
days.splice(dayIndex, 1);
markDirty();
renderAll();
} else if (target.dataset.addDayExercise !== undefined) {
const select = dayShell.querySelector("[data-new-day-exercise]");
const exerciseId = select?.value;
if (!exerciseId) return toast("Keine Übung ausgewählt.", true);
day.exercises = day.exercises || [];
day.exercises.push({id: rid("day-exercise"), exercise_id: exerciseId});
markDirty();
renderAll();
}
const itemRow = target.closest("[data-day-item]");
if (itemRow) {
const itemIndex = (day.exercises || []).findIndex((item) => item.id === itemRow.dataset.dayItem);
if (target.dataset.moveDayItem) {
move(day.exercises, itemIndex, Number(target.dataset.moveDayItem));
} else if (target.dataset.removeDayItem !== undefined) {
day.exercises.splice(itemIndex, 1);
markDirty();
renderAll();
}
}
}
const exerciseRow = target.closest("[data-exercise-id]");
if (exerciseRow && target.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);
if (used) {
(state.cfg.days || []).forEach((day) => {
day.exercises = (day.exercises || []).filter((item) => item.exercise_id !== id);
});
}
markDirty();
renderAll();
}
});
$("#planSelect").addEventListener("change", async (event) => {
if (state.dirty && !confirm("Ungespeicherte Änderungen verwerfen?")) {
renderPlanSelect();
return;
}
await request(`/api/plans/${encodeURIComponent(event.target.value)}/select`, {method: "POST"});
await loadPlan(event.target.value);
});
window.addEventListener("beforeunload", (event) => {
if (!state.dirty) return;
event.preventDefault();
event.returnValue = "";
});
loadPlans().catch((err) => toast(err.message, true));
</script>
</body>
</html>