window.timelineEngine = { config: null, template: null, csvOverride: false, cssOverride: false, async autoLoadDefaultProject() { // Versuche zuerst Binect-Projekt, sonst Example const candidates = ["binect/project.json", "example/project.json"]; for (const path of candidates) { try { const res = await fetch(path); if (!res.ok) continue; const cfg = await res.json(); await this.loadProjectConfigObject(cfg); return; } catch (e) { continue; } } }, async loadProjectConfigObject(cfg) { this.config = cfg; const name = cfg.name || "Timeline"; document.getElementById("projectName").innerText = name; document.getElementById("projectSubtitle").innerText = cfg.description || "Projektkonfiguration geladen."; // Stylesheet if (cfg.stylesheet && !this.cssOverride) { document.getElementById("dynamicCss").href = cfg.stylesheet; } // SVG template if (cfg.svgTemplate) { try { this.template = await fetch(cfg.svgTemplate).then(r => r.text()); } catch (e) { console.warn("SVG template could not be loaded:", e); } } // CSV data if (cfg.dataSource && !this.csvOverride) { try { const csvText = await fetch(cfg.dataSource).then(r => r.text()); this.processCsv(csvText); } catch (e) { console.warn("CSV could not be loaded:", e); } } }, processCsv(text) { if (!this.config || !this.config.fieldMapping) { console.error("No config or fieldMapping found."); return; } const m = this.config.fieldMapping; Papa.parse(text, { header: true, skipEmptyLines: true, complete: (res) => { const rows = res.data; const items = rows.map((r) => { const dueField = (m.due || []).find(f => r[f]); return { id: m.id ? r[m.id] : (r["ID"] || ""), title: m.title ? r[m.title] : (r["Title"] || ""), lane: m.lane ? (r[m.lane] || "Ohne Epic") : (r["Lane"] || "Default"), due: this.parseDate(dueField ? r[dueField] : null) }; }).filter(i => i.title && i.due); if (!items.length) { document.getElementById("viewer").innerHTML = "Keine gültigen Items gefunden."; return; } const svg = window.timelineGenerator.generate(items, this.config, this.template); document.getElementById("viewer").innerHTML = svg; const dlBtn = document.getElementById("downloadSvg"); dlBtn.disabled = false; dlBtn.style.opacity = 1; } }); }, parseDate(str) { if (!str) return null; str = String(str).trim(); let d = null; // YYYY/MM/DD oder YYYY-MM-DD let m = str.match(/^(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})$/); if (m) { const [_, yy, mm, dd] = m; d = new Date(Number(yy), Number(mm) - 1, Number(dd)); return isNaN(d.getTime()) ? null : d; } // DD.MM.YYYY m = str.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/); if (m) { const [_, dd, mm, yy] = m; d = new Date(Number(yy), Number(mm) - 1, Number(dd)); return isNaN(d.getTime()) ? null : d; } // Fallback d = new Date(str); return isNaN(d.getTime()) ? null : d; } }; // --------- UI event handlers --------- document.getElementById("projectInput").addEventListener("change", async (ev) => { const file = ev.target.files[0]; if (!file) return; const text = await file.text(); const cfg = JSON.parse(text); await window.timelineEngine.loadProjectConfigObject(cfg); }); document.getElementById("csvInput").addEventListener("change", async (ev) => { const file = ev.target.files[0]; if (!file) return; const text = await file.text(); window.timelineEngine.csvOverride = true; window.timelineEngine.processCsv(text); }); document.getElementById("cssInput").addEventListener("change", async (ev) => { const file = ev.target.files[0]; if (!file) return; const cssText = await file.text(); window.timelineEngine.cssOverride = true; const blob = new Blob([cssText], { type: "text/css" }); document.getElementById("dynamicCss").href = URL.createObjectURL(blob); }); document.getElementById("svgInput").addEventListener("change", async (ev) => { const file = ev.target.files[0]; if (!file) return; window.timelineEngine.template = await file.text(); }); document.getElementById("downloadSvg").addEventListener("click", () => { const svg = document.querySelector("#viewer svg"); if (!svg) return; // Always external view for export: IDs ausblenden svg.querySelectorAll(".item-id").forEach(el => el.style.display = "none"); const now = new Date(); const ts = String(now.getFullYear()).slice(2) + String(now.getMonth() + 1).padStart(2, "0") + String(now.getDate()).padStart(2, "0") + "T" + String(now.getHours()).padStart(2, "0") + String(now.getMinutes()).padStart(2, "0"); const blob = new Blob([svg.outerHTML], { type: "image/svg+xml" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${ts}-timeline.svg`; a.click(); URL.revokeObjectURL(url); });