ProjectRecommended 4 × 9Project managementProject team

Ownership load across the structure

Groups open and overdue tasks by the responsible person across every element of the work breakdown structure.

OPEN TASKS PER PERSON371510115no owner6openoverdue

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

What it is good for

'On whose desk is it piling up' is hard to read out of a task list. This tile turns the view around: it aggregates across all structure elements per owner and shows open and overdue tasks in a single bar. Elements with no owner assigned deliberately get their own row - that is not a person but a planning gap, and it should be visible.

What the tile shows you

  • One row per responsible person, with the sum of their tasks.
  • The bar splits into open and overdue tasks.
  • The final, dashed row collects every structure element without an owner.

The complete code

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

TypeScript108 lines
Raw file
// Verantwortungslast im Strukturplan
// Quelle: widget.query("project.containers")
// Aggregation je Verantwortlichem; Elemente ohne Zuordnung werden
// bewusst als eigene Zeile ausgewiesen (Planungslücke).

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",
  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."
      : "Daten konnten nicht geladen werden.",
  );
}

function initials(name: string): string {
  return name.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0].toUpperCase()).join("");
}

widget.el.innerHTML = note("Werte Verantwortlichkeiten aus …");

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

  const UNASSIGNED = "Ohne Verantwortlichen";
  const acc = new Map<string, { open: number; overdue: number; elements: number }>();

  res.data.items.forEach((c) => {
    if (c.type !== "container") return;
    const key: string = c.responsibleName && c.responsibleName.trim() !== ""
      ? c.responsibleName
      : UNASSIGNED;
    const cur = acc.get(key) || { open: 0, overdue: 0, elements: 0 };
    cur.open += c.openTaskCount;
    cur.overdue += c.overdueTaskCount;
    cur.elements += 1;
    acc.set(key, cur);
  });

  const rows = Array.from(acc.entries())
    .map((e) => ({ name: e[0], open: e[1].open, overdue: e[1].overdue, elements: e[1].elements }))
    .filter((r) => r.open > 0 || r.overdue > 0 || r.name === UNASSIGNED)
    .sort((a, b) => (b.overdue - a.overdue) || (b.open - a.open));

  if (rows.length === 0) {
    widget.el.innerHTML = note("Keine offenen Aufgaben im Strukturplan.");
    widget.done();
    return;
  }

  const max: number = Math.max.apply(null, rows.map((r) => r.open)) || 1;

  const list: string = rows.slice(0, 10).map((r) => {
    const isNobody: boolean = r.name === UNASSIGNED;
    const w: number = Math.max(2, (r.open / max) * 100);
    const overduePart: number = r.open > 0 ? Math.min((r.overdue / r.open) * 100, 100) : 0;
    return `<div style="display:flex;align-items:center;gap:8px;padding:5px 0;border-bottom:1px solid ${C.line}">
      <span style="width:24px;height:24px;flex-shrink:0;border-radius:50%;background:${isNobody ? C.line : C.blue + "22"};color:${isNobody ? C.sub : C.blue};display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700">${isNobody ? "?" : esc(initials(r.name))}</span>
      <div style="flex:1;min-width:0">
        <div style="display:flex;justify-content:space-between;gap:8px;font-size:11px;margin-bottom:3px">
          <span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:${isNobody ? C.sub : C.text};${isNobody ? "font-style:italic" : ""}">${esc(r.name)}</span>
          <span style="color:${C.sub};white-space:nowrap">${r.elements} Elem. · ${r.open} offen${r.overdue > 0 ? ` · <span style="color:${C.red};font-weight:600">${r.overdue} überfällig</span>` : ""}</span>
        </div>
        <div style="height:5px;background:${C.line};border-radius:3px;overflow:hidden;position:relative">
          <div style="height:100%;width:${w.toFixed(0)}%;background:${C.blue};opacity:.6"></div>
          <div style="position:absolute;left:0;top:0;height:100%;width:${(w * overduePart / 100).toFixed(0)}%;background:${C.red}"></div>
        </div>
      </div>
    </div>`;
  }).join("");

  const nobody = acc.get(UNASSIGNED);

  widget.el.innerHTML = `
    <div style="padding:8px 12px;color:${C.text}">
      <div style="font-size:10px;color:${C.sub};margin-bottom:6px">
        Offene Aufgaben je Verantwortlichem — roter Anteil ist überfällig.
        ${nobody ? ` ${nobody.elements} Strukturelemente ohne Zuordnung.` : ""}
      </div>
      ${list}
      ${rows.length > 10 ? `<div style="font-size:10px;color:${C.sub};margin-top:5px">${rows.length - 10} weitere Personen nicht dargestellt.</div>` : ""}
    </div>`;
  widget.done();
})();

Adjusting it

ConstantDefaultMeaning
UNASSIGNED"Ohne Verantwortlichen"Label for the row holding structure elements without an owner.
  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.