WORKSPACE.PM
ProjektEmpfohlen 6 × 10ProjektleitungProjektteam

Strukturplan-Fortschrittsbaum

Zeigt den Projektstrukturplan als eingerückte Liste mit PSP-Code, Fortschrittsbalken und einem Zähler für überfällige Aufgaben je Element.

Zum Code
STRUKTURPLAN58 %178 %1.1100 %1.2342 %255 %2.161 %2.1.1512 %

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

Wofür das gut ist

Ein Gesamtfortschritt von 58 Prozent sagt nicht, welcher Ast bremst. Diese Kachel bildet den Strukturplan als eingerückte Liste ab - PSP-Code, Fortschrittsbalken, und je Element die Zahl der überfälligen Aufgaben. Die Reihenfolge folgt dem PSP-Code in natürlicher Sortierung, 1.2 steht also vor 1.10. In wenigen Sekunden ist sichtbar, wo der Rückstand tatsächlich sitzt.

Was Sie in der Kachel sehen

  • Die Einrückung bildet die Ebene im Strukturplan ab, davor steht der PSP-Code.
  • Der Balken ist der Fortschritt des Elements einschließlich seiner Unterelemente.
  • Der rote Zähler nennt die überfälligen Aufgaben an genau diesem Element.

Der vollständige Code

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

TypeScript114 Zeilen
Rohdatei
// Strukturplan-Fortschrittsbaum
// Quelle: widget.query("project.containers")
// Reihenfolge über den PSP-Code (natürliche Sortierung: 1.2 vor 1.10).

const isDark: boolean = widget.data.theme === "dark";
const C = {
  text: isDark ? "#f1f5f9" : "#1e293b",
  sub: "#94a3b8",
  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 === "&" ? "&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 den Strukturplan."
      : code === "unsupported_in_context" ? "Nur auf Projektebene verfügbar."
      : "Strukturplan konnte nicht geladen werden.",
  );
}

function pspKey(code: string): number[] {
  return String(code).split(".").map((part) => {
    const n: number = parseInt(part, 10);
    return Number.isNaN(n) ? 0 : n;
  });
}

function pspCompare(a: string, b: string): number {
  const ka = pspKey(a), kb = pspKey(b);
  const len: number = Math.max(ka.length, kb.length);
  for (let i = 0; i < len; i++) {
    const d: number = (ka[i] || 0) - (kb[i] || 0);
    if (d !== 0) return d;
  }
  return String(a).localeCompare(String(b));
}

widget.el.innerHTML = note("Lade Strukturplan …");

(async () => {
  const res = await widget.query("project.containers", { limit: 300 });
  if (!res.ok) {
    widget.el.innerHTML = queryError(res.error.code);
    widget.done();
    return;
  }

  const items = res.data.items.filter((c) => c.type === "container");
  if (items.length === 0) {
    widget.el.innerHTML = note("Kein Strukturplan hinterlegt.");
    widget.done();
    return;
  }

  const byId = new Map<string, WidgetContainer>();
  items.forEach((c) => byId.set(c.id, c));

  function depth(c: WidgetContainer): number {
    let d = 0;
    let cur: WidgetContainer | undefined = c;
    // Schutz gegen zyklische Daten: maximal 8 Ebenen hochlaufen.
    while (cur && cur.parentId && d < 8) {
      cur = byId.get(cur.parentId);
      if (!cur) break;
      d++;
    }
    return d;
  }

  const sorted = items.slice().sort((a, b) => pspCompare(a.pspCode, b.pspCode));

  const rows: string = sorted.map((c) => {
    const d: number = depth(c);
    const pct: number = Math.min(Math.max(c.progress, 0), 100);
    const bar: string = c.overdueTaskCount > 0 ? C.red : pct >= 100 ? C.green : C.blue;
    const weight: string = d === 0 ? "600" : "400";
    return `<div style="display:flex;align-items:center;gap:6px;padding:3px 0;font-size:11px;padding-left:${d * 12}px">
      <span style="color:${C.sub};font-variant-numeric:tabular-nums;white-space:nowrap;min-width:34px">${esc(c.pspCode)}</span>
      <span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:${C.text};font-weight:${weight}">${esc(c.name)}</span>
      ${c.overdueTaskCount > 0 ? `<span style="flex-shrink:0;background:${C.red}22;color:${C.red};font-size:9px;font-weight:700;padding:1px 5px;border-radius:8px">${c.overdueTaskCount} überfällig</span>` : ""}
      <span style="width:64px;flex-shrink:0;height:5px;background:${C.line};border-radius:3px;overflow:hidden">
        <span style="display:block;height:100%;width:${pct}%;background:${bar}"></span>
      </span>
      <span style="width:30px;flex-shrink:0;text-align:right;color:${C.sub};font-variant-numeric:tabular-nums">${pct}%</span>
    </div>`;
  }).join("");

  const overdueTotal: number = items.reduce((s, c) => s + c.overdueTaskCount, 0);
  const openTotal: number = items.reduce((s, c) => s + c.openTaskCount, 0);

  widget.el.innerHTML = `
    <div style="padding:8px 12px;color:${C.text}">
      <div style="display:flex;gap:12px;font-size:10px;color:${C.sub};padding-bottom:6px;margin-bottom:4px;border-bottom:1px solid ${C.line}">
        <span>${items.length} Elemente</span>
        <span>${openTotal} offene Aufgaben</span>
        ${overdueTotal > 0 ? `<span style="color:${C.red};font-weight:600">${overdueTotal} überfällig</span>` : ""}
        ${res.data.truncated ? `<span style="margin-left:auto">Auszug</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.