// Strukturplan-Fortschrittsbaum // Quelle: widget.query("project.containers") // Reihenfolge über den PSP-Code (natürliche Sortierung: 1.2 vor 1.10). const isDark: boolean = widget.data.theme === "dark"; const C = { text: isDark ? "#f1f5f9" : "#1e293b", sub: "#94a3b8", line: isDark ? "#334155" : "#e2e8f0", green: "#22c55e", amber: "#f59e0b", red: "#ef4444", blue: "#3b82f6", }; function esc(s: string): string { return String(s).replace(/[&<>"]/g, (c: string) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """); } function note(text: string): string { return `
${text}
`; } function queryError(code: string): string { return note( code === "forbidden" ? "Keine Berechtigung für den Strukturplan." : code === "unsupported_in_context" ? "Nur auf Projektebene verfügbar." : "Strukturplan konnte nicht geladen werden.", ); } function pspKey(code: string): number[] { return String(code).split(".").map((part) => { const n: number = parseInt(part, 10); return Number.isNaN(n) ? 0 : n; }); } function pspCompare(a: string, b: string): number { const ka = pspKey(a), kb = pspKey(b); const len: number = Math.max(ka.length, kb.length); for (let i = 0; i < len; i++) { const d: number = (ka[i] || 0) - (kb[i] || 0); if (d !== 0) return d; } return String(a).localeCompare(String(b)); } widget.el.innerHTML = note("Lade Strukturplan …"); (async () => { const res = await widget.query("project.containers", { limit: 300 }); if (!res.ok) { widget.el.innerHTML = queryError(res.error.code); widget.done(); return; } const items = res.data.items.filter((c) => c.type === "container"); if (items.length === 0) { widget.el.innerHTML = note("Kein Strukturplan hinterlegt."); widget.done(); return; } const byId = new Map(); items.forEach((c) => byId.set(c.id, c)); function depth(c: WidgetContainer): number { let d = 0; let cur: WidgetContainer | undefined = c; // Schutz gegen zyklische Daten: maximal 8 Ebenen hochlaufen. while (cur && cur.parentId && d < 8) { cur = byId.get(cur.parentId); if (!cur) break; d++; } return d; } const sorted = items.slice().sort((a, b) => pspCompare(a.pspCode, b.pspCode)); const rows: string = sorted.map((c) => { const d: number = depth(c); const pct: number = Math.min(Math.max(c.progress, 0), 100); const bar: string = c.overdueTaskCount > 0 ? C.red : pct >= 100 ? C.green : C.blue; const weight: string = d === 0 ? "600" : "400"; return `
${esc(c.pspCode)} ${esc(c.name)} ${c.overdueTaskCount > 0 ? `${c.overdueTaskCount} überfällig` : ""} ${pct}%
`; }).join(""); const overdueTotal: number = items.reduce((s, c) => s + c.overdueTaskCount, 0); const openTotal: number = items.reduce((s, c) => s + c.openTaskCount, 0); widget.el.innerHTML = `
${items.length} Elemente ${openTotal} offene Aufgaben ${overdueTotal > 0 ? `${overdueTotal} überfällig` : ""} ${res.data.truncated ? `Auszug` : ""}
${rows}
`; widget.done(); })();