Files
boehmitools-training/plugins/trainingsplan/static/index.html
T
2026-07-24 21:37:03 +02:00

1119 lines
68 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>
/* ------------------------------------------------------------------
Trainingsplan- & Rezept-Editor · plugin-eigenes Layout.
Farben, Buttons, Felder, Karten und Tabs kommen aus /shared/boehmi.css.
Die Klassennamen bleiben identisch zum Einzeltool.
------------------------------------------------------------------ */
/* Kompatibilitaets-Aliase fuer die alten Variablennamen */
:root{
--ink:var(--bt-ink); --accent:var(--bt-accent); --acc2:var(--bt-teal);
--kb:var(--bt-violet); --light:var(--bt-bg-2); --grid:var(--bt-line);
--mute:var(--bt-muted); --bg:var(--bt-bg); --card:var(--bt-surface);
}
body > header{
position:sticky; top:var(--bt-appbar-h); z-index:30;
display:flex; align-items:center; gap:10px; flex-wrap:wrap;
padding:8px 16px; background:var(--bt-surface);
border-bottom:1px solid var(--bt-line);
}
body > header h1{ display:none } /* Titel steht in der App-Bar */
header .sub{ color:var(--bt-muted); font-size:12px; font-weight:600 }
header .spacer{ flex:1 }
.planbar{
position:sticky; top:calc(var(--bt-appbar-h) + 47px); z-index:25;
display:flex; align-items:center; gap:8px; flex-wrap:wrap;
background:var(--bt-bg-2); border-bottom:1px solid var(--bt-line); padding:7px 16px;
}
.planbar .pblabel{
font-weight:800; color:var(--bt-muted); font-size:11px;
letter-spacing:.08em; text-transform:uppercase;
}
.planbar select{ width:auto; min-width:190px; font-weight:650; padding:5px 9px }
.planbar .spacer{ flex:1 }
.tabs{
position:sticky; top:calc(var(--bt-appbar-h) + 47px + 42px); z-index:20;
display:flex; gap:3px; flex-wrap:wrap; padding:0 16px;
background:var(--bt-bg); border-bottom:1px solid var(--bt-line);
}
.tab{
padding:9px 13px; font-size:13px; font-weight:650; color:var(--bt-muted);
background:transparent; border:none; border-bottom:2px solid transparent;
margin-bottom:-1px; cursor:pointer; border-radius:var(--bt-r-sm) var(--bt-r-sm) 0 0;
}
.tab:hover{ color:var(--bt-ink); background:var(--bt-bg-2) }
.tab.active{ color:var(--bt-accent); border-bottom-color:var(--bt-accent) }
main{ padding:18px 16px 70px; max-width:1080px; margin:0 auto }
.panel{ display:none } .panel.active{ display:block }
.card{
background:var(--bt-surface); border:1px solid var(--bt-line);
border-radius:var(--bt-r-lg); padding:16px; margin-bottom:14px;
box-shadow:var(--bt-shadow);
}
.card h2{ margin:0 0 3px; font-size:15px; font-weight:750; color:var(--bt-ink) }
.card h3{
margin:16px 0 7px; font-size:11.5px; font-weight:800; letter-spacing:.09em;
text-transform:uppercase; color:var(--bt-teal);
}
.card .hint{ color:var(--bt-muted); font-size:12.5px; margin:0 0 11px }
.row{ display:flex; gap:10px; flex-wrap:wrap }
.row>*{ flex:1; min-width:120px }
.grid2{ display:grid; grid-template-columns:1fr 1fr; gap:10px }
.grid3{ display:grid; grid-template-columns:repeat(3,1fr); gap:10px }
.grid4{ display:grid; grid-template-columns:repeat(4,1fr); gap:8px }
.colorbox{ display:flex; align-items:center; gap:6px }
.colorbox input[type=color]{ flex:none }
.item{
border:1px solid var(--bt-line); border-radius:var(--bt-r);
padding:11px 13px; margin:10px 0; background:var(--bt-surface-2); position:relative;
}
.item .item-head{ display:flex; align-items:center; gap:8px; margin-bottom:7px }
.item .item-head .n{ font-weight:800; color:var(--bt-teal); font-size:11px; letter-spacing:.05em }
.item .item-head .spacer{ flex:1 }
.ex{
border:1px solid var(--bt-line); border-left:3px solid var(--bt-teal);
padding:9px 11px; margin:8px 0; background:var(--bt-surface);
border-radius:0 var(--bt-r) var(--bt-r) 0;
}
.ex.kb{ border-left-color:var(--bt-violet) }
.rotlabel{ font-weight:750; color:var(--bt-ink); margin:13px 0 2px; font-size:13px }
.daytag{
display:inline-block; background:var(--bt-accent); color:var(--bt-accent-ink);
border-radius:999px; padding:2px 10px; font-weight:700; font-size:11px;
}
.daytag.kb{ background:var(--bt-violet); color:#fff }
.stagerow{ display:grid; grid-template-columns:120px repeat(4,1fr) 34px; gap:8px; align-items:center; margin:6px 0 }
.stagerow.head{ font-size:11px; color:var(--bt-muted); font-weight:800; letter-spacing:.05em; text-transform:uppercase }
.stagekey{ font-weight:750; color:var(--bt-violet) }
.flex-add{ margin-top:8px }
.schema-editor{
margin-top:9px; border:1px solid var(--bt-line); border-radius:var(--bt-r-sm);
background:var(--bt-bg-2); overflow:hidden;
}
.schema-editor>summary{
cursor:pointer; padding:9px 11px; font-size:12px; font-weight:750; color:var(--bt-teal);
list-style-position:inside;
}
.schema-editor-body{ padding:0 11px 11px }
.schema-toggle{ display:flex; align-items:flex-start; gap:8px; margin:3px 0 10px; font-size:12px; color:var(--bt-muted) }
.schema-toggle input{ width:auto; margin-top:2px }
.schema-grid{ display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px }
.schema-note{ margin:7px 0 0; color:var(--bt-muted); font-size:11.5px; line-height:1.45 }
.phase-schema-cell{ border:1px solid var(--bt-line); border-radius:var(--bt-r-sm); padding:9px; background:var(--bt-surface) }
.legend{
background:var(--bt-accent-soft); border:1px solid var(--bt-line-soft);
border-left:3px solid var(--bt-accent); border-radius:0 var(--bt-r) var(--bt-r) 0;
padding:10px 13px; font-size:12.5px; color:var(--bt-ink-soft); margin-bottom:14px;
}
.legend code{ background:rgba(128,128,128,.16); padding:1px 5px; border-radius:4px }
#toast{
position:fixed; bottom:18px; left:50%; transform:translateX(-50%) translateY(8px);
background:var(--bt-ink); color:var(--bt-bg); padding:10px 18px;
border-radius:var(--bt-r); font-size:13px; font-weight:600; opacity:0;
transition:opacity .2s, transform .2s; pointer-events:none; z-index:100;
max-width:90vw; box-shadow:var(--bt-shadow-lg);
}
#toast.show{ opacity:.97; transform:translateX(-50%) }
#toast.err{ background:var(--bt-err); color:#fff }
.muted{ color:var(--bt-muted); font-size:12.5px }
.revision-pill{display:inline-flex;align-items:center;padding:4px 8px;border:1px solid var(--bt-line);border-radius:999px;font-size:11px;font-weight:750;color:var(--bt-muted)}
.revision-pill.draft{color:var(--bt-accent);background:var(--bt-accent-soft)}
.validation-list{display:grid;gap:8px;margin-top:10px}.validation-item{padding:10px;border:1px solid var(--bt-line);border-radius:var(--bt-r-sm);background:var(--bt-surface-2);font-size:12px}.validation-item.error{border-left:3px solid var(--bt-err)}.validation-item.warning{border-left:3px solid var(--bt-accent)}
.proposal-card{padding:12px;border:1px solid var(--bt-line);border-radius:var(--bt-r);background:var(--bt-surface-2);margin:9px 0}.proposal-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:9px}.proposal-target{outline:3px solid var(--bt-accent);outline-offset:4px;transition:outline-color .25s ease}
.table-scroll{overflow:auto;max-width:100%;margin-top:10px} .table-scroll table{width:100%;border-collapse:collapse;font-size:12px;min-width:680px}.table-scroll th,.table-scroll td{padding:8px;border-bottom:1px solid var(--bt-line);vertical-align:top;text-align:left}.table-scroll th{color:var(--bt-muted);font-size:11px;text-transform:uppercase;letter-spacing:.04em}
@media(max-width:760px){
.grid2,.grid3,.grid4,.schema-grid{ grid-template-columns:1fr }
.stagerow{ grid-template-columns:1fr }
}
</style>
</head>
<body data-bt-title="Trainingsplan- &amp; Rezept-Editor" data-bt-icon="🏋">
<header>
<h1>Trainingsplan-Editor</h1>
<span class="sub" id="metaSub"></span>
<span class="spacer"></span>
<span class="revision-pill" id="revisionState">Revision</span>
<button class="primary" id="btnPdf">PDF generieren</button>
<button id="btnValidate">Prüfen</button>
<button id="btnSave">Entwurf speichern</button>
<button class="primary" id="btnPublish">Veröffentlichen</button>
<button class="ghost" id="btnReset">Zurücksetzen</button>
</header>
<div class="planbar">
<span class="pblabel">Phase:</span>
<select id="planSelect" title="Aktive Phase"></select>
<button class="mini" id="btnPlanNew" title="Neue Phase aus Vorlage">Neu</button>
<button class="mini" id="btnPlanDup" title="Aktuelle Phase duplizieren">Duplizieren</button>
<button class="mini" id="btnPlanRen" title="Phase umbenennen">Umbenennen</button>
<button class="mini danger" id="btnPlanDel" title="Phase löschen">Löschen</button>
<span class="spacer"></span>
<button class="mini" id="btnPlanExport" title="Phase als .json-Datei herunterladen">Export</button>
<button class="mini" id="btnPlanImport" title="Phase aus .json-Datei laden">Import</button>
<input type="file" id="planFile" accept="application/json,.json" style="display:none">
</div>
<div class="tabs" id="tabs"></div>
<main>
<div class="legend">
Umlaute und Sonderzeichen (<b>ü ä ö ß · • × °</b>) einfach direkt eintippen — sie landen so im PDF.
Für Formatierung: <b>&lt;b&gt;&lt;/b&gt;</b> = fett, <code>&lt;br/&gt;</code> = Zeilenumbruch,
<code>&lt;font color='#6D28D9'&gt;&lt;/font&gt;</code> = Farbe. Ein kaufmännisches Und tippst du normal als <code>&amp;</code>.
</div>
<div id="panels"></div>
</main>
<div id="toast"></div>
<script>
"use strict";
let cfg = null;
let planRevision=1, publishedRevision=1, hasUnpublishedChanges=false, trackedSessions=0;
const TABS_TRAINING = [
["allgemein","Allgemein"],
["vorspann","Vorspann"],
["bibliothek","Bibliothek"],
["tage","Trainingstage"],
["stufen","Progressionsstufen"],
["phasen","Progression"],
["warmup","Warm-up / Cool-down"],
["pruefung","Prüfung & Vorschläge"],
];
const TABS_RECIPE = [
["kopf","Kopf & Intro"],
["kategorien","Kategorien"],
["rezepte","Rezepte"],
];
function currentTabs(){ return (cfg && cfg.type==="recipe") ? TABS_RECIPE : TABS_TRAINING; }
// ---------- tiny DOM helpers ----------
function el(tag, props, kids){
const n = document.createElement(tag);
if(props) for(const k in props){
if(k==="class") n.className=props[k];
else if(k==="html") n.innerHTML=props[k];
else if(k==="text") n.textContent=props[k];
else if(k.startsWith("on")) n.addEventListener(k.slice(2).toLowerCase(),props[k]);
else if(k==="value") n.value=props[k];
else n.setAttribute(k,props[k]);
}
(kids||[]).forEach(c=>{ if(c==null)return; n.appendChild(typeof c==="string"?document.createTextNode(c):c); });
return n;
}
// bound text input -> writes obj[key]
function inp(obj,key,opts){
opts=opts||{};
const e=el(opts.area?"textarea":"input",{value:obj[key]!=null?obj[key]:""});
if(!opts.area) e.type=opts.type||"text";
if(opts.rows) e.rows=opts.rows;
e.addEventListener("input",()=>{ obj[key]= (opts.type==="number") ? (e.value===""?"":e.value) : e.value; });
return e;
}
function field(labelText,inputEl){ return el("div",{},[el("label",{text:labelText}), inputEl]); }
function toast(msg,isErr){
const t=document.getElementById("toast");
t.textContent=msg; t.className="show"+(isErr?" err":"");
clearTimeout(t._t); t._t=setTimeout(()=>t.className="",2200);
}
// ---------- tabs ----------
function buildTabs(){
const tabs=currentTabs();
const bar=document.getElementById("tabs"); bar.innerHTML="";
const panels=document.getElementById("panels"); panels.innerHTML="";
tabs.forEach(([id,label],i)=>{
bar.appendChild(el("div",{class:"tab"+(i===0?" active":""),"data-tab":id,onClick:()=>selectTab(id)},[label]));
panels.appendChild(el("div",{class:"panel"+(i===0?" active":""),"data-panel":id}));
});
}
function selectTab(id){
document.querySelectorAll(".tab").forEach(t=>t.classList.toggle("active",t.getAttribute("data-tab")===id));
document.querySelectorAll(".panel").forEach(p=>p.classList.toggle("active",p.getAttribute("data-panel")===id));
window.scrollTo({top:0});
}
// ============================================================
// RENDER PANELS
// ============================================================
function renderAll(){
document.getElementById("metaSub").textContent = (cfg.meta&&cfg.meta.title)||"";
buildTabs();
if(cfg.type==="recipe"){
renderKopf(); renderKategorien(); renderRezepte();
}else{
renderAllgemein(); renderVorspann(); renderBibliothek();
renderTage(); renderStufen(); renderPhasen(); renderWarmup(); renderPruefung();
}
}
function renderAll_training_unused(){
renderAllgemein();
renderVorspann();
renderBibliothek();
renderTage();
renderStufen();
renderPhasen();
renderWarmup();
}
// ---- Zeichen-Ebene: Anzeige = echte Zeichen, Versand = ReportLab-sicher ----
// Beim Laden: HTML-Entities -> echte Zeichen (damit die Felder lesbar sind).
const DECODE_MAP={"&uuml;":"ü","&Uuml;":"Ü","&auml;":"ä","&Auml;":"Ä","&ouml;":"ö","&Ouml;":"Ö",
"&szlig;":"ß","&middot;":"·","&ndash;":"","&mdash;":"—","&bull;":"•","&times;":"×","&deg;":"°",
"&nbsp;":"\u00A0","&ldquo;":"\u201C","&rdquo;":"\u201D","&bdquo;":"\u201E","&lsquo;":"\u2018",
"&rsquo;":"\u2019","&hellip;":"…","&lt;":"<","&gt;":">","&amp;":"&"};
function decodeStr(s){
if(typeof s!=="string")return s;
return s.replace(/&[a-zA-Z]+;/g,m=>DECODE_MAP.hasOwnProperty(m)?DECODE_MAP[m]:m);
}
// Beim Versand: nacktes & -> &amp; (Umlaute bleiben echt, ReportLab rendert sie direkt).
function encodeStr(s){
if(typeof s!=="string")return s;
return s.replace(/&(?!(?:amp|lt|gt|quot|apos|nbsp|#\d+|#x[0-9a-fA-F]+);)/g,"&amp;");
}
// Rekursiv über alle Strings; skipKeys werden nicht angefasst (z.B. Farben, drawString-Texte).
function walkStrings(obj,fn,skipKeys){
if(Array.isArray(obj)){ for(let i=0;i<obj.length;i++){ obj[i]=walkStrings(obj[i],fn,skipKeys); } return obj; }
if(obj&&typeof obj==="object"){
for(const k in obj){ if(skipKeys&&skipKeys.has(k))continue; obj[k]=walkStrings(obj[k],fn,skipKeys); }
return obj;
}
return fn(obj);
}
// Diese Felder gehen NICHT durch einen ReportLab-Paragraph (Fußzeile/Metadaten/Farben) -> beim Encode auslassen.
const ENCODE_SKIP=new Set(["footer_left","header_left","doc_title","output_name","colors"]);
function decodeConfig(c){ return walkStrings(c,decodeStr,null); }
function encodeConfig(c){ return walkStrings(c,encodeStr,ENCODE_SKIP); }
// Interne UI-Flags (führender _) nicht mitspeichern/senden
function stripInternal(o){
if(Array.isArray(o)) return o.map(stripInternal);
if(o&&typeof o==="object"){
const out={};
for(const k in o){ if(k.charAt(0)==="_")continue; out[k]=stripInternal(o[k]); }
return out;
}
return o;
}
function payloadOf(){ return encodeConfig(stripInternal(JSON.parse(JSON.stringify(cfg)))); }
function panel(id){ const p=document.querySelector('.panel[data-panel="'+id+'"]'); p.innerHTML=""; return p; }
function card(title,hint){ const c=el("div",{class:"card"}); if(title)c.appendChild(el("h2",{text:title})); if(hint)c.appendChild(el("p",{class:"hint",html:hint})); return c; }
// ---- ALLGEMEIN (meta + colors) ----
const COLOR_LABELS={INK:"Text",ACCENT:"Akzent (orange)",ACC2:"Zweitfarbe (teal)",KBCOL:"Kettlebell (lila)",
LINK:"Links",LIGHT:"Zeilen hell",GRID:"Rahmen/Linien",MUTE:"Grau",DARK:"Kopfzeilen dunkel",
WARMBG:"Warm-up Hintergrund",GOALBG:"Ziele Hintergrund",KBBG:"KB Hintergrund"};
function renderAllgemein(){
const p=panel("allgemein");
const c1=card("Kopf & Datei");
c1.appendChild(field("Titel", inp(cfg.meta,"title")));
c1.appendChild(field("Untertitel", inp(cfg.meta,"subtitle",{area:true,rows:2})));
const g=el("div",{class:"grid3"});
g.appendChild(field("Dateiname (PDF)", inp(cfg.meta,"output_name")));
g.appendChild(field("Anzahl Wochen", inp(cfg.meta,"weeks",{type:"number"})));
const dayInp=el("input",{type:"number",value:(cfg.meta.days!=null?cfg.meta.days:(cfg.days||[]).length)});
dayInp.min=0;
dayInp.addEventListener("change",()=>{ const n=Math.max(0,parseInt(dayInp.value||"0",10)); setDayCount(n); renderTage(); renderWarmup(); });
g.appendChild(field("Anzahl Trainingstage", dayInp));
c1.appendChild(g);
c1.appendChild(field("Fußzeile links", inp(cfg.meta,"footer_left")));
c1.appendChild(field("PDF-Dokumenttitel (Metadaten)", inp(cfg.meta,"doc_title")));
p.appendChild(c1);
cfg.training_format=cfg.training_format||{mode:"auto",work_seconds:null,rest_seconds:null,rounds:null,fixed_interval:false,rounds_scope:"block"};
const tf=cfg.training_format;
const formatCard=card("Trainingsformat & feste Intervalle","Diese Angaben sind die eindeutige Quelle für Tracker und Analyse. Bei „Automatisch“ bleibt die Erkennung aus den Plantexten als Fallback aktiv.");
const fg=el("div",{class:"grid4"});
const modeSel=el("select",{});
[["auto","Automatisch erkennen"],["sets_reps","Normale Sätze / Reps"],["tabata","Tabata"],["fixed_interval","Feste Intervalle"]].forEach(([v,t])=>modeSel.appendChild(el("option",{value:v,text:t})));
modeSel.value=tf.mode||"auto";
modeSel.addEventListener("change",()=>{tf.mode=modeSel.value;tf.fixed_interval=["tabata","fixed_interval"].includes(tf.mode);drawFormatHint();});
fg.appendChild(field("Format",modeSel));
const work=el("input",{type:"number",min:"1",value:tf.work_seconds||"",placeholder:"z. B. 20"});
work.addEventListener("input",()=>tf.work_seconds=parseInt(work.value||"0",10)||null);
fg.appendChild(field("Arbeitszeit (Sek.)",work));
const rest=el("input",{type:"number",min:"1",value:tf.rest_seconds||"",placeholder:"z. B. 10"});
rest.addEventListener("input",()=>tf.rest_seconds=parseInt(rest.value||"0",10)||null);
fg.appendChild(field("Pause (Sek.)",rest));
const rounds=el("input",{type:"number",min:"1",max:"99",value:tf.rounds||"",placeholder:"z. B. 8"});
rounds.addEventListener("input",()=>{tf.rounds=parseInt(rounds.value||"0",10)||null;drawFormatHint();});
fg.appendChild(field("Intervalle pro Block",rounds));
formatCard.appendChild(fg);
const formatHint=el("div",{class:"schema-note"});
function drawFormatHint(){
const count=parseInt(tf.rounds||0,10);
formatHint.textContent=["tabata","fixed_interval"].includes(tf.mode)
? `Die Rundenzahl gilt pro Trainingsblock. Beispiel: 8 Intervalle bei 4 Übungen = 2 Durchgänge je Übung. Steht im Rotationstitel zusätzlich „3 Sätze“, zeigt der Tracker 6 Werte je Übung.`
: (tf.mode==="auto" ? "Der Tracker versucht Tabata, Arbeits-/Pausenzeit und Rundenzahl aus den Texten des Plans zu erkennen." : "Satzanzahlen werden aus den Rotationstiteln oder aus einem Ergebnisschema übernommen.");
}
drawFormatHint(); formatCard.appendChild(formatHint); p.appendChild(formatCard);
const c2=card("Farbpalette","Wirkt auf das gesamte PDF. Klick auf das Farbfeld zum Ändern.");
const grid=el("div",{class:"grid3"});
Object.keys(cfg.colors).forEach(k=>{
const wrap=el("div",{});
wrap.appendChild(el("label",{text:(COLOR_LABELS[k]||k)+" ("+k+")"}));
const box=el("div",{class:"colorbox"});
const col=el("input",{type:"color",value:cfg.colors[k]});
const txt=el("input",{type:"text",value:cfg.colors[k]});
col.addEventListener("input",()=>{cfg.colors[k]=col.value.toUpperCase();txt.value=cfg.colors[k];});
txt.addEventListener("input",()=>{cfg.colors[k]=txt.value; if(/^#[0-9a-fA-F]{6}$/.test(txt.value))col.value=txt.value;});
box.appendChild(col); box.appendChild(txt); wrap.appendChild(box); grid.appendChild(wrap);
});
c2.appendChild(grid);
p.appendChild(c2);
}
// ---- VORSPANN ----
function renderVorspann(){
const p=panel("vorspann"); const f=cfg.front;
const c1=card("So funktioniert eine Session");
c1.appendChild(field("Überschrift", inp(f,"session_how_head")));
c1.appendChild(field("Fließtext", inp(f,"session_how_body",{area:true,rows:4})));
c1.appendChild(el("h3",{text:"Ablauf-Tabelle"}));
c1.appendChild(rowsEditor(f.flow_rows,["Teil","Inhalt","Zeit"]));
c1.appendChild(field("Timer-Hinweis", inp(f,"timer_note",{area:true,rows:2})));
p.appendChild(c1);
const c2=card("Kettlebell-Kasten");
c2.appendChild(field("Überschrift", inp(f,"kb_head")));
c2.appendChild(field("Kasten-Text", inp(f,"kb_box",{area:true,rows:8})));
p.appendChild(c2);
const c3=card("Ziele-Kasten");
c3.appendChild(field("Überschrift", inp(f,"goals_head")));
c3.appendChild(field("Kasten-Text", inp(f,"goals_box",{area:true,rows:7})));
c3.appendChild(field("Hinweis darunter", inp(f,"goals_note",{area:true,rows:4})));
p.appendChild(c3);
const c4=card("Übungs-Bibliothek — Einleitung");
c4.appendChild(field("Überschrift", inp(f,"lib_head")));
c4.appendChild(field("Intro-Text", inp(f,"lib_intro",{area:true,rows:2})));
p.appendChild(c4);
const c5=card("Standortbestimmung & Reminder");
c5.appendChild(field("Überschrift Standortbestimmung", inp(f,"baseline_head")));
c5.appendChild(field("Intro", inp(f,"baseline_intro",{area:true,rows:2})));
c5.appendChild(el("h3",{text:"Test-Tabelle"}));
c5.appendChild(rowsEditor(f.baseline_rows,["Test","Spalte 1","Spalte 2"]));
c5.appendChild(el("h3",{text:"Kurz-Reminder"}));
c5.appendChild(field("Überschrift Reminder", inp(f,"reminder_head")));
c5.appendChild(listEditor(f.reminders,"Reminder"));
c5.appendChild(field("Hinweis vor den Session-Blättern", inp(f,"sheets_note",{area:true,rows:3})));
p.appendChild(c5);
}
// generic list-of-strings editor
function listEditor(arr,label){
const box=el("div",{});
function draw(){
box.innerHTML="";
arr.forEach((v,i)=>{
const line=el("div",{class:"item"});
const head=el("div",{class:"item-head"},[el("span",{class:"n",text:label+" "+(i+1)}),el("span",{class:"spacer"}),
el("button",{class:"mini danger",onClick:()=>{arr.splice(i,1);draw();}},["Entfernen"])]);
line.appendChild(head);
const t=el("textarea",{value:v}); t.rows=2;
t.addEventListener("input",()=>arr[i]=t.value);
line.appendChild(t); box.appendChild(line);
});
box.appendChild(el("button",{class:"mini flex-add",onClick:()=>{arr.push("");draw();}},["+ "+label+" hinzufügen"]));
}
draw(); return box;
}
// generic table-rows editor (array of arrays). First row = header.
function rowsEditor(rows,placeholders){
const box=el("div",{});
function draw(){
box.innerHTML="";
rows.forEach((r,i)=>{
const line=el("div",{class:"item"});
const head=el("div",{class:"item-head"},[
el("span",{class:"n",text:i===0?"Kopfzeile":"Zeile "+i}),el("span",{class:"spacer"}),
i===0?null:el("button",{class:"mini danger",onClick:()=>{rows.splice(i,1);draw();}},["Entfernen"])]);
line.appendChild(head);
const g=el("div",{class:"grid3"});
r.forEach((cell,j)=>{
const e=el("input",{type:"text",value:cell,placeholder:(placeholders&&placeholders[j])||""});
e.addEventListener("input",()=>r[j]=e.value);
g.appendChild(e);
});
line.appendChild(g); box.appendChild(line);
});
box.appendChild(el("button",{class:"mini flex-add",onClick:()=>{rows.push(rows[0].map(()=>""));draw();}},["+ Zeile hinzufügen"]));
}
draw(); return box;
}
// ---- BIBLIOTHEK ----
function renderBibliothek(){
const p=panel("bibliothek");
const c=card("Übungs-Bibliothek","Gruppen mit Übungen. Farbe „KBCOL“ = lila Kettlebell-Gruppe.");
const host=el("div",{});
function draw(){
host.innerHTML="";
cfg.library.forEach((grp,gi)=>{
const gcard=el("div",{class:"item"});
const head=el("div",{class:"item-head"},[
el("span",{class:"n",text:"Gruppe "+(gi+1)}),el("span",{class:"spacer"}),
el("button",{class:"mini",onClick:()=>{if(gi>0){cfg.library.splice(gi-1,0,cfg.library.splice(gi,1)[0]);draw();}}},["▲"]),
el("button",{class:"mini",onClick:()=>{if(gi<cfg.library.length-1){cfg.library.splice(gi+1,0,cfg.library.splice(gi,1)[0]);draw();}}},["▼"]),
el("button",{class:"mini danger",onClick:()=>{cfg.library.splice(gi,1);draw();}},["Gruppe löschen"])]);
gcard.appendChild(head);
const g2=el("div",{class:"grid2"});
g2.appendChild(field("Gruppen-Titel", inp(grp,"group")));
const sel=el("select",{}); ["ACC2","KBCOL","ACCENT"].forEach(o=>sel.appendChild(el("option",{value:o,text:o+(o==="KBCOL"?" (lila)":o==="ACC2"?" (teal)":" (orange)")})));
sel.value=grp.color; sel.addEventListener("change",()=>grp.color=sel.value);
g2.appendChild(field("Farbe", sel));
gcard.appendChild(g2);
grp.items.forEach((it,ii)=>{
const ic=el("div",{class:"ex"+(grp.color==="KBCOL"?" kb":"")});
const ih=el("div",{class:"item-head"},[el("span",{class:"n",text:"#"+(ii+1)}),el("span",{class:"spacer"}),
el("button",{class:"mini danger",onClick:()=>{grp.items.splice(ii,1);draw();}},["×"])]);
ic.appendChild(ih);
ic.appendChild(field("Name", inp(it,"name")));
ic.appendChild(field("Ausführung (Kurz)", inp(it,"desc",{area:true,rows:2})));
ic.appendChild(field("Anleitungs-Link (leer = „Standard-Bewegung“)", inp(it,"url")));
gcard.appendChild(ic);
});
gcard.appendChild(el("button",{class:"mini flex-add",onClick:()=>{grp.items.push({name:"",desc:"",url:""});draw();}},["+ Übung"]));
host.appendChild(gcard);
});
host.appendChild(el("button",{class:"flex-add",onClick:()=>{cfg.library.push({group:"NEUE GRUPPE",color:"ACC2",items:[]});draw();}},["+ Gruppe hinzufügen"]));
}
draw(); c.appendChild(host); p.appendChild(c);
}
// ---- TRAININGSTAGE ----
function stageKeys(){ return ["",...Object.keys(cfg.stages)]; }
function keyExerciseNames(key){
const names=[];
(cfg.days||[]).forEach(d=>(d.rotations||[]).forEach(rot=>(rot.exercises||[]).forEach(it=>{
if(it.key===key && it.name && !names.includes(it.name)) names.push(it.name);
})));
return names;
}
function labelForKey(key){
if(!key) return "(keine — leichte Erholung)";
const st=cfg.stages[key];
if(st && st.name) return st.name;
const names=keyExerciseNames(key);
if(names.length) return names.join(" / ");
const steps=((st&&st.steps)||[]).map(v=>typeof v==="object"?(v.name||""):v).filter(Boolean);
return steps.length ? ("Ohne Übungszuordnung — "+steps.join(" → ")) : "Ohne Übungszuordnung";
}
function freshResultSchema(){
return {mode:"auto",weight_mode:"none",laterality:"bilateral",sides_mode:"same",sets:null,locked_sets:false};
}
function cleanResultSchema(raw){
const src=(raw&&typeof raw==="object")?raw:{}; const out=freshResultSchema();
if(["auto","reps","seconds","minutes","none"].includes(src.mode))out.mode=src.mode;
if(["none","optional","required"].includes(src.weight_mode))out.weight_mode=src.weight_mode;
if(["bilateral","unilateral"].includes(src.laterality))out.laterality=src.laterality;
if(["same","separate"].includes(src.sides_mode))out.sides_mode=src.sides_mode;
const sets=parseInt(src.sets||src.default_sets||0,10); out.sets=sets>0?Math.min(20,sets):null;
out.locked_sets=!!src.locked_sets; if(out.laterality==="bilateral")out.sides_mode="same"; return out;
}
function resultSchemaEditor(getSchema,setSchema,options={}){
const details=el("details",{class:"schema-editor"});
details.appendChild(el("summary",{text:options.title||"Ergebniserfassung"}));
const body=el("div",{class:"schema-editor-body"}); details.appendChild(body);
function draw(){
body.innerHTML=""; const active=!!getSchema();
const toggle=el("label",{class:"schema-toggle"});
const cb=el("input",{type:"checkbox"}); cb.checked=active;
cb.addEventListener("change",()=>{setSchema(cb.checked?freshResultSchema():null);draw();});
toggle.appendChild(cb); toggle.appendChild(document.createTextNode(options.toggleText||(options.stage?"Für diese Progressionsstufe eigenes Format verwenden":"Festes Standardformat für diese Übung verwenden")));
body.appendChild(toggle);
if(!active){body.appendChild(el("div",{class:"schema-note",text:options.inheritText||(options.stage?"Ohne Override gilt zuerst der Übungsstandard, danach die automatische Erkennung.":"Ohne Standard leitet der Tracker das Format aus Übungsname, Cue, Progressionsstufe und Trainingsformat ab.")}));return;}
const schema=cleanResultSchema(getSchema()); setSchema(schema);
const grid=el("div",{class:"schema-grid"});
const mode=el("select",{}); [["auto","Automatisch je Stufe"],["reps","Wiederholungen"],["seconds","Sekunden"],["minutes","Minuten"],["none","Kein Messwert"]].forEach(([v,t])=>mode.appendChild(el("option",{value:v,text:t}))); mode.value=schema.mode; mode.addEventListener("change",()=>{schema.mode=mode.value;setSchema(schema);}); grid.appendChild(field("Messwert",mode));
const weight=el("select",{}); [["none","Kein Gewicht"],["optional","Gewicht optional"],["required","Gewicht erforderlich"]].forEach(([v,t])=>weight.appendChild(el("option",{value:v,text:t}))); weight.value=schema.weight_mode; weight.addEventListener("change",()=>{schema.weight_mode=weight.value;setSchema(schema);}); grid.appendChild(field("Gewicht",weight));
const lateral=el("select",{}); [["bilateral","Gemeinsamer Wert"],["unilateral","Einseitig / je Seite"]].forEach(([v,t])=>lateral.appendChild(el("option",{value:v,text:t}))); lateral.value=schema.laterality; lateral.addEventListener("change",()=>{schema.laterality=lateral.value;if(schema.laterality==="bilateral")schema.sides_mode="same";setSchema(schema);draw();}); grid.appendChild(field("Ausführung",lateral));
if(schema.laterality==="unilateral"){const sides=el("select",{}); [["same","Ein Wert gilt je Seite"],["separate","Links / rechts getrennt"]].forEach(([v,t])=>sides.appendChild(el("option",{value:v,text:t}))); sides.value=schema.sides_mode; sides.addEventListener("change",()=>{schema.sides_mode=sides.value;setSchema(schema);}); grid.appendChild(field("Seitenwerte",sides));}
const sets=el("input",{type:"number",min:"1",max:"20",value:schema.sets||"",placeholder:"automatisch"}); sets.addEventListener("input",()=>{const n=parseInt(sets.value||"0",10);schema.sets=n>0?Math.min(20,n):null;setSchema(schema);}); grid.appendChild(field("Werte / Sätze",sets));
const lockWrap=el("label",{class:"schema-toggle"}); const lock=el("input",{type:"checkbox"}); lock.checked=!!schema.locked_sets; lock.addEventListener("change",()=>{schema.locked_sets=lock.checked;setSchema(schema);}); lockWrap.appendChild(lock); lockWrap.appendChild(document.createTextNode("Anzahl im Tracker sperren")); const lockField=el("div",{}); lockField.appendChild(el("label",{text:"Anzahl"})); lockField.appendChild(lockWrap); grid.appendChild(lockField);
body.appendChild(grid); body.appendChild(el("div",{class:"schema-note",text:"Leere Satzanzahl bedeutet: aus Rotation, Arbeitssätzen oder festen Intervallen ableiten. Ein Stufen-Override hat im Tracker Vorrang vor dem Übungsstandard."}));
}
draw(); return details;
}
function exEditor(item,onDel){
const box=el("div",{class:"ex","data-exercise-instance-id":item.id||"","data-exercise-id":item.exercise_id||""});
box.appendChild(el("div",{class:"item-head"},[el("span",{class:"spacer"}),
el("button",{class:"mini danger",onClick:onDel},["×"])]));
box.appendChild(field("Übungsname", inp(item,"name")));
const g=el("div",{class:"grid2"});
const sel=el("select",{});
stageKeys().forEach(k=>sel.appendChild(el("option",{value:k,text:labelForKey(k)})));
sel.value=item.progression_id||item.key||""; sel.addEventListener("change",()=>{item.key=sel.value;item.progression_id=sel.value;});
g.appendChild(field("Progression", sel));
g.appendChild(field("Cue (kurzer Hinweis)", inp(item,"cue")));
box.appendChild(g);
const identity=el("details",{class:"schema-editor"}); identity.appendChild(el("summary",{text:"Stabile Identität & Analysecluster"}));
const identityBody=el("div",{class:"schema-editor-body"});
const ig=el("div",{class:"grid3"});
const idInput=inp(item,"id"); idInput.placeholder="wird beim Speichern erzeugt"; ig.appendChild(field("Übungsinstanz-ID",idInput));
const exerciseId=inp(item,"exercise_id"); exerciseId.placeholder="gemeinsame Übungs-ID"; ig.appendChild(field("Übungsbibliothek-ID",exerciseId));
const cluster=inp(item,"movement_cluster"); cluster.placeholder="z. B. horizontal_push"; ig.appendChild(field("Bewegungscluster",cluster));
identityBody.appendChild(ig); identity.appendChild(identityBody); box.appendChild(identity);
box.appendChild(resultSchemaEditor(()=>item.result_schema||null,value=>{if(value)item.result_schema=value;else delete item.result_schema;},{title:"Standard-Ergebniserfassung",toggleText:"Für diese Übung ein festes Standardformat speichern"}));
return box;
}
function setDayCount(n){
cfg.days=cfg.days||[]; cfg.prepost=cfg.prepost||{};
while(cfg.days.length<n){
const num=cfg.days.length+1;
cfg.days.push({num:num,focus:"Tag "+num,rotations:[{label:"ROTATION A — 3 Sätze",exercises:[]}]});
if(!cfg.prepost[String(num)]) cfg.prepost[String(num)]={warmup:"",cooldown:"",stretch:""};
}
while(cfg.days.length>n) cfg.days.pop();
cfg.days.forEach((d,i)=>d.num=i+1);
cfg.meta.days=cfg.days.length;
}
function renderTage(){
const p=panel("tage");
p.appendChild(card("Trainingstage","Beliebig viele Tage, je mit Fokus, optionaler Band-Farbe, Badge und Notiz. Rotationen und Übungen frei hinzufügbar/entfernbar. Leere Tage (ohne Übung) erzeugen keine Seite im PDF."));
const host=el("div",{});
function renumber(){ cfg.days.forEach((d,i)=>d.num=i+1); cfg.meta.days=cfg.days.length; }
function drawAll(){
host.innerHTML="";
(cfg.days||[]).forEach((day,di)=>{
const c=el("div",{class:"card"});
const badge=day.badge||(day.badge={text:"",color:"#6D28D9"});
const head=el("div",{class:"item-head"},[
el("span",{class:"daytag",style:"background:"+(day.color||"#C2410C"),text:"TAG "+day.num}),
el("span",{class:"spacer"}),
moveBtns(cfg.days,di,()=>{renumber();drawAll();renderWarmup();}),
el("button",{class:"mini danger",onClick:()=>{cfg.days.splice(di,1);renumber();drawAll();renderWarmup();}},["Tag löschen"])]);
c.appendChild(head);
c.appendChild(field("Fokus / Titel des Tages", inp(day,"focus")));
const g=el("div",{class:"grid3"});
g.appendChild(colorField("Band-Farbe (optional)", day, "color"));
g.appendChild(field("Badge-Text (optional)", inp(badge,"text")));
g.appendChild(colorField("Badge-Farbe", badge, "color"));
c.appendChild(g);
c.appendChild(field("Notiz-Kasten (optional, unter Warm-up)", inp(day,"note",{area:true,rows:2})));
const lc=el("label",{style:"display:block;margin:8px 0;font-size:13px;color:var(--mute);font-weight:600"});
const cb=el("input",{type:"checkbox"}); cb.checked=!!day.light;
cb.addEventListener("change",()=>day.light=cb.checked);
lc.appendChild(cb); lc.appendChild(document.createTextNode(" Leichter Tag (nutzt den „leichten“ Parametertext statt der Phasen-Parameter)"));
c.appendChild(lc);
// Optional: eigener Parameter-Text je Phase für diesen Tag
const pbpBox=el("div",{});
function drawPbp(){
pbpBox.innerHTML="";
const labels=phaseLabels();
const has=(day.params_by_phase||[]).some(x=>(x||"").trim());
const t=el("div",{class:"rotlabel",text:"Parameter-Text je Phase (optional überschreibt den Phasen-Text)"});
pbpBox.appendChild(t);
if(!has && !day._pbpOpen){
pbpBox.appendChild(el("button",{class:"mini",onClick:()=>{day._pbpOpen=true;day.params_by_phase=day.params_by_phase||labels.map(()=>"");drawPbp();}},["+ eigene Texte je Phase"]));
return;
}
day.params_by_phase=day.params_by_phase||[];
while(day.params_by_phase.length<labels.length) day.params_by_phase.push("");
labels.forEach((lab,i)=>{
const e=el("textarea",{value:day.params_by_phase[i]||""}); e.rows=2;
e.addEventListener("input",()=>day.params_by_phase[i]=e.value);
pbpBox.appendChild(field(lab, e));
});
pbpBox.appendChild(el("button",{class:"mini danger",onClick:()=>{delete day.params_by_phase;day._pbpOpen=false;drawPbp();}},["eigene Texte entfernen"]));
}
drawPbp(); c.appendChild(pbpBox);
day.rotations=day.rotations||[];
const rotHost=el("div",{});
function drawRot(){
rotHost.innerHTML="";
day.rotations.forEach((rot,ri)=>{
rot.exercises=rot.exercises||[];
const rc=el("div",{class:"item"});
rc.appendChild(el("label",{text:"Rotations-Titel"}));
rc.appendChild(el("div",{class:"item-head"},[
el("div",{style:"flex:1"},[inp(rot,"label")]),
moveBtns(day.rotations,ri,drawRot),
el("button",{class:"mini danger",onClick:()=>{day.rotations.splice(ri,1);drawRot();}},["Rotation ×"])]));
rot.exercises.forEach((it,ii)=>rc.appendChild(exEditor(it,()=>{rot.exercises.splice(ii,1);drawRot();})));
rc.appendChild(el("button",{class:"mini flex-add",onClick:()=>{rot.exercises.push({name:"",key:"",cue:""});drawRot();}},["+ Übung"]));
rotHost.appendChild(rc);
});
rotHost.appendChild(el("button",{class:"mini flex-add",onClick:()=>{day.rotations.push({label:"ROTATION "+String.fromCharCode(65+day.rotations.length)+" — 3 Sätze",exercises:[]});drawRot();}},["+ Rotation"]));
}
drawRot(); c.appendChild(rotHost);
host.appendChild(c);
});
host.appendChild(el("button",{class:"flex-add",onClick:()=>{const num=cfg.days.length+1;cfg.days.push({num:num,focus:"Tag "+num,rotations:[{label:"ROTATION A — 3 Sätze",exercises:[]}]});if(!cfg.prepost[String(num)])cfg.prepost[String(num)]={warmup:"",cooldown:"",stretch:""};renumber();drawAll();renderWarmup();}},["+ Tag hinzufügen"]));
}
drawAll(); p.appendChild(host);
}
// ---- PROGRESSIONSSTUFEN ----
function phaseLabels(){
const items=(cfg.phases&&cfg.phases.items)||[];
let start=1; const out=[];
items.forEach((ph,i)=>{
const w=parseInt(ph.weeks||0,10)||0;
const end=start+Math.max(0,w-1);
out.push((ph.name||("Phase "+(i+1)))+(w>0?(" · Woche "+start+(end>start?(""+end):"")):""));
start=end+1;
});
return out.length?out:["Phase 1"];
}
function renderStufen(){
const p=panel("stufen");
const c=card("Progressionsstufen","Jede Stufe ist ein eigenes Objekt mit stabiler ID, Ergebnisformat, Analysecluster und Faktor. Dadurch bleiben Stufe und Eingabemaske auch beim Verschieben fest verbunden.");
const host=el("div",{});
function draw(){
host.innerHTML="";
const labels=phaseLabels(); const phases=(cfg.phases&&cfg.phases.items)||[];
Object.keys(cfg.stages||{}).forEach(key=>{
const st=cfg.stages[key]; st.key=st.key||key; st.steps=Array.isArray(st.steps)?st.steps:[];
while(st.steps.length<labels.length) st.steps.push({id:"",name:"",phase_id:phases[st.steps.length]?.id||"",result_schema:null,factor:1,movement_cluster:"general"});
const box=el("div",{class:"item","data-progression-key":key,"data-progression-id":st.id||""});
box.appendChild(el("div",{class:"item-head"},[el("span",{class:"n",text:key}),el("span",{class:"spacer"}),el("button",{class:"mini danger",onClick:()=>{if(confirm('Progression „'+(st.name||"?")+'“ löschen?')){delete cfg.stages[key];draw();}}},["×"])]));
const headGrid=el("div",{class:"grid3"}); headGrid.appendChild(field("Übungsname",inp(st,"name"))); headGrid.appendChild(field("Progressions-ID",inp(st,"id"))); headGrid.appendChild(field("Schlüssel",inp(st,"key"))); box.appendChild(headGrid);
const g=el("div",{class:"grid2"});
labels.forEach((label,i)=>{
let step=st.steps[i]; if(typeof step!=="object"||!step){step={id:"",name:String(step||""),phase_id:phases[i]?.id||"",result_schema:null,factor:1,movement_cluster:"general"};st.steps[i]=step;}
const cell=el("div",{class:"phase-schema-cell"});
cell.appendChild(field(label,inp(step,"name")));
const sg=el("div",{class:"grid3"}); sg.appendChild(field("Stufen-ID",inp(step,"id"))); sg.appendChild(field("Cluster",inp(step,"movement_cluster"))); const factor=inp(step,"factor",{type:"number"});factor.step="0.01";factor.min="0.01";sg.appendChild(field("Analysefaktor",factor));cell.appendChild(sg);
cell.appendChild(resultSchemaEditor(()=>step.result_schema||null,value=>{if(value)step.result_schema=value;else delete step.result_schema;},{stage:true,title:"Ergebnisformat dieser Stufe",toggleText:"Übungsstandard für diese Stufe überschreiben"}));
g.appendChild(cell);
});
box.appendChild(g); host.appendChild(box);
});
host.appendChild(el("button",{class:"mini flex-add",onClick:()=>{let i=1,k="prog"+i;while(cfg.stages[k]){i++;k="prog"+i;}cfg.stages[k]={id:"",key:k,name:"Neue Übung",steps:labels.map((_,idx)=>({id:"",name:"",phase_id:phases[idx]?.id||"",factor:1,movement_cluster:"general"}))};draw();}},["+ Neue Progression"]));
}
draw(); c.appendChild(host); p.appendChild(c);
}
// ---- PROGRESSION (Phasen) ----
function renderPhasen(){
const p=panel("phasen"); const ph=cfg.phases; ph.items=ph.items||[];
const c=card("Progressions-Phasen","Beliebig viele Phasen. Jede hat Name (Kopfband), Parameter-Text (Kasten unter den Übungen) und Dauer in Wochen. Die Wochen werden fortlaufend auf die Gesamt-Wochen verteilt.");
const host=el("div",{});
function draw(){
host.innerHTML="";
let start=1;
ph.items.forEach((it,i)=>{
const w=parseInt(it.weeks||0,10)||0;
const end=start+Math.max(0,w-1);
const box=el("div",{class:"item"});
box.appendChild(el("div",{class:"item-head"},[
el("span",{class:"n",text:"Phase "+(i+1)+(w>0?(" · Woche "+start+(end>start?(""+end):"")):"")}),
el("span",{class:"spacer"}),
moveBtns(ph.items,i,draw),
el("button",{class:"mini danger",onClick:()=>{ph.items.splice(i,1);draw();}},["×"])]));
const g=el("div",{class:"grid2"});
g.appendChild(field("Name", inp(it,"name")));
g.appendChild(field("Dauer (Wochen)", inp(it,"weeks",{type:"number"})));
box.appendChild(g);
box.appendChild(field("Parameter-Text", inp(it,"params",{area:true,rows:3})));
host.appendChild(box);
start=end+1;
});
host.appendChild(el("button",{class:"mini flex-add",onClick:()=>{ph.items.push({name:"Phase "+(ph.items.length+1),params:"",weeks:2});draw();}},["+ Phase hinzufügen"]));
}
draw(); c.appendChild(host);
c.appendChild(field("Text für „leichten“ Tag", inp(ph,"param_light",{area:true,rows:2})));
p.appendChild(c);
}
// ---- WARM-UP / COOL-DOWN ----
function renderWarmup(){
const p=panel("warmup");
const c=card("Warm-up / Cool-down / Dehnen","Pro Trainingstag. Warm-up steht über den Übungen, Cool-down + Dehnen darunter.");
(cfg.days||[]).forEach(day=>{
const k=String(day.num);
cfg.prepost=cfg.prepost||{};
const pp=cfg.prepost[k]||(cfg.prepost[k]={warmup:"",cooldown:"",stretch:""});
const box=el("div",{class:"item"});
box.appendChild(el("div",{class:"item-head"},[
el("span",{class:"daytag",style:"background:"+(day.color||"#C2410C"),text:"TAG "+day.num}),
el("span",{class:"muted",style:"margin-left:8px",text:day.focus||""})]));
box.appendChild(field("Warm-up", inp(pp,"warmup",{area:true,rows:2})));
box.appendChild(field("Cool-down", inp(pp,"cooldown",{area:true,rows:2})));
box.appendChild(field("Dehnen", inp(pp,"stretch",{area:true,rows:3})));
c.appendChild(box);
});
p.appendChild(c);
}
function updateRevisionState(){
const el=document.getElementById("revisionState"); if(!el)return;
el.textContent=`Entwurf R${planRevision} · veröffentlicht R${publishedRevision}`;
el.className="revision-pill"+(hasUnpublishedChanges||dirty?" draft":"");
document.getElementById("btnPublish").disabled=!(hasUnpublishedChanges||dirty);
}
function validationMarkup(result){
const errors=result?.errors||[], warnings=result?.warnings||[];
if(!errors.length&&!warnings.length)return '<div class="validation-item">Keine Fehler oder Warnungen. Der Plan ist veröffentlichungsbereit.</div>';
return [...errors.map(x=>({...x,severity:'error'})),...warnings.map(x=>({...x,severity:'warning'}))].map(x=>`<div class="validation-item ${x.severity}"><strong>${x.severity==='error'?'Fehler':'Hinweis'} · ${x.location||'Plan'}</strong><div>${x.message||''}</div></div>`).join('');
}
function diffMarkup(diff){
if(!diff)return '<div class="validation-item muted">Noch kein Änderungsbericht. Plan prüfen, um ihn zu erzeugen.</div>';
const names={days:'Trainingstage',rotations:'Blöcke',exercises:'Übungen',progression_steps:'Progressionsstufen'};
const rows=Object.keys(names).map(key=>{const v=diff[key]||{};return `<tr><td>${names[key]}</td><td>${v.before||0}</td><td>${v.after||0}</td><td>+${v.added||0} / ${v.removed||0} / Δ${v.changed||0}</td></tr>`;}).join('');
const labels=(diff.changes||[]).slice(0,12).map(x=>`<div class="validation-item"><strong>${x.kind==='added'?'Hinzugefügt':x.kind==='removed'?'Entfernt':'Geändert'} · ${x.label||x.id}</strong><div class="muted">${x.section||''} · ${x.id||''}</div></div>`).join('');
const extra=[];if(diff.training_format_changed)extra.push('Trainingsformat geändert');if(diff.plan_meta_changed)extra.push('Plan-Metadaten geändert');if(diff.tracked_sessions)extra.push(`${diff.tracked_sessions} vorhandene Tracker-Sessions bleiben ihrer bisherigen Planrevision zugeordnet`);
return `<div class="table-scroll"><table><thead><tr><th>Bereich</th><th>Veröffentlicht</th><th>Entwurf</th><th>Änderungen</th></tr></thead><tbody>${rows}</tbody></table></div>${extra.length?`<div class="validation-item warning">${extra.join(' · ')}</div>`:''}${labels||'<div class="validation-item">Keine inhaltlichen Änderungen zur veröffentlichten Fassung.</div>'}${(diff.total_changes||0)>12?`<div class="muted">Weitere ${(diff.total_changes||0)-12} Änderungen werden beim Veröffentlichen ebenfalls berücksichtigt.</div>`:''}`;
}
async function validateCfg(showToast=true){
const res=await fetch(BT.url("/api/plans/")+encodeURIComponent(activePlanId)+"/validate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({config:payloadOf()})});
const data=await res.json(); window._lastValidation=data; renderPruefung(); if(showToast)toast(data.errors?.length?`${data.errors.length} Fehler gefunden`:`Plan geprüft · ${data.warnings?.length||0} Hinweise`,!!data.errors?.length); return data;
}
async function loadProposals(){
try{const res=await fetch(BT.url("/api/plans/")+encodeURIComponent(activePlanId)+"/proposals");window._planProposals=await res.json();}catch(_){window._planProposals={};}
}
async function proposalStatus(id,status){
await fetch(BT.url("/api/plans/")+encodeURIComponent(activePlanId)+"/proposals/"+encodeURIComponent(id),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({status})});await loadProposals();renderPruefung();
}
function openProposalTarget(item){
const progression=String(item.target_progression_id||"");
const exercise=String(item.target_exercise_id||"");
let tab=progression?"stufen":"tage";selectTab(tab);
requestAnimationFrame(()=>{
const selector=progression?`[data-progression-key="${CSS.escape(progression)}"],[data-progression-id="${CSS.escape(progression)}"]`:`[data-exercise-id="${CSS.escape(exercise)}"],[data-exercise-instance-id="${CSS.escape(exercise)}"]`;
const target=document.querySelector(selector);
if(!target){toast("Ziel-ID ist im aktuellen Entwurf nicht eindeutig auffindbar",true);return;}
target.classList.add("proposal-target");target.scrollIntoView({behavior:"smooth",block:"center"});setTimeout(()=>target.classList.remove("proposal-target"),2600);
});
}
function resultSchemaText(schema){
if(!schema||typeof schema!=="object")return "automatische Erkennung";
const mode=({reps:"Wiederholungen",seconds:"Sekunden",minutes:"Minuten",none:"keine Zahl",auto:"automatisch"})[schema.mode||"auto"]||schema.mode;
const weight=({none:"ohne Gewicht",optional:"Gewicht optional",required:"Gewicht erforderlich"})[schema.weight_mode||"none"];
const side=schema.laterality==="unilateral"?(schema.sides_mode==="separate"?"links/rechts getrennt":"je Seite gleicher Wert"):"gemeinsamer Wert";
const sets=schema.sets?`${schema.sets} Werte${schema.locked_sets===false?" (änderbar)":" (fest)"}`:"Anzahl aus Block/Format";
return `${mode} · ${weight} · ${side} · ${sets}`;
}
function trackerPreviewCard(){
const c=card("Tracker-Vorschau","So interpretiert der Session-Tracker die veröffentlichte Datenstruktur. Stufenschemata überschreiben den Übungsstandard.");
const fmt=cfg.training_format||{};
c.appendChild(el("div",{class:"validation-item",text:`Format: ${fmt.mode||"auto"}${fmt.fixed_interval?` · ${fmt.work_seconds||"?"} s Arbeit / ${fmt.rest_seconds||"?"} s Pause · ${fmt.rounds||"?"} Intervalle pro ${fmt.rounds_scope||"Block"}`:""}`}));
const table=el("table",{});table.appendChild(el("thead",{},[el("tr",{},[el("th",{text:"Übung"}),el("th",{text:"Standard"}),el("th",{text:"Stufenabhängige Abweichungen"})])]));
const body=el("tbody",{});const seen=new Set();
(cfg.days||[]).forEach(day=>(day.rotations||[]).forEach(rot=>(rot.exercises||[]).forEach(ex=>{
const identity=ex.exercise_id||ex.progression_id||ex.key||ex.name;if(seen.has(identity))return;seen.add(identity);
const stage=(cfg.stages||{})[ex.progression_id||ex.key];
const variants=(stage?.steps||[]).filter(step=>step&&step.result_schema).map(step=>`${step.name}: ${resultSchemaText(step.result_schema)}`);
body.appendChild(el("tr",{},[el("td",{},[el("strong",{text:ex.name||"Übung"}),el("div",{class:"muted",text:ex.exercise_id||identity})]),el("td",{text:resultSchemaText(ex.result_schema)}),el("td",{text:variants.length?variants.join(" | "):"keine Überschreibung"})]));
})));
table.appendChild(body);const wrap=el("div",{class:"table-scroll"},[table]);c.appendChild(wrap);
if(fmt.fixed_interval&&fmt.rounds){
const notes=[];(cfg.days||[]).forEach(day=>(day.rotations||[]).forEach(rot=>{const count=(rot.exercises||[]).length;if(count)notes.push(`${day.focus||"Tag "+day.num} · ${rot.label||"Block"}: ${fmt.rounds} Intervalle ÷ ${count} Übungen = ${fmt.rounds%count===0?fmt.rounds/count+" Durchgänge je Übung":"nicht gleichmäßig, feste Wertezahl erforderlich"}`);}));
c.appendChild(el("div",{class:"validation-list"},notes.map(text=>el("div",{class:"validation-item",text}))));
}
return c;
}
function renderPruefung(){
const p=panel("pruefung"); const validation=window._lastValidation||{errors:[],warnings:[]};
const vc=card("Planprüfung & Tracker-Vorschau","Die Prüfung kontrolliert IDs, Progressionsbezüge, Ergebnisschemata und feste Intervalle. Veröffentlicht wird nur ein fehlerfreier Entwurf.");
vc.appendChild(el("div",{class:"row"},[el("button",{class:"primary",onClick:()=>validateCfg()},["Jetzt prüfen"]),el("button",{onClick:()=>publishCfg()},["Entwurf veröffentlichen"])]));
const list=el("div",{class:"validation-list",html:validationMarkup(validation)});vc.appendChild(list);p.appendChild(vc);
const dc=card("Änderungen zur veröffentlichten Fassung","Stabile IDs ermöglichen einen belastbaren Vergleich. Alte Sessions bleiben ihrer gespeicherten Planrevision zugeordnet.");
dc.appendChild(el("div",{html:diffMarkup(validation.diff)}));p.appendChild(dc);
p.appendChild(trackerPreviewCard());
const pc=card("KI-Vorschläge aus dem Tracker","Vorschläge werden strukturiert mit stabilen Ziel-IDs übergeben. Sie verändern den Entwurf niemals automatisch.");
const proposals=(window._planProposals&&window._planProposals.proposals)||[];
if(!proposals.length)pc.appendChild(el("div",{class:"muted",text:"Keine offenen Vorschläge vorhanden."}));
proposals.forEach(item=>{const box=el("div",{class:"proposal-card"});box.appendChild(el("strong",{text:item.target_label||item.target||"Plananpassung"}));box.appendChild(el("p",{text:item.suggested_change||item.change||""}));if(item.reason)box.appendChild(el("div",{class:"muted",text:"Warum: "+item.reason}));if(item.condition)box.appendChild(el("div",{class:"muted",text:"Bedingung: "+item.condition}));if(item.manual_step)box.appendChild(el("div",{class:"muted",text:"Im Editor: "+item.manual_step}));const ids=[item.target_exercise_id&&`Übung ${item.target_exercise_id}`,item.target_progression_id&&`Progression ${item.target_progression_id}`,item.target_step_id&&`Stufe ${item.target_step_id}`].filter(Boolean);if(ids.length)box.appendChild(el("div",{class:"tiny muted",text:ids.join(" · ")}));box.appendChild(el("div",{class:"proposal-actions"},[el("button",{class:"mini",onClick:()=>openProposalTarget(item)},["Ziel im Entwurf öffnen"]),el("button",{class:"mini",onClick:()=>proposalStatus(item.id,"accepted")},["Manuell übernommen"]),el("button",{class:"mini danger",onClick:()=>proposalStatus(item.id,"rejected")},["Ablehnen"]),el("span",{class:"muted",text:item.status||"open"})]));pc.appendChild(box);});p.appendChild(pc);
}
async function publishCfg(){
if(dirty)await saveCfg();
const validation=await validateCfg(false);if(validation.errors?.length){toast("Veröffentlichung wegen Validierungsfehlern gestoppt",true);return;}
const impact=trackedSessions?` Bereits ${trackedSessions} dokumentierte Session${trackedSessions===1?"":"s"} bleiben mit ihrer bisherigen Planrevision gekennzeichnet.`:"";
if(!confirm("Diesen Entwurf als neue veröffentlichte Planrevision freigeben? Der Tracker liest anschließend diese Fassung."+impact))return;
const res=await fetch(BT.url("/api/plans/")+encodeURIComponent(activePlanId)+"/publish",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({expected_revision:planRevision})});
const data=await res.json();if(!res.ok){toast(data.error||"Veröffentlichen fehlgeschlagen",true);return;}
planRevision=data.revision;publishedRevision=data.published_revision;hasUnpublishedChanges=data.has_unpublished_changes;trackedSessions=data.tracked_sessions||trackedSessions;cfg=decodeConfig(data.config);dirty=false;updateRevisionState();renderAll();toast("Plan veröffentlicht ✓");
}
// ============================================================
// HELFER (Farbe, Verschieben) + REZEPT-EDITOREN
// ============================================================
function colorField(labelText,obj,key){
const wrap=el("div",{});
wrap.appendChild(el("label",{text:labelText}));
const box=el("div",{class:"colorbox"});
const col=el("input",{type:"color",value:/^#[0-9a-fA-F]{6}$/.test(obj[key]||"")?obj[key]:"#334155"});
const txt=el("input",{type:"text",value:obj[key]||""});
col.addEventListener("input",()=>{obj[key]=col.value.toUpperCase();txt.value=obj[key];});
txt.addEventListener("input",()=>{obj[key]=txt.value; if(/^#[0-9a-fA-F]{6}$/.test(txt.value))col.value=txt.value;});
box.appendChild(col); box.appendChild(txt); wrap.appendChild(box); return wrap;
}
function moveBtns(arr,i,redraw){
return el("span",{},[
el("button",{class:"mini",title:"nach oben",onClick:()=>{if(i>0){arr.splice(i-1,0,arr.splice(i,1)[0]);redraw();}}},["▲"]),
el("button",{class:"mini",title:"nach unten",onClick:()=>{if(i<arr.length-1){arr.splice(i+1,0,arr.splice(i,1)[0]);redraw();}}},["▼"]),
]);
}
// ---- REZEPT: Kopf & Intro ----
function renderKopf(){
const p=panel("kopf"); const m=cfg.meta, it=cfg.intro||(cfg.intro={});
const c1=card("Kopf & Datei");
c1.appendChild(field("Titel", inp(m,"title")));
c1.appendChild(field("Kopfzeile oben (jede Seite)", inp(m,"header_left")));
c1.appendChild(field("Fußzeile unten links", inp(m,"footer_left")));
const g=el("div",{class:"grid2"});
g.appendChild(field("Dateiname (PDF)", inp(m,"output_name")));
g.appendChild(colorField("Markenfarbe (Kopf)", m, "brand_color"));
c1.appendChild(g);
const g2=el("div",{class:"grid3"});
g2.appendChild(field("Kopfzeile rechts", inp(m,"header_right")));
g2.appendChild(field("Schritt-Label", inp(m,"steps_label")));
g2.appendChild(field("Tipp-Label (Fallback)", inp(m,"tip_label")));
c1.appendChild(g2);
p.appendChild(c1);
const c2=card("Intro / Vorspann (Seite 1)");
c2.appendChild(field("Überschrift", inp(it,"headline")));
c2.appendChild(field("Untertitel", inp(it,"subtitle",{area:true,rows:2})));
c2.appendChild(el("h3",{text:"Absätze"}));
c2.appendChild(listEditor(it.paragraphs||(it.paragraphs=[]),"Absatz"));
c2.appendChild(field("Regel-Überschrift", inp(it,"rule_head")));
c2.appendChild(field("Regel-Text", inp(it,"rule_text",{area:true,rows:2})));
c2.appendChild(field("Kategorien-Überschrift", inp(it,"categories_head")));
p.appendChild(c2);
}
// ---- REZEPT: Kategorien ----
function renderKategorien(){
const p=panel("kategorien");
const c=card("Kategorien","Name + Akzentfarbe. Reihenfolge = Reihenfolge im PDF. Der Name muss exakt mit der Kategorie in den Rezepten übereinstimmen.");
const host=el("div",{});
function draw(){
host.innerHTML="";
cfg.categories.forEach((cat,i)=>{
const row=el("div",{class:"item"});
row.appendChild(el("div",{class:"item-head"},[el("span",{class:"n",text:"#"+(i+1)}),el("span",{class:"spacer"}),
moveBtns(cfg.categories,i,draw),
el("button",{class:"mini danger",onClick:()=>{cfg.categories.splice(i,1);draw();}},["×"])]));
const g=el("div",{class:"grid2"});
g.appendChild(field("Name", inp(cat,"name")));
g.appendChild(colorField("Farbe", cat, "color"));
row.appendChild(g); host.appendChild(row);
});
host.appendChild(el("button",{class:"mini flex-add",onClick:()=>{cfg.categories.push({name:"Neue Kategorie",color:"#334155"});draw();}},["+ Kategorie"]));
}
draw(); c.appendChild(host); p.appendChild(c);
}
// ---- REZEPT: Rezepte ----
function catSelect(r){
const sel=el("select",{});
const names=cfg.categories.map(x=>x.name);
if(r.category && !names.includes(r.category)) names.unshift(r.category);
names.forEach(n=>sel.appendChild(el("option",{value:n,text:n})));
sel.value=r.category||""; sel.addEventListener("change",()=>r.category=sel.value);
return sel;
}
function renderRezepte(){
const p=panel("rezepte");
const c=card(cfg.recipes.length+" Rezepte","Kategorie per Dropdown. Zutaten mit „ · “ trennen. Schritte als Freitext (bei nummerierten Schritten je Schritt eine Zeile).");
const filter=el("input",{type:"text",placeholder:"Filtern nach Titel oder Kategorie…"});
filter.style.marginBottom="10px";
c.appendChild(filter);
const host=el("div",{});
function draw(){
host.innerHTML="";
cfg.recipes.forEach((r,i)=>{
const box=el("div",{class:"item"});
box.dataset.search=((r.title||"")+" "+(r.category||"")).toLowerCase();
box.appendChild(el("div",{class:"item-head"},[
el("span",{class:"n",text:"#"+(r.num||(i+1))}),
el("span",{class:"spacer"}),
moveBtns(cfg.recipes,i,draw),
el("button",{class:"mini danger",onClick:()=>{cfg.recipes.splice(i,1);draw();}},["Rezept löschen"])]));
const g1=el("div",{class:"grid3"});
g1.appendChild(field("Nr.", inp(r,"num")));
g1.appendChild(field("Kategorie", catSelect(r)));
g1.appendChild(field("Badge (z.B. AIRFRYER)", inp(r,"badge")));
box.appendChild(g1);
box.appendChild(field("Titel", inp(r,"title")));
box.appendChild(field("Untertitel", inp(r,"tagline")));
box.appendChild(field("Nährwerte (leer = kein Strip)", inp(r,"nutrition",{area:true,rows:2})));
box.appendChild(field("Zutaten", inp(r,"ingredients",{area:true,rows:2})));
box.appendChild(field("Zubereitung", inp(r,"steps",{area:true,rows:3})));
box.appendChild(field("Zubereitung Airfryer/Ofen (optional)", inp(r,"methods")));
box.appendChild(field("Ergibt / Portionen (optional)", inp(r,"yield")));
box.appendChild(el("h3",{text:"Variationen"}));
box.appendChild(listEditor(r.variations||(r.variations=[]),"Variation"));
box.appendChild(field("Tipp", inp(r,"tip",{area:true,rows:2})));
host.appendChild(box);
});
host.appendChild(el("button",{class:"flex-add",onClick:()=>{
const lastCat=cfg.recipes.length?cfg.recipes[cfg.recipes.length-1].category:(cfg.categories[0]||{}).name||"";
cfg.recipes.push({num:String(cfg.recipes.length+1),category:lastCat,title:"",tagline:"",nutrition:"",
ingredients:"",steps:"",methods:"",variations:[],tip:"",badge:"",yield:""}); draw();
}},["+ Rezept hinzufügen"]));
}
filter.addEventListener("input",()=>{
const q=filter.value.toLowerCase().trim();
host.querySelectorAll(".item").forEach(it=>{ it.style.display = (!q||it.dataset.search.includes(q))?"":"none"; });
});
draw(); c.appendChild(host); p.appendChild(c);
}
async function generatePdf(){
const btn=document.getElementById("btnPdf"); btn.disabled=true; const old=btn.textContent; btn.textContent="Generiere…";
try{
const payload=payloadOf();
const res=await fetch(BT.url("/api/pdf"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});
if(!res.ok){ const e=await res.json().catch(()=>({error:"Fehler"})); throw new Error(e.error||("HTTP "+res.status)); }
const blob=await res.blob();
const url=URL.createObjectURL(blob);
const a=el("a",{href:url,download:cfg.meta.output_name||"Trainingsplan.pdf"}); document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url); toast("PDF erstellt ✓");
}catch(err){ toast("PDF-Fehler: "+err.message,true); }
finally{ btn.disabled=false; btn.textContent=old; }
}
async function saveCfg(){
try{
const payload=payloadOf();
const res=await fetch(BT.url("/api/plans/")+encodeURIComponent(activePlanId),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({config:payload,expected_revision:planRevision})});
const data=await res.json();
if(!res.ok){if(res.status===409)throw new Error("Konflikt: Plan wurde in einem anderen Tab geändert. Neu laden.");throw new Error(data.error||("HTTP "+res.status));}
planRevision=data.revision;publishedRevision=data.published_revision;hasUnpublishedChanges=data.has_unpublished_changes;trackedSessions=data.tracked_sessions||trackedSessions;cfg=decodeConfig(data.config);window._lastValidation=data.validation||window._lastValidation;dirty=false;updateRevisionState();toast("Entwurf gespeichert ✓");
}catch(err){ toast("Speichern fehlgeschlagen: "+err.message,true); }
}
async function resetCfg(){
if(!confirm("Wirklich alle Änderungen verwerfen und Standard laden?"))return;
const res=await fetch(BT.url("/api/reset"),{method:"POST"});
const data=await res.json(); cfg=decodeConfig(data.config); planRevision=data.revision; publishedRevision=data.published_revision; hasUnpublishedChanges=data.has_unpublished_changes; trackedSessions=data.tracked_sessions||0; dirty=false; updateRevisionState(); renderAll(); toast("Entwurf auf Standard zurückgesetzt");
}
// ============================================================
// PHASEN-MANAGER
// ============================================================
let activePlanId=null;
let dirty=false;
function markDirty(){ dirty=true; updateRevisionState(); }
function confirmDiscard(){ return !dirty || confirm("Diese Phase hat ungespeicherte Änderungen. Verwerfen?"); }
async function loadPlans(){
const res=await fetch(BT.url("/api/plans"));
const data=await res.json();
activePlanId=data.active;
const sel=document.getElementById("planSelect");
sel.innerHTML="";
data.plans.forEach(p=>{
const o=el("option",{value:p.id,text:p.name}); if(p.id===data.active)o.selected=true; sel.appendChild(o);
});
document.getElementById("btnPlanDel").disabled = data.plans.length<=1;
}
function applyLoadedPlan(data){
activePlanId=data.id; planRevision=data.revision||1; publishedRevision=data.published_revision||1; hasUnpublishedChanges=!!data.has_unpublished_changes; trackedSessions=data.tracked_sessions||0;
cfg=decodeConfig(data.config); dirty=false; window._lastValidation={errors:[],warnings:[]};
updateRevisionState(); loadProposals().then(()=>renderPruefung()); renderAll();
const sel=document.getElementById("planSelect"); if(sel) sel.value=data.id;
}
async function switchPlan(id){
if(id===activePlanId)return;
if(!confirmDiscard()){ document.getElementById("planSelect").value=activePlanId; return; }
const res=await fetch(BT.url("/api/plans/")+encodeURIComponent(id)+"/select",{method:"POST"});
applyLoadedPlan(await res.json());
toast("Phase geladen: "+id);
}
async function newPlan(){
if(!confirmDiscard())return;
const isRecipe=confirm("Neue Phase als REZEPTSAMMLUNG anlegen?\n\nOK = Rezepte\nAbbrechen = Trainingsplan");
const kind=isRecipe?"recipe":"training";
const name=prompt("Name der neuen Phase:", isRecipe?"Neue Rezepte":"Phase "+(document.getElementById("planSelect").options.length+1));
if(!name)return;
const res=await fetch(BT.url("/api/plans"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name,kind})});
const data=await res.json(); await loadPlans(); applyLoadedPlan(data); toast("Neue Phase angelegt");
}
async function dupPlan(){
if(!confirmDiscard())return;
const cur=document.getElementById("planSelect");
const curName=cur.options[cur.selectedIndex]?cur.options[cur.selectedIndex].text:"Phase";
const name=prompt("Name der Kopie:",curName+" (Kopie)");
if(!name)return;
const res=await fetch(BT.url("/api/plans"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name,from:activePlanId})});
const data=await res.json(); await loadPlans(); applyLoadedPlan(data); toast("Phase dupliziert");
}
async function renamePlan(){
const cur=document.getElementById("planSelect");
const curName=cur.options[cur.selectedIndex]?cur.options[cur.selectedIndex].text:"";
const name=prompt("Neuer Name:",curName);
if(!name||name===curName)return;
await fetch(BT.url("/api/plans/")+encodeURIComponent(activePlanId)+"/rename",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name})});
await loadPlans(); toast("Umbenannt");
}
async function deletePlan(){
const cur=document.getElementById("planSelect");
const curName=cur.options[cur.selectedIndex]?cur.options[cur.selectedIndex].text:activePlanId;
if(!confirm("Phase \""+curName+"\" wirklich löschen? Das kann nicht rückgängig gemacht werden."))return;
const res=await fetch(BT.url("/api/plans/")+encodeURIComponent(activePlanId),{method:"DELETE"});
if(!res.ok){ const e=await res.json().catch(()=>({})); toast(e.error||"Löschen fehlgeschlagen",true); return; }
const d=await res.json(); await loadPlans();
const sres=await fetch(BT.url("/api/plans/")+encodeURIComponent(d.active)+"/select",{method:"POST"});
applyLoadedPlan(await sres.json()); toast("Phase gelöscht");
}
function exportPlan(){
window.location.href=BT.url("/api/plans/")+encodeURIComponent(activePlanId)+"/export";
}
async function importPlan(file){
const fd=new FormData(); fd.append("file",file);
const res=await fetch(BT.url("/api/plans/import"),{method:"POST",body:fd});
if(!res.ok){ const e=await res.json().catch(()=>({})); toast(e.error||"Import fehlgeschlagen",true); return; }
const data=await res.json(); await loadPlans(); applyLoadedPlan(data); toast("Phase importiert: "+data.name);
}
// ============================================================
// INIT
// ============================================================
async function init(){
buildTabs();
await loadPlans();
const res=await fetch(BT.url("/api/config"));
const initial=await res.json(); activePlanId=initial.id||activePlanId; planRevision=initial.revision||1; publishedRevision=initial.published_revision||1; hasUnpublishedChanges=!!initial.has_unpublished_changes; trackedSessions=initial.tracked_sessions||0; cfg=decodeConfig(initial.config||initial); updateRevisionState(); await loadProposals(); renderAll();
document.getElementById("btnPdf").addEventListener("click",generatePdf);
document.getElementById("btnValidate").addEventListener("click",()=>validateCfg());
document.getElementById("btnSave").addEventListener("click",saveCfg);
document.getElementById("btnPublish").addEventListener("click",publishCfg);
document.getElementById("btnReset").addEventListener("click",resetCfg);
document.getElementById("planSelect").addEventListener("change",e=>switchPlan(e.target.value));
document.getElementById("btnPlanNew").addEventListener("click",newPlan);
document.getElementById("btnPlanDup").addEventListener("click",dupPlan);
document.getElementById("btnPlanRen").addEventListener("click",renamePlan);
document.getElementById("btnPlanDel").addEventListener("click",deletePlan);
document.getElementById("btnPlanExport").addEventListener("click",exportPlan);
document.getElementById("btnPlanImport").addEventListener("click",()=>document.getElementById("planFile").click());
document.getElementById("planFile").addEventListener("change",e=>{ if(e.target.files[0]){importPlan(e.target.files[0]); e.target.value="";} });
// Änderungen an Eingabefeldern markieren die Phase als ungespeichert
const mainEl=document.querySelector("main");
mainEl.addEventListener("input",markDirty);
mainEl.addEventListener("click",e=>{ if(e.target.closest("button"))markDirty(); });
window.addEventListener("beforeunload",e=>{ if(dirty){ e.preventDefault(); e.returnValue=""; } });
}
init();
</script>
</body>
</html>