From eeebdd72d5d5dccef2ff2fa90f9ab5a6afed4e61 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 18 Nov 2025 21:36:59 +0100 Subject: [PATCH] init: first commit of chatgpt prebuild --- LICENSE | 2 +- engine.js | 175 ++++++++++++++++++++++++++++++++++++++++ example/project.json | 21 +++++ example/sample.csv | 4 + example/style.css | 1 + example/template.svg | 5 ++ generator.js | 108 +++++++++++++++++++++++++ index.html | 52 ++++++++++++ my-project/project.json | 22 +++++ my-project/sample.csv | 6 ++ my-project/style.css | 1 + my-project/template.svg | 5 ++ 12 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 engine.js create mode 100644 example/project.json create mode 100644 example/sample.csv create mode 100644 example/style.css create mode 100644 example/template.svg create mode 100644 generator.js create mode 100644 index.html create mode 100644 my-project/project.json create mode 100644 my-project/sample.csv create mode 100644 my-project/style.css create mode 100644 my-project/template.svg diff --git a/LICENSE b/LICENSE index a4e9dc9..5f0cfa3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT No Attribution -Copyright +Copyright 2025 @Tegwick Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software diff --git a/engine.js b/engine.js new file mode 100644 index 0000000..e8f1920 --- /dev/null +++ b/engine.js @@ -0,0 +1,175 @@ +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); +}); + diff --git a/example/project.json b/example/project.json new file mode 100644 index 0000000..cf3a70f --- /dev/null +++ b/example/project.json @@ -0,0 +1,21 @@ +{ + "name": "Example Timeline Project", + "description": "Neutrales Beispielprojekt f\u00fcr die Timeline Engine.", + "dataSource": "sample.csv", + "stylesheet": "style.css", + "svgTemplate": "template.svg", + "settings": { + "timelineMonths": 18 + }, + "fieldMapping": { + "id": "ID", + "title": "Title", + "lane": "Lane", + "due": [ + "Due" + ], + "epic": null, + "type": null, + "color": null + } +} diff --git a/example/sample.csv b/example/sample.csv new file mode 100644 index 0000000..0fb269e --- /dev/null +++ b/example/sample.csv @@ -0,0 +1,4 @@ +ID,Title,Due,Lane +1,Example Task A,2025-12-01,Team Alpha +2,Example Task B,2026-02-15,Team Beta +3,Example Task C,2026-03-10,Team Alpha diff --git a/example/style.css b/example/style.css new file mode 100644 index 0000000..c78f748 --- /dev/null +++ b/example/style.css @@ -0,0 +1 @@ +body { background:#fafafa; } diff --git a/example/template.svg b/example/template.svg new file mode 100644 index 0000000..91cc643 --- /dev/null +++ b/example/template.svg @@ -0,0 +1,5 @@ + + + {{MONTHS}} + {{LANES}} + diff --git a/generator.js b/generator.js new file mode 100644 index 0000000..aad2a93 --- /dev/null +++ b/generator.js @@ -0,0 +1,108 @@ +window.timelineGenerator = { + generate(items, cfg, template) { + const monthsRange = (cfg.settings && cfg.settings.timelineMonths) || 18; + + // Determine time window from earliest due date + const sorted = [...items].sort((a, b) => a.due - b.due); + const minDue = sorted[0].due; + const start = new Date(minDue.getFullYear(), minDue.getMonth(), 1); + const end = new Date(start); + end.setMonth(end.getMonth() + monthsRange); + + // Build months array + const months = []; + const cursor = new Date(start); + while (cursor <= end) { + months.push(new Date(cursor)); + cursor.setMonth(cursor.getMonth() + 1); + } + + // Group items by lane + const laneMap = new Map(); + for (const it of items) { + const laneName = it.lane || "Ohne Epic"; + if (!laneMap.has(laneName)) laneMap.set(laneName, []); + laneMap.get(laneName).push(it); + } + + const laneNames = Array.from(laneMap.keys()).sort((a, b) => + a.localeCompare(b, "de") + ); + + // Layout constants + const left = 220; + const top = 140; + const monthWidth = 120; + const laneHeight = 80; + const laneGap = 16; + + // Month grid (labels + vertical lines) + let monthGraphics = ""; + const monthLabelY = 90; + const gridTop = top - 20; + const gridBottom = top + laneNames.length * (laneHeight + laneGap) + 40; + + months.forEach((m, i) => { + const x = left + i * monthWidth; + const label = m.toLocaleString("de-DE", { month: "short", year: "2-digit" }); + monthGraphics += ``; + monthGraphics += `${label}`; + }); + + // Helper to compute month index + function monthIndexForDate(d) { + return (d.getFullYear() - start.getFullYear()) * 12 + + (d.getMonth() - start.getMonth()); + } + + // Lane blocks + let laneBlocks = ""; + laneNames.forEach((laneName, laneIdx) => { + const laneY = top + laneIdx * (laneHeight + laneGap); + + // Background band for lane + laneBlocks += ``; + // Lane label + laneBlocks += `${this.escapeXml(laneName)}`; + + const laneItems = laneMap.get(laneName).sort((a, b) => a.due - b.due); + laneItems.forEach((it, idx) => { + const mi = monthIndexForDate(it.due); + const clampedMi = Math.max(0, Math.min(months.length - 1, mi)); + const cx = left + clampedMi * monthWidth + monthWidth * 0.5; + const cy = laneY + 10 + idx * 18; + + laneBlocks += ``; + laneBlocks += ` + ${this.escapeXml(it.id || "")}: + ${this.escapeXml(it.title || "")} + `; + }); + }); + + if (template && template.includes("{{MONTHS}}") && template.includes("{{LANES}}")) { + return template + .replace("{{MONTHS}}", monthGraphics) + .replace("{{LANES}}", laneBlocks); + } + + // Fallback: embed directly in simple SVG + const height = top + laneNames.length * (laneHeight + laneGap) + 80; + const width = left + months.length * monthWidth + 100; + + return ` + + ${monthGraphics} + ${laneBlocks} + `; + }, + + escapeXml(str) { + return String(str) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } +}; diff --git a/index.html b/index.html new file mode 100644 index 0000000..1a81da5 --- /dev/null +++ b/index.html @@ -0,0 +1,52 @@ + + + + + Timeline Generator + + + + + + + +

Timeline Generator

+

+ Wähle ein Projekt oder lade CSV/CSS/SVG-Dateien, um eine Timeline zu erzeugen. +

+ +
+ + + + + + + +
+ +
+ Noch keine Timeline generiert. +
+ + + + + diff --git a/my-project/project.json b/my-project/project.json new file mode 100644 index 0000000..a38055f --- /dev/null +++ b/my-project/project.json @@ -0,0 +1,22 @@ +{ + "name": "My Project Roadmap Timeline", + "description": "Roadmap-Timeline for My Project", + "dataSource": "sample.csv", + "stylesheet": "style.css", + "svgTemplate": "template.svg", + "settings": { + "timelineMonths": 18 + }, + "fieldMapping": { + "id": "Schlüssel", + "title": "Zusammenfassung", + "lane": "Übergeordnet", + "due": [ + "Abgeleitetes Fälligkeitsdatum", + "Fälligkeitsdatum" + ], + "epic": "Übergeordnet", + "type": "Vorgangstyp", + "color": "Issue color" + } +} diff --git a/my-project/sample.csv b/my-project/sample.csv new file mode 100644 index 0000000..fa80be5 --- /dev/null +++ b/my-project/sample.csv @@ -0,0 +1,6 @@ +"Schlüssel","Vorgangstyp","Übergeordnet","Zusammenfassung","Status","Zugewiesene Person","Start date","Abgeleitetes Startdatum","Fälligkeitsdatum","Abgeleitetes Fälligkeitsdatum","Issue color" +"AN-121","Epic","","Benutzerauthentifizierung mit Entra-ID","Zu Erledigen","Axel Hörnke","2025/10/01","2025/10/01","2026/01/31","2026/01/31","purple" +"AN-124","Epic","","Binect IDP (Smartdoc) MVP","Zu Erledigen","","2026/02/01","2026/02/01","2026/03/31","2026/03/31","green" +"AN-123","Epic","","Multi-Tenant-Fähigkeit ONE und Dynamic ","In Arbeit","","2025/09/01","2025/09/01","2025/10/31","2025/10/31","purple" +"AN-122","Epic","","Zusammenführen von Entwürfen","In Arbeit","","2025/09/01","2025/09/01","2025/11/30","2025/11/30","dark_blue" +"AN-111","Epic","","👑🖊 Internet-/Paket-Marke","In Arbeit","Bernhard Hirmer","2025/11/01","2025/11/01","2026/02/27","2026/02/27","dark_blue" diff --git a/my-project/style.css b/my-project/style.css new file mode 100644 index 0000000..ac96614 --- /dev/null +++ b/my-project/style.css @@ -0,0 +1 @@ +body { background:#f5f7fa; } diff --git a/my-project/template.svg b/my-project/template.svg new file mode 100644 index 0000000..91cc643 --- /dev/null +++ b/my-project/template.svg @@ -0,0 +1,5 @@ + + + {{MONTHS}} + {{LANES}} +