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
const isDark: boolean = widget.data.theme === "dark";
const C = {
text: isDark ? "#f1f5f9" : "#1e293b",
sub: "#94a3b8",
card: isDark ? "#1e293b" : "#f8fafc",
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."
: "Daten konnten nicht geladen werden.",
);
}
function initials(name: string): string {
return name.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0].toUpperCase()).join("");
}
widget.el.innerHTML = note("Werte Verantwortlichkeiten aus …");
(async () => {
const res = await widget.query("project.containers", { limit: 300 });
if (!res.ok) {
widget.el.innerHTML = queryError(res.error.code);
widget.done();
return;
}
const UNASSIGNED = "Ohne Verantwortlichen";
const acc = new Map<string, { open: number; overdue: number; elements: number }>();
res.data.items.forEach((c) => {
if (c.type !== "container") return;
const key: string = c.responsibleName && c.responsibleName.trim() !== ""
? c.responsibleName
: UNASSIGNED;
const cur = acc.get(key) || { open: 0, overdue: 0, elements: 0 };
cur.open += c.openTaskCount;
cur.overdue += c.overdueTaskCount;
cur.elements += 1;
acc.set(key, cur);
});
const rows = Array.from(acc.entries())
.map((e) => ({ name: e[0], open: e[1].open, overdue: e[1].overdue, elements: e[1].elements }))
.filter((r) => r.open > 0 || r.overdue > 0 || r.name === UNASSIGNED)
.sort((a, b) => (b.overdue - a.overdue) || (b.open - a.open));
if (rows.length === 0) {
widget.el.innerHTML = note("Keine offenen Aufgaben im Strukturplan.");
widget.done();
return;
}
const max: number = Math.max.apply(null, rows.map((r) => r.open)) || 1;
const list: string = rows.slice(0, 10).map((r) => {
const isNobody: boolean = r.name === UNASSIGNED;
const w: number = Math.max(2, (r.open / max) * 100);
const overduePart: number = r.open > 0 ? Math.min((r.overdue / r.open) * 100, 100) : 0;
return `<div style="display:flex;align-items:center;gap:8px;padding:5px 0;border-bottom:1px solid ${C.line}">
<span style="width:24px;height:24px;flex-shrink:0;border-radius:50%;background:${isNobody ? C.line : C.blue + "22"};color:${isNobody ? C.sub : C.blue};display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700">${isNobody ? "?" : esc(initials(r.name))}</span>
<div style="flex:1;min-width:0">
<div style="display:flex;justify-content:space-between;gap:8px;font-size:11px;margin-bottom:3px">
<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:${isNobody ? C.sub : C.text};${isNobody ? "font-style:italic" : ""}">${esc(r.name)}</span>
<span style="color:${C.sub};white-space:nowrap">${r.elements} Elem. · ${r.open} offen${r.overdue > 0 ? ` · <span style="color:${C.red};font-weight:600">${r.overdue} überfällig</span>` : ""}</span>
</div>
<div style="height:5px;background:${C.line};border-radius:3px;overflow:hidden;position:relative">
<div style="height:100%;width:${w.toFixed(0)}%;background:${C.blue};opacity:.6"></div>
<div style="position:absolute;left:0;top:0;height:100%;width:${(w * overduePart / 100).toFixed(0)}%;background:${C.red}"></div>
</div>
</div>
</div>`;
}).join("");
const nobody = acc.get(UNASSIGNED);
widget.el.innerHTML = `
<div style="padding:8px 12px;color:${C.text}">
<div style="font-size:10px;color:${C.sub};margin-bottom:6px">
Offene Aufgaben je Verantwortlichem — roter Anteil ist überfällig.
${nobody ? ` ${nobody.elements} Strukturelemente ohne Zuordnung.` : ""}
</div>
${list}
${rows.length > 10 ? `<div style="font-size:10px;color:${C.sub};margin-top:5px">${rows.length - 10} weitere Personen nicht dargestellt.</div>` : ""}
</div>`;
widget.done();
})();