window.timelineEngine = { config: null, template: null, csvOverride: false, cssOverride: false, csvData: null, // Store current CSV text cssData: null, // Store current CSS text projectBasePath: '', showTemplatePreview() { if (!this.template) return; console.log("Showing template preview"); // Detect template theme for appropriate colors const isEnhanced = this.template.includes('Enhanced'); const isBlueTheme = this.template.includes('My Project'); const isGreenTheme = this.template.includes('Example'); // Color scheme selection let primaryColor = '#0A4D8C'; // default let secondaryColor = '#5C6B7A'; // default if (isEnhanced) { if (isBlueTheme) { primaryColor = '#3b82f6'; secondaryColor = '#60a5fa'; } else if (isGreenTheme) { primaryColor = '#2d8659'; secondaryColor = '#4a9b6b'; } } // Create sample month graphics matching the template style const sampleMonths = isEnhanced ? ` Jan 25 Feb 25 Mar 25 Apr 25 ` : ` Jan 25 Feb 25 Mar 25 Apr 25 `; // Create sample lane content matching the template style const sampleLanes = isEnhanced ? ` Example Epic EX-1: Sample Task Preview Another Epic EX-2: Template Preview ` : ` Example Epic EX-1: Sample Task Preview Another Epic EX-2: Template Preview `; // Replace placeholders with sample data let previewSvg = this.template .replace("{{MONTHS}}", sampleMonths) .replace("{{LANES}}", sampleLanes); // Add dimensions for proper preview display (using reasonable default size) const previewWidth = 720; const previewHeight = 360; previewSvg = previewSvg.replace( /]*?)>/, `` ); document.getElementById("viewer").innerHTML = previewSvg; // Initialize zoom functionality for the preview if (window.svgViewer) { window.svgViewer.resetZoom(); window.svgViewer.updateZoomControls(); } console.log("Template preview displayed with theme:", { isEnhanced, isBlueTheme, isGreenTheme }); }, 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"); // Try projects in order: binect, my-project, example const candidates = [ { path: "binect/project.json", basePath: "binect/" }, { path: "my-project/project.json", basePath: "my-project/" }, { 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").textContent = name; document.getElementById("projectSubtitle").textContent = cfg.description || "Projektkonfiguration geladen."; // Update project status this.updateFileStatus('project', name, 'loaded'); // Enable project edit button if (window.fileEditor) { window.fileEditor.enableEditButton('project'); } // Show field mappings in debug panel this.showFieldMappings(); // Track loading errors for user feedback const loadingErrors = []; // Stylesheet if (cfg.stylesheet && !this.cssOverride) { const stylesheetPath = this.resolveProjectPath(cfg.stylesheet); // Load CSS via fetch to store content for editing try { const cssResponse = await fetch(stylesheetPath); if (cssResponse.ok) { const cssText = await cssResponse.text(); this.cssData = cssText; // Store for editing // Apply CSS const blob = new Blob([cssText], { type: "text/css" }); const linkElement = document.getElementById("dynamicCss"); linkElement.href = URL.createObjectURL(blob); this.updateFileStatus('css', cfg.stylesheet + ' ✨', 'loaded'); // Enable CSS edit button if (window.fileEditor) { window.fileEditor.enableEditButton('css'); } console.log("Stylesheet loaded successfully:", stylesheetPath); } else { throw new Error(`HTTP ${cssResponse.status}`); } } catch (e) { console.error("Stylesheet could not be loaded:", e); this.updateFileStatus('css', cfg.stylesheet, 'error'); loadingErrors.push(`Stylesheet: ${cfg.stylesheet}`); } } // SVG template if (cfg.svgTemplate) { try { const templatePath = this.resolveProjectPath(cfg.svgTemplate); console.log("Attempting to fetch SVG template from:", templatePath); const svgResponse = await fetch(templatePath); console.log("SVG template fetch response:", svgResponse.status, svgResponse.statusText); if (!svgResponse.ok) { throw new Error(`HTTP ${svgResponse.status}: ${svgResponse.statusText}`); } this.template = await svgResponse.text(); console.log("SVG template loaded, length:", this.template.length); this.updateFileStatus('svg', cfg.svgTemplate, 'loaded'); // Enable SVG edit button if (window.fileEditor) { window.fileEditor.enableEditButton('svg'); } // Show template fields in debug panel this.showTemplateFields(); // Show template preview if no CSV data is loaded yet if (!this.csvOverride && document.querySelector("#viewer").innerHTML.includes("Keine Timeline verfügbar")) { this.showTemplatePreview(); } } 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 csvResponse = await fetch(csvPath); console.log("CSV fetch response:", csvResponse.status, csvResponse.statusText); if (!csvResponse.ok) { throw new Error(`HTTP ${csvResponse.status}: ${csvResponse.statusText}`); } const csvText = await csvResponse.text(); console.log("CSV text loaded, length:", csvText.length); console.log("CSV preview:", csvText.substring(0, 200)); this.updateFileStatus('csv', cfg.dataSource, 'loaded'); // Enable CSV edit button if (window.fileEditor) { window.fileEditor.enableEditButton('csv'); } 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); // Store CSV data for editing this.csvData = text; 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; // Show CSV preview in debug panel self.showCSVPreview(text, rows); 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"); // Auto-detect template type: prototype-based (new) or template-based (old) const isPrototypeBased = self.template && ( self.template.includes('id="month-proto"') || self.template.includes('id="lane-proto"') || self.template.includes('id="item-proto"') ); let svg; if (isPrototypeBased && window.timelineGeneratorDOM) { console.log("Using DOM-based generator (prototype templates)"); svg = window.timelineGeneratorDOM.generate(items, self.config, self.template); } else if (window.timelineGenerator) { console.log("Using string-based generator (template-v2)"); svg = window.timelineGenerator.generate(items, self.config, self.template); } else { throw new Error("No timeline generator available"); } document.getElementById("viewer").innerHTML = svg; const dlBtn = document.getElementById("downloadSvg"); dlBtn.disabled = false; dlBtn.style.opacity = 1; // Initialize zoom functionality for the new SVG if (window.svgViewer) { window.svgViewer.resetZoom(); window.svgViewer.updateZoomControls(); } console.log("Timeline generated successfully"); } catch (error) { console.error("Error generating timeline:", error); document.getElementById("viewer").innerHTML = "Fehler beim Generieren der Timeline: " + error.message + ""; } } }); }, 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; }, // --------- Debug Display Functions --------- showFieldMappings() { if (!this.config || !this.config.fieldMapping) return; const debugInfo = document.getElementById("debugInfo"); const fieldMappingInfo = document.getElementById("fieldMappingInfo"); const fieldMappingDisplay = document.getElementById("fieldMappingDisplay"); if (debugInfo && fieldMappingInfo && fieldMappingDisplay) { debugInfo.style.display = "block"; fieldMappingInfo.style.display = "block"; const mapping = this.config.fieldMapping; let display = "CSV Column → Timeline Field\n"; display += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; display += `ID: ${JSON.stringify(mapping.id)}\n`; display += `Title: ${JSON.stringify(mapping.title)}\n`; display += `Lane: ${JSON.stringify(mapping.lane)}\n`; display += `Due: ${JSON.stringify(mapping.due)}\n`; if (mapping.epic) display += `Epic: ${JSON.stringify(mapping.epic)}\n`; if (mapping.type) display += `Type: ${JSON.stringify(mapping.type)}\n`; if (mapping.color) display += `Color: ${JSON.stringify(mapping.color)}\n`; fieldMappingDisplay.textContent = display; } }, showTemplateFields() { if (!this.template) return; const debugInfo = document.getElementById("debugInfo"); const templateFieldsInfo = document.getElementById("templateFieldsInfo"); const templateFieldsDisplay = document.getElementById("templateFieldsDisplay"); if (debugInfo && templateFieldsInfo && templateFieldsDisplay) { debugInfo.style.display = "block"; templateFieldsInfo.style.display = "block"; // Extract placeholders from template const placeholders = new Set(); const placeholderRegex = /\{\{([A-Z_]+)\}\}/g; let match; while ((match = placeholderRegex.exec(this.template)) !== null) { placeholders.add(match[1]); } let display = "Required Template Placeholders:\n"; display += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; // Group by category const monthFields = Array.from(placeholders).filter(p => p.includes('MONTH')); const laneFields = Array.from(placeholders).filter(p => p.includes('LANE')); const taskFields = Array.from(placeholders).filter(p => p.includes('TASK') || p.includes('TEXT')); const otherFields = Array.from(placeholders).filter(p => !monthFields.includes(p) && !laneFields.includes(p) && !taskFields.includes(p) ); if (monthFields.length > 0) { display += "\nMonth Fields:\n"; monthFields.forEach(f => display += ` • ${f}\n`); } if (laneFields.length > 0) { display += "\nLane Fields:\n"; laneFields.forEach(f => display += ` • ${f}\n`); } if (taskFields.length > 0) { display += "\nTask Fields:\n"; taskFields.forEach(f => display += ` • ${f}\n`); } if (otherFields.length > 0) { display += "\nOther Fields:\n"; otherFields.forEach(f => display += ` • ${f}\n`); } templateFieldsDisplay.textContent = display; } }, showCSVPreview(csvText, parsedData) { const debugInfo = document.getElementById("debugInfo"); const csvDataInfo = document.getElementById("csvDataInfo"); const csvDataDisplay = document.getElementById("csvDataDisplay"); if (debugInfo && csvDataInfo && csvDataDisplay) { debugInfo.style.display = "block"; csvDataInfo.style.display = "block"; const lines = csvText.trim().split('\n'); const headers = lines[0] ? lines[0].split(',') : []; const firstDataLine = lines[1] ? lines[1].split(',') : []; let display = "CSV Structure Preview:\n"; display += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; display += `Headers: ${headers.join(', ')}\n\n`; if (firstDataLine.length > 0) { display += "First Data Row:\n"; headers.forEach((header, i) => { display += ` ${header}: "${firstDataLine[i] || ''}"\n`; }); } if (parsedData) { display += `\nParsed Rows: ${parsedData.length}\n`; const validCount = parsedData.filter(r => { const mapping = this.config?.fieldMapping || {}; const titleField = mapping.title; const dueField = Array.isArray(mapping.due) ? mapping.due.find(f => r[f]) : mapping.due; return r[titleField] && r[dueField]; }).length; display += `Valid Items: ${validCount}\n`; if (validCount === 0 && parsedData.length > 0) { display += "\n⚠️ WARNING: No valid items found!\n"; display += "Check that:\n"; display += " • CSV headers match field mappings\n"; display += " • Title and Due fields have values\n"; display += " • Date format is parseable (e.g., YYYY-MM-DD)\n"; } } csvDataDisplay.textContent = display; } } }; // --------- SVG Viewer with Zoom functionality --------- window.svgViewer = { currentZoom: 1, minZoom: 0.25, maxZoom: 3, zoomStep: 0.25, initializeZoom() { const viewer = document.getElementById("viewer"); const zoomControls = document.getElementById("zoomControls"); const zoomIn = document.getElementById("zoomIn"); const zoomOut = document.getElementById("zoomOut"); const zoomReset = document.getElementById("zoomReset"); if (!viewer || !zoomControls || !zoomIn || !zoomOut || !zoomReset) { console.warn("Zoom controls not found in DOM"); return; } // Show zoom controls when SVG is present this.updateZoomControls(); // Zoom in zoomIn.addEventListener("click", () => { this.zoomIn(); }); // Zoom out zoomOut.addEventListener("click", () => { this.zoomOut(); }); // Reset zoom zoomReset.addEventListener("click", () => { this.resetZoom(); }); // Mouse wheel zoom (with Ctrl key) viewer.addEventListener("wheel", (e) => { if (e.ctrlKey || e.metaKey) { e.preventDefault(); if (e.deltaY < 0) { this.zoomIn(); } else { this.zoomOut(); } } }); console.log("SVG zoom functionality initialized"); }, showZoomControls() { const zoomControls = document.getElementById("zoomControls"); if (zoomControls) { zoomControls.style.display = "block"; } }, hideZoomControls() { const zoomControls = document.getElementById("zoomControls"); if (zoomControls) { zoomControls.style.display = "none"; } }, zoomIn() { if (this.currentZoom < this.maxZoom) { this.currentZoom = Math.min(this.maxZoom, this.currentZoom + this.zoomStep); this.applyZoom(); } }, zoomOut() { if (this.currentZoom > this.minZoom) { this.currentZoom = Math.max(this.minZoom, this.currentZoom - this.zoomStep); this.applyZoom(); } }, resetZoom() { this.currentZoom = 1; this.applyZoom(); }, applyZoom() { const viewer = document.getElementById("viewer"); const container = document.getElementById("viewerContainer"); if (viewer && container) { viewer.style.transform = `scale(${this.currentZoom})`; // Adjust container overflow behavior based on zoom level if (this.currentZoom !== 1) { viewer.classList.add("zoomed"); container.style.overflow = "auto"; } else { viewer.classList.remove("zoomed"); container.style.overflow = "hidden"; } this.updateZoomControls(); } }, updateZoomControls() { const zoomIn = document.getElementById("zoomIn"); const zoomOut = document.getElementById("zoomOut"); const zoomReset = document.getElementById("zoomReset"); if (zoomIn && zoomOut && zoomReset) { // Update button states zoomIn.disabled = this.currentZoom >= this.maxZoom; zoomOut.disabled = this.currentZoom <= this.minZoom; // Update zoom percentage display const percentage = Math.round(this.currentZoom * 100); zoomReset.textContent = `${percentage}%`; // Show/hide controls based on whether SVG is present const hasSvg = document.querySelector("#viewer svg") !== null; if (hasSvg) { this.showZoomControls(); } else { this.hideZoomControls(); } } } }; // --------- UI event handlers --------- window.setupEventHandlers = function() { // Handler for loading entire project folder const folderInput = document.getElementById("folderInput"); if (folderInput) { folderInput.addEventListener("change", async (ev) => { const files = Array.from(ev.target.files); if (!files.length) return; console.log("Folder selected with", files.length, "files"); try { // Find project.json in the uploaded files const projectFile = files.find(f => f.name === 'project.json'); if (!projectFile) { alert('No project.json found in selected folder. Please select a folder containing a project.json file.'); return; } // Parse project.json const projectText = await projectFile.text(); const cfg = JSON.parse(projectText); console.log("Loaded project configuration:", cfg); // Clear projectBasePath since we're loading from uploaded files window.timelineEngine.projectBasePath = ''; // Update project status window.timelineEngine.updateFileStatus('project', projectFile.name, 'loaded'); // Enable project edit button if (window.fileEditor) { window.fileEditor.enableEditButton('project'); } // Load referenced files from the folder const errors = []; // Load stylesheet if (cfg.stylesheet) { const cssFile = files.find(f => f.name === cfg.stylesheet || f.webkitRelativePath.endsWith(cfg.stylesheet)); if (cssFile) { const cssText = await cssFile.text(); window.timelineEngine.cssData = cssText; // Store for editing window.timelineEngine.cssOverride = true; const blob = new Blob([cssText], { type: "text/css" }); document.getElementById("dynamicCss").href = URL.createObjectURL(blob); window.timelineEngine.updateFileStatus('css', cssFile.name, 'loaded'); // Enable CSS edit button if (window.fileEditor) { window.fileEditor.enableEditButton('css'); } console.log("Loaded stylesheet:", cssFile.name); } else { errors.push(`Stylesheet: ${cfg.stylesheet}`); window.timelineEngine.updateFileStatus('css', cfg.stylesheet, 'error'); } } // Load SVG template if (cfg.svgTemplate) { const svgFile = files.find(f => f.name === cfg.svgTemplate || f.webkitRelativePath.endsWith(cfg.svgTemplate)); if (svgFile) { window.timelineEngine.template = await svgFile.text(); window.timelineEngine.updateFileStatus('svg', svgFile.name, 'loaded'); window.timelineEngine.showTemplateFields(); // Enable SVG edit button if (window.fileEditor) { window.fileEditor.enableEditButton('svg'); } console.log("Loaded SVG template:", svgFile.name); } else { errors.push(`SVG template: ${cfg.svgTemplate}`); window.timelineEngine.updateFileStatus('svg', cfg.svgTemplate, 'error'); } } // Load CSV data if (cfg.dataSource) { const csvFile = files.find(f => f.name === cfg.dataSource || f.webkitRelativePath.endsWith(cfg.dataSource)); if (csvFile) { const csvText = await csvFile.text(); window.timelineEngine.csvOverride = true; window.timelineEngine.updateFileStatus('csv', csvFile.name, 'loaded'); // Enable CSV edit button if (window.fileEditor) { window.fileEditor.enableEditButton('csv'); } console.log("Loaded CSV data:", csvFile.name); // Set config and process CSV window.timelineEngine.config = cfg; window.timelineEngine.processCsv(csvText); } else { errors.push(`CSV data: ${cfg.dataSource}`); window.timelineEngine.updateFileStatus('csv', cfg.dataSource, 'error'); } } // Update project name and description const name = cfg.name || "Timeline"; document.getElementById("projectName").textContent = name; document.getElementById("projectSubtitle").textContent = cfg.description || "Project loaded from folder."; // Show field mappings window.timelineEngine.showFieldMappings(); // Show any errors if (errors.length > 0) { setTimeout(() => { alert(`⚠️ Some files could not be loaded:\n\n${errors.join('\n')}\n\nMake sure all referenced files are in the selected folder.`); }, 500); } else { console.log("✅ All project files loaded successfully from folder"); } } catch (error) { console.error("Error loading project folder:", error); alert(`Error loading project: ${error.message}`); } }); } // Individual file inputs removed - use folder picker instead 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"); } }); } }
Fehlgeschlagene Dateien: ${loadingErrors.map(err => `• ${err}`).join('')}
💡 Tipp: Lade die Dateien manuell mit den Load-Buttons oben oder verwende einen lokalen Server (make serve).
make serve