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
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",
};
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 die Termindaten."
: code === "unsupported_in_context" ? "Nur auf Projektebene verfügbar."
: "Quality Gates konnten nicht geladen werden.",
);
}
function dayStart(iso: string): number {
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso || "");
return m ? new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])).getTime() : NaN;
}
function dateShort(iso: string | undefined): string {
if (!iso) return "—";
const t: number = dayStart(iso);
return Number.isNaN(t) ? "—" : new Date(t).toLocaleDateString(navigator.language, {
day: "2-digit", month: "2-digit", year: "2-digit",
});
}
widget.el.innerHTML = note("Lade Quality Gates …");
(async () => {
const res = await widget.query("project.milestones");
if (!res.ok) {
widget.el.innerHTML = queryError(res.error.code);
widget.done();
return;
}
const gates = res.data.items
.filter((m) => m.type === "quality-gate")
.slice()
.sort((a, b) => (Date.parse(a.planDate) || 0) - (Date.parse(b.planDate) || 0));
if (gates.length === 0) {
widget.el.innerHTML = note("Für dieses Projekt sind keine Quality Gates definiert.");
widget.done();
return;
}
const passed = gates.filter((g) => g.status === "completed").length;
const late = gates.filter((g) => g.status === "overdue").length;
function color(g: WidgetMilestone): string {
return g.status === "completed" ? C.green : g.status === "overdue" ? C.red : C.sub;
}
function label(g: WidgetMilestone): string {
return g.status === "completed" ? "entschieden" : g.status === "overdue" ? "überfällig" : "offen";
}
const strip: string = gates.map((g) =>
`<div style="flex:1;height:6px;background:${color(g)};border-radius:3px"></div>`,
).join("");
const rows: string = gates.map((g, i) => `
<div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid ${C.line};font-size:11px">
<span style="width:18px;height:18px;flex-shrink:0;display:flex;align-items:center;justify-content:center;border-radius:4px;background:${color(g)};color:#fff;font-size:10px;font-weight:700">${i + 1}</span>
<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:${C.text}">${esc(g.name)}</span>
<span style="color:${C.sub};white-space:nowrap">Plan ${dateShort(g.planDate)}</span>
<span style="color:${color(g)};font-weight:600;white-space:nowrap;min-width:74px;text-align:right">${label(g)}</span>
<span style="color:${C.sub};white-space:nowrap;min-width:56px;text-align:right">${dateShort(g.actualDate)}</span>
</div>`).join("");
widget.el.innerHTML = `
<div style="padding:10px 12px;color:${C.text}">
<div style="display:flex;align-items:baseline;gap:8px;margin-bottom:8px">
<span style="font-size:22px;font-weight:700">${passed}<span style="font-size:13px;color:${C.sub}">/${gates.length}</span></span>
<span style="font-size:11px;color:${C.sub}">Gates entschieden</span>
${late > 0 ? `<span style="margin-left:auto;font-size:11px;font-weight:600;color:${C.red}">${late} überfällig</span>` : ""}
</div>
<div style="display:flex;gap:3px;margin-bottom:10px">${strip}</div>
<div style="display:flex;gap:8px;font-size:9px;color:${C.sub};text-transform:uppercase;letter-spacing:.04em;padding-bottom:4px">
<span style="width:18px"></span><span style="flex:1">Gate</span><span>Plan</span>
<span style="min-width:74px;text-align:right">Status</span>
<span style="min-width:56px;text-align:right">Entschieden</span>
</div>
${rows}
</div>`;
widget.done();
})();