WORKSPACE.PM
ProjektEmpfohlen 6 × 8Qualität und RevisionPMO

Quality-Gate-Cockpit

Trennt Quality Gates von normalen Meilensteinen und zeigt sie als Stufenleiste mit Entscheidungsdatum: bestanden, offen, überfällig.

Zum Code
QUALITY GATES2 / 5G1G2G3!G4·G5·bestandenüberfälligoffen

Nachbau im Stil dieser Website. Die echte Kachel übernimmt Schriften und Farbschema Ihres Mandanten.

Wofür das gut ist

In Phasenmodellen hält ein nicht entschiedenes Gate das ganze Projekt an - und genau das geht in einer gemischten Meilensteinliste unter. Diese Kachel filtert auf den Typ Quality Gate und zeigt nur diese, als Leiter in ihrer Reihenfolge. Nur Quality Gates führen ein Entscheidungsdatum; daran erkennt die Kachel, welches Gate bestanden ist und welches auf eine Entscheidung wartet, die längst fällig war.

Was Sie in der Kachel sehen

  • Die Leiter zeigt alle Quality Gates in ihrer Reihenfolge, von links unten nach rechts oben.
  • Jeder Knoten trägt seinen Zustand: bestanden, auf Entscheidung wartend oder überfällig.
  • Unter jedem Knoten steht das Entscheidungsdatum, sofern es bereits eines gibt.

Der vollständige Code

Eigenständig und ohne externe Bibliothek. In den Editor einfügen, speichern, fertig.

TypeScript111 Zeilen
Rohdatei
// 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 === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&quot;");
}

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.",
  );
}

/** "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) =>
    `<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();
})();
  1. 1Widget anlegen
  2. 2Code einfügen
  3. 3Größe setzen und speichern

Bauen Sie Ihre eigene Kachel

Testen Sie WORKSPACE.PM dreißig Tage lang mit allen Funktionen. Die Vorlagen dieser Seite funktionieren im Testmandanten genauso wie in der Produktivumgebung.