1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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 `<div style="display:flex;align-items:center;justify-content:center;height:100%;min-height:60px;padding:12px;font-size:12px;color:${C.sub};text-align:center">${text}</div>`;
}
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<string, WidgetContainer>();
items.forEach((c) => byId.set(c.id, c));
function depth(c: WidgetContainer): number {
let d = 0;
let cur: WidgetContainer | undefined = c;
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 `<div style="display:flex;align-items:center;gap:6px;padding:3px 0;font-size:11px;padding-left:${d * 12}px">
<span style="color:${C.sub};font-variant-numeric:tabular-nums;white-space:nowrap;min-width:34px">${esc(c.pspCode)}</span>
<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:${C.text};font-weight:${weight}">${esc(c.name)}</span>
${c.overdueTaskCount > 0 ? `<span style="flex-shrink:0;background:${C.red}22;color:${C.red};font-size:9px;font-weight:700;padding:1px 5px;border-radius:8px">${c.overdueTaskCount} überfällig</span>` : ""}
<span style="width:64px;flex-shrink:0;height:5px;background:${C.line};border-radius:3px;overflow:hidden">
<span style="display:block;height:100%;width:${pct}%;background:${bar}"></span>
</span>
<span style="width:30px;flex-shrink:0;text-align:right;color:${C.sub};font-variant-numeric:tabular-nums">${pct}%</span>
</div>`;
}).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 = `
<div style="padding:8px 12px;color:${C.text}">
<div style="display:flex;gap:12px;font-size:10px;color:${C.sub};padding-bottom:6px;margin-bottom:4px;border-bottom:1px solid ${C.line}">
<span>${items.length} Elemente</span>
<span>${openTotal} offene Aufgaben</span>
${overdueTotal > 0 ? `<span style="color:${C.red};font-weight:600">${overdueTotal} überfällig</span>` : ""}
${res.data.truncated ? `<span style="margin-left:auto">Auszug</span>` : ""}
</div>
${rows}
</div>`;
widget.done();
})();