window.timelineEngine = { config: null, template: null, csvOverride: false, cssOverride: false, projectBasePath: '', updateFileStatus(fileType, filename, status = 'loaded') { const statusElement = document.getElementById(`${fileType}File`); if (statusElement) { if (filename && status === 'loaded') { statusElement.textContent = filename; statusElement.style.color = '#28a745'; statusElement.title = `Loaded: ${filename}`; } else if (filename && status === 'error') { statusElement.textContent = `Error: ${filename}`; statusElement.style.color = '#dc3545'; statusElement.title = `Failed to load: ${filename}`; } else { statusElement.textContent = 'Not loaded'; statusElement.style.color = '#6c757d'; statusElement.title = 'No file loaded'; } } // Add visual feedback with brief highlight animation if (statusElement && filename) { statusElement.style.transform = 'scale(1.05)'; setTimeout(() => { statusElement.style.transform = 'scale(1)'; }, 200); } }, resolveProjectPath(relativePath) { if (!relativePath) return relativePath; // If path is already absolute (starts with http/https) or has no base path, return as-is if (relativePath.startsWith('http') || !this.projectBasePath) { return relativePath; } const resolvedPath = this.projectBasePath + relativePath; console.log("Resolved path:", relativePath, "->", resolvedPath); return resolvedPath; }, async autoLoadDefaultProject() { console.log("Starting autoLoadDefaultProject"); // Versuche zuerst Binect-Projekt, sonst Example const candidates = [ { path: "binect/project.json", basePath: "binect/" }, { path: "example/project.json", basePath: "example/" } ]; for (const candidate of candidates) { try { const res = await fetch(candidate.path); if (!res.ok) continue; const cfg = await res.json(); this.projectBasePath = candidate.basePath; await this.loadProjectConfigObject(cfg); console.log("Project loaded successfully from:", candidate.path); return; } catch (e) { continue; } } console.log("No project could be auto-loaded"); }, async loadProjectConfigObject(cfg) { this.config = cfg; const name = cfg.name || "Timeline"; document.getElementById("projectName").innerText = name; document.getElementById("projectSubtitle").innerText = cfg.description || "Projektkonfiguration geladen."; // Update project status this.updateFileStatus('project', name, 'loaded'); // Track loading errors for user feedback const loadingErrors = []; // Stylesheet if (cfg.stylesheet && !this.cssOverride) { const stylesheetPath = this.resolveProjectPath(cfg.stylesheet); const linkElement = document.getElementById("dynamicCss"); // Set up load/error event handlers before setting href const handleLoad = () => { console.log("Stylesheet loaded successfully:", stylesheetPath); this.updateFileStatus('css', cfg.stylesheet + ' ✨', 'loaded'); linkElement.removeEventListener('load', handleLoad); linkElement.removeEventListener('error', handleError); }; const handleError = () => { console.error("Stylesheet could not be loaded:", stylesheetPath); this.updateFileStatus('css', cfg.stylesheet, 'error'); loadingErrors.push(`Stylesheet: ${cfg.stylesheet}`); linkElement.removeEventListener('load', handleLoad); linkElement.removeEventListener('error', handleError); }; linkElement.addEventListener('load', handleLoad); linkElement.addEventListener('error', handleError); linkElement.href = stylesheetPath; } // SVG template if (cfg.svgTemplate) { try { const templatePath = this.resolveProjectPath(cfg.svgTemplate); console.log("Attempting to fetch SVG template from:", templatePath); const response = await fetch(templatePath); console.log("SVG template fetch response:", response.status, response.statusText); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } this.template = await response.text(); console.log("SVG template loaded, length:", this.template.length); this.updateFileStatus('svg', cfg.svgTemplate, 'loaded'); } catch (e) { console.error("SVG template could not be loaded:", e); console.error("Failed SVG template path was:", this.resolveProjectPath(cfg.svgTemplate)); this.updateFileStatus('svg', cfg.svgTemplate, 'error'); loadingErrors.push(`SVG template: ${cfg.svgTemplate}`); } } // CSV data if (cfg.dataSource && !this.csvOverride) { try { const csvPath = this.resolveProjectPath(cfg.dataSource); console.log("Attempting to fetch CSV from:", csvPath); const response = await fetch(csvPath); console.log("CSV fetch response:", response.status, response.statusText); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const csvText = await response.text(); console.log("CSV text loaded, length:", csvText.length); console.log("CSV preview:", csvText.substring(0, 200)); this.updateFileStatus('csv', cfg.dataSource, 'loaded'); this.processCsv(csvText); } catch (e) { console.error("CSV could not be loaded:", e); console.error("Failed CSV path was:", this.resolveProjectPath(cfg.dataSource)); this.updateFileStatus('csv', cfg.dataSource, 'error'); loadingErrors.push(`CSV data: ${cfg.dataSource}`); } } // Show user feedback if there were loading errors if (loadingErrors.length > 0) { setTimeout(() => { const viewer = document.getElementById("viewer"); if (viewer && !viewer.innerHTML.includes('
⚠️

Projektdateien konnten nicht geladen werden

Fehlgeschlagene Dateien:
${loadingErrors.map(err => `• ${err}`).join('
')}

💡 Tipp: Lade die Dateien manuell mit den Load-Buttons oben oder verwende einen lokalen Server (make serve).

`; } }, 500); // Small delay to let other operations complete } }, processCsv(text) { console.log("processCsv called with text length:", text?.length); if (!this.config || !this.config.fieldMapping) { console.error("No config or fieldMapping found."); return; } if (typeof Papa === 'undefined') { console.error("Papa Parse library not available"); document.getElementById("viewer").innerHTML = "Fehler: CSV Parser nicht verfügbar."; return; } const m = this.config.fieldMapping; const self = this; // Capture 'this' context Papa.parse(text, { header: true, skipEmptyLines: true, complete: (res) => { console.log("Papa.parse complete, found", res.data.length, "rows"); const rows = res.data; const items = rows.map((r) => { const dueField = (m.due || []).find(f => r[f]); const item = { 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: self.parseDate(dueField ? r[dueField] : null) }; return item; }).filter(i => i.title && i.due); console.log("Filtered to", items.length, "valid items"); if (!items.length) { document.getElementById("viewer").innerHTML = "Keine gültigen Items gefunden."; return; } // Ensure generator is available if (!window.timelineGenerator) { console.error("Timeline generator not available"); document.getElementById("viewer").innerHTML = "Fehler: Timeline Generator nicht verfügbar."; return; } try { console.log("Generating timeline with:", items, self.config, self.template ? "template loaded" : "no template"); const svg = window.timelineGenerator.generate(items, self.config, self.template); document.getElementById("viewer").innerHTML = svg; const dlBtn = document.getElementById("downloadSvg"); dlBtn.disabled = false; dlBtn.style.opacity = 1; console.log("Timeline generated successfully"); } catch (error) { console.error("Error generating timeline:", error); document.getElementById("viewer").innerHTML = "Fehler beim Generieren der Timeline."; } } }); }, 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 --------- function setupEventHandlers() { const projectInput = document.getElementById("projectInput"); if (projectInput) { projectInput.addEventListener("change", async (ev) => { const file = ev.target.files[0]; if (!file) return; try { const text = await file.text(); const cfg = JSON.parse(text); // For manually loaded projects, try to infer base path from filename // If it's example/project.json or binect/project.json, set appropriate base path const filename = file.name; if (filename === 'project.json') { // Try to detect if this is a known project by checking the config if (cfg.name && cfg.name.includes('Example')) { window.timelineEngine.projectBasePath = 'example/'; console.log("Detected example project, setting base path to example/"); } else { window.timelineEngine.projectBasePath = ''; console.log("Unknown project, clearing base path"); } } else { window.timelineEngine.projectBasePath = ''; } window.timelineEngine.updateFileStatus('project', file.name, 'loaded'); await window.timelineEngine.loadProjectConfigObject(cfg); // Show message about relative paths if project has data sources if (cfg.dataSource || cfg.stylesheet || cfg.svgTemplate) { const viewer = document.getElementById("viewer"); if (viewer && (viewer.innerHTML.includes("could not be loaded") || viewer.innerHTML.includes("Keine gültigen Items"))) { viewer.innerHTML += "

💡 Hinweis: Stelle sicher, dass sich die referenzierten Dateien (CSV, CSS, SVG) im gleichen Verzeichnis wie die HTML-Datei befinden."; } } } catch (error) { console.error("Error loading project:", error); window.timelineEngine.updateFileStatus('project', file.name, 'error'); } }); } const csvInput = document.getElementById("csvInput"); if (csvInput) { 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.updateFileStatus('csv', file.name, 'loaded'); window.timelineEngine.processCsv(text); }); } const cssInput = document.getElementById("cssInput"); if (cssInput) { 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); window.timelineEngine.updateFileStatus('css', file.name, 'loaded'); }); } const svgInput = document.getElementById("svgInput"); if (svgInput) { svgInput.addEventListener("change", async (ev) => { const file = ev.target.files[0]; if (!file) return; window.timelineEngine.template = await file.text(); window.timelineEngine.updateFileStatus('svg', file.name, 'loaded'); }); } const downloadSvg = document.getElementById("downloadSvg"); if (downloadSvg) { 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); }); } const toggleView = document.getElementById("toggleView"); if (toggleView) { toggleView.addEventListener("click", () => { const body = document.body; const isInternal = body.classList.contains("internal-mode"); if (isInternal) { body.classList.remove("internal-mode"); body.classList.add("external-mode"); toggleView.textContent = "Switch to Internal View"; // Hide IDs in external mode document.querySelectorAll(".item-id").forEach(el => el.style.display = "none"); } else { body.classList.remove("external-mode"); body.classList.add("internal-mode"); toggleView.textContent = "Switch to External View"; // Show IDs in internal mode document.querySelectorAll(".item-id").forEach(el => el.style.display = "inline"); } }); } }