ProjectRecommended 6 × 8Quality and auditPMO

Quality gate cockpit

Separates quality gates from ordinary milestones and shows them as a ladder with decision dates: passed, pending, overdue.

QUALITY GATES2 / 5G1G2G3!G4·G5·passedoverduepending

A rebuild in this website's style. The real tile picks up your tenant's fonts and color scheme.

What it is good for

In phase models an undecided gate holds up the entire project - and that is exactly what gets lost in a mixed milestone list. This tile filters on the quality gate type and shows only those, as a ladder in their proper order. Only quality gates carry a decision date; that is how the tile tells a passed gate from one still waiting on a decision that was due long ago.

What the tile shows you

  • The ladder shows every quality gate in order, from bottom left to top right.
  • Each node carries its state: passed, awaiting a decision, or overdue.
  • Below each node is the decision date, where one already exists.

The complete code

Self-contained, with no external library. Paste it into the editor, save, done.

TypeScript111 lines
Raw file
// 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. 1Create the widget
  2. 2Paste the code
  3. 3Set the size and save

Build your own tile

Try WORKSPACE.PM for thirty days with every feature. The templates on this page work in a trial tenant exactly as they do in production.