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, "'"); } };