1119 lines
68 KiB
HTML
1119 lines
68 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>
|
||
/* ------------------------------------------------------------------
|
||
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- & 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><b>…</b></b> = fett, <code><br/></code> = Zeilenumbruch,
|
||
<code><font color='#6D28D9'>…</font></code> = Farbe. Ein kaufmännisches Und tippst du normal als <code>&</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={"ü":"ü","Ü":"Ü","ä":"ä","Ä":"Ä","ö":"ö","Ö":"Ö",
|
||
"ß":"ß","·":"·","–":"–","—":"—","•":"•","×":"×","°":"°",
|
||
" ":"\u00A0","“":"\u201C","”":"\u201D","„":"\u201E","‘":"\u2018",
|
||
"’":"\u2019","…":"…","<":"<",">":">","&":"&"};
|
||
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 & -> & (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,"&");
|
||
}
|
||
// 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>
|