// Quality-Gate-Cockpit // Quelle: widget.query("project.milestones") — gefiltert auf type "quality-gate". // Nur Quality Gates führen ein Entscheidungsdatum (actualDate). 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 `
${text}
`; } 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.", ); } /** "YYYY-MM-DD" als LOKALE Mitternacht lesen. Date.parse würde reine * Datumsangaben als UTC interpretieren — westlich von Greenwich zeigt die * Ausgabe sonst den Vortag an. */ 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"; } // Stufenleiste: ein Segment je Gate. const strip: string = gates.map((g) => `
`, ).join(""); const rows: string = gates.map((g, i) => `
${i + 1} ${esc(g.name)} Plan ${dateShort(g.planDate)} ${label(g)} ${dateShort(g.actualDate)}
`).join(""); widget.el.innerHTML = `
${passed}/${gates.length} Gates entschieden ${late > 0 ? `${late} überfällig` : ""}
${strip}
GatePlan Status Entschieden
${rows}
`; widget.done(); })();