init: first commit of chatgpt prebuild

This commit is contained in:
2025-11-18 21:36:59 +01:00
parent e0d2689a9d
commit eeebdd72d5
12 changed files with 401 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
MIT No Attribution MIT No Attribution
Copyright <YEAR> <COPYRIGHT HOLDER> Copyright 2025 @Tegwick
Permission is hereby granted, free of charge, to any person obtaining a copy of this 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 software and associated documentation files (the "Software"), to deal in the Software

175
engine.js Normal file
View File

@@ -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 =
"<em style='color:#999;'>Keine gültigen Items gefunden.</em>";
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);
});

21
example/project.json Normal file
View File

@@ -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
}
}

4
example/sample.csv Normal file
View File

@@ -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
1 ID Title Due Lane
2 1 Example Task A 2025-12-01 Team Alpha
3 2 Example Task B 2026-02-15 Team Beta
4 3 Example Task C 2026-03-10 Team Alpha

1
example/style.css Normal file
View File

@@ -0,0 +1 @@
body { background:#fafafa; }

5
example/template.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#FFFFFF"/>
{{MONTHS}}
{{LANES}}
</svg>

After

Width:  |  Height:  |  Size: 125 B

108
generator.js Normal file
View File

@@ -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 += `<line x1="${x}" y1="${gridTop}" x2="${x}" y2="${gridBottom}" stroke="#E3E8EF" />`;
monthGraphics += `<text x="${x + 4}" y="${monthLabelY}" fill="#5C6B7A" font-size="12">${label}</text>`;
});
// 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 += `<rect x="40" y="${laneY - 24}" width="${left + months.length * monthWidth}" height="${laneHeight}" fill="#FFFFFF" stroke="#E3E8EF" rx="10" />`;
// Lane label
laneBlocks += `<text x="56" y="${laneY - 4}" fill="#0B1F3B" font-size="14" font-weight="600">${this.escapeXml(laneName)}</text>`;
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 += `<circle cx="${cx}" cy="${cy}" r="5" fill="#0A4D8C" />`;
laneBlocks += `<text x="${cx + 10}" y="${cy + 4}" font-size="12" fill="#0B1F3B">
<tspan class="item-id">${this.escapeXml(it.id || "")}: </tspan>
<tspan class="item-title">${this.escapeXml(it.title || "")}</tspan>
</text>`;
});
});
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 `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
<rect width="100%" height="100%" fill="#FFFFFF" />
${monthGraphics}
${laneBlocks}
</svg>`;
},
escapeXml(str) {
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
};

52
index.html Normal file
View File

@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<title>Timeline Generator</title>
<script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script>
<link id="dynamicCss" rel="stylesheet" href="">
<script defer src="engine.js"></script>
<script defer src="generator.js"></script>
</head>
<body class="internal-mode" style="font-family: Inter, Arial, sans-serif; background:#f5f7fa; margin:20px;">
<h1 id="projectName" style="color:#0A4D8C; margin-bottom:8px;">Timeline Generator</h1>
<p id="projectSubtitle" style="color:#5C6B7A; margin-top:0; margin-bottom:16px;">
Wähle ein Projekt oder lade CSV/CSS/SVG-Dateien, um eine Timeline zu erzeugen.
</p>
<div class="controls" style="margin-bottom:16px; display:flex; flex-wrap:wrap; gap:12px; align-items:center;">
<label>Projekt laden:
<input type="file" id="projectInput" accept=".json" />
</label>
<label>CSV laden:
<input type="file" id="csvInput" accept=".csv" />
</label>
<label>CSS laden:
<input type="file" id="cssInput" accept=".css" />
</label>
<label>SVG Template laden:
<input type="file" id="svgInput" accept=".svg" />
</label>
<button id="toggleView" style="padding:8px 14px; background:#0A4D8C; color:white; border:none; border-radius:6px; cursor:pointer;">
Switch View (Internal / External)
</button>
<button id="downloadSvg" disabled
style="padding:8px 14px; background:#0A4D8C; color:white; border:none; border-radius:6px; cursor:pointer; opacity:0.6;">
Download SVG
</button>
</div>
<div id="viewer" style="border:1px solid #ccd3db; background:white; padding:12px; border-radius:8px; overflow-x:auto; min-height:200px;">
<em style="color:#999;">Noch keine Timeline generiert.</em>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
window.timelineEngine.autoLoadDefaultProject();
});
</script>
</body>
</html>

22
my-project/project.json Normal file
View File

@@ -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"
}
}

6
my-project/sample.csv Normal file
View File

@@ -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"
1 Schlüssel Vorgangstyp Übergeordnet Zusammenfassung Status Zugewiesene Person Start date Abgeleitetes Startdatum Fälligkeitsdatum Abgeleitetes Fälligkeitsdatum Issue color
2 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
3 AN-124 Epic Binect IDP (Smartdoc) MVP Zu Erledigen 2026/02/01 2026/02/01 2026/03/31 2026/03/31 green
4 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
5 AN-122 Epic Zusammenführen von Entwürfen In Arbeit 2025/09/01 2025/09/01 2025/11/30 2025/11/30 dark_blue
6 AN-111 Epic 👑🖊 Internet-/Paket-Marke In Arbeit Bernhard Hirmer 2025/11/01 2025/11/01 2026/02/27 2026/02/27 dark_blue

1
my-project/style.css Normal file
View File

@@ -0,0 +1 @@
body { background:#f5f7fa; }

5
my-project/template.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#FFFFFF"/>
{{MONTHS}}
{{LANES}}
</svg>

After

Width:  |  Height:  |  Size: 125 B