// Portfolio-Zeitstrahl // Quelle: widget.query("portfolio.projects") // Zeitfenster: 3 Monate zurück bis 12 Monate voraus; Balken werden am // Fensterrand abgeschnitten und mit einem Pfeil markiert. const MONTHS_BACK = 3; const MONTHS_AHEAD = 12; const MAX_ROWS = 18; const isDark: boolean = widget.data.theme === "dark"; const C = { text: isDark ? "#f1f5f9" : "#1e293b", sub: "#94a3b8", line: isDark ? "#334155" : "#e2e8f0", track: isDark ? "#1e293b" : "#f1f5f9", green: "#22c55e", amber: "#f59e0b", red: "#ef4444", blue: "#3b82f6", }; const MAX_PAGES = 3; function esc(s: string): string { return String(s).replace(/[&<>"]/g, (c: string) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """); } function note(text: string): string { return `
${text}
`; } async function loadProjects() { const items: WidgetPortfolioProject[] = []; let cursor: string | undefined = undefined; let total: number | null = null; for (let page = 0; page < MAX_PAGES; page++) { const res = await widget.query("portfolio.projects", { limit: 200, cursor: cursor }); if (!res.ok) return { error: res.error.code, items: items, total: total, complete: false }; if (total === null) total = res.data.total; res.data.items.forEach((p) => items.push(p)); if (!res.data.nextCursor) return { error: null, items: items, total: total, complete: true }; cursor = res.data.nextCursor; } return { error: null, items: items, total: total, complete: false }; } widget.el.innerHTML = note("Baue Zeitstrahl …"); (async () => { const loaded = await loadProjects(); if (loaded.error) { widget.el.innerHTML = note( loaded.error === "forbidden" ? "Keine Berechtigung für die Portfoliodaten." : loaded.error === "unsupported_in_context" ? "Nur auf Portfolioebene verfügbar." : "Projekte konnten nicht geladen werden.", ); widget.done(); return; } const now = new Date(); const from = new Date(now.getFullYear(), now.getMonth() - MONTHS_BACK, 1).getTime(); const to = new Date(now.getFullYear(), now.getMonth() + MONTHS_AHEAD, 1).getTime(); const span: number = to - from; function pos(t: number): number { return Math.min(Math.max((t - from) / span, 0), 1) * 100; } const rows = loaded.items .filter((p) => !p.actualEnd || p.actualEnd === "") .map((p) => ({ p: p, s: Date.parse(p.planStart), e: Date.parse(p.planEnd) })) .filter((r) => !Number.isNaN(r.s) && !Number.isNaN(r.e) && r.e >= from && r.s <= to) .sort((a, b) => a.s - b.s); if (rows.length === 0) { widget.el.innerHTML = note("Keine laufenden Projekte im Zeitfenster."); widget.done(); return; } // Monatsraster (Beschriftung nur zum Quartalsanfang, sonst wird es eng). let grid = ""; let labels = ""; for (let m = -MONTHS_BACK; m <= MONTHS_AHEAD; m++) { const d = new Date(now.getFullYear(), now.getMonth() + m, 1); const x: number = pos(d.getTime()); grid += `
`; if (d.getMonth() % 3 === 0) { labels += `${d.toLocaleDateString(navigator.language, { month: "short", year: "2-digit" })}`; } } const todayX: number = pos(now.getTime()); function barColor(p: WidgetPortfolioProject): string { if (p.termin === "red") return C.red; if (p.termin === "yellow") return C.amber; if (p.termin === "green") return C.green; return C.blue; } const bars: string = rows.slice(0, MAX_ROWS).map((r) => { const left: number = pos(r.s); const right: number = pos(r.e); const width: number = Math.max(right - left, 0.8); const color: string = barColor(r.p); const pct: number = Math.min(Math.max(r.p.progress, 0), 100); return `
${esc(r.p.name)} ${grid}
`; }).join(""); widget.el.innerHTML = `
${labels}
${bars}
${rows.length} laufende Projekte${rows.length > MAX_ROWS ? ` (${MAX_ROWS} dargestellt)` : ""} Kräftiger Balkenteil = Fortschritt Farbe = Terminampel
`; widget.done(); })();