// Nutzen-Dringlichkeits-Matrix // Quelle: widget.query("portfolio.projects") // Skala von Dringlichkeit und Nutzen ist 1–10; die Trennlinien liegen bei 5,5. const CURRENCY = "EUR"; // an die Mandantenwährung anpassen const isDark: boolean = widget.data.theme === "dark"; const C = { text: isDark ? "#f1f5f9" : "#1e293b", sub: "#94a3b8", line: isDark ? "#334155" : "#e2e8f0", quad: isDark ? "#1e293b" : "#f8fafc", green: "#22c55e", amber: "#f59e0b", red: "#ef4444", blue: "#3b82f6", }; const MAX_PAGES = 3; const CUR: string = new Intl.NumberFormat(navigator.language, { style: "currency", currency: CURRENCY, }).formatToParts(0).filter((x) => x.type === "currency").map((x) => x.value)[0] || CURRENCY; function esc(s: string): string { return String(s).replace(/[&<>"]/g, (c: string) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """); } function note(text: string): string { return `
${text}
`; } function short(v: number): string { const a: number = Math.abs(v); if (a >= 1000000) return (v / 1000000).toLocaleString(navigator.language, { maximumFractionDigits: 1 }) + " Mio. " + CUR; if (a >= 10000) return Math.round(v / 1000).toLocaleString(navigator.language) + " Tsd. " + CUR; return Math.round(v).toLocaleString(navigator.language) + " " + CUR; } async function loadProjects() { const items: WidgetPortfolioProject[] = []; let cursor: string | undefined = undefined; let total: number | null = null; for (let page = 0; page < MAX_PAGES; page++) { const res = await widget.query("portfolio.projects", { limit: 200, cursor: cursor }); if (!res.ok) return { error: res.error.code, items: items, total: total, complete: false }; if (total === null) total = res.data.total; res.data.items.forEach((p) => items.push(p)); if (!res.data.nextCursor) return { error: null, items: items, total: total, complete: true }; cursor = res.data.nextCursor; } return { error: null, items: items, total: total, complete: false }; } widget.el.innerHTML = note("Lade Bewertungen …"); (async () => { const loaded = await loadProjects(); if (loaded.error) { widget.el.innerHTML = note( loaded.error === "forbidden" ? "Keine Berechtigung für die Portfoliodaten." : loaded.error === "unsupported_in_context" ? "Nur auf Portfolioebene verfügbar." : "Projekte konnten nicht geladen werden.", ); widget.done(); return; } const rated = loaded.items.filter((p) => p.urgency > 0 && p.benefitRating > 0); if (rated.length === 0) { widget.el.innerHTML = note("Für keines der Projekte sind Dringlichkeit und Nutzen bewertet."); widget.done(); return; } const W = 320, H = 210, PAD = 22; const maxBudget: number = Math.max.apply(null, rated.map((p) => p.budget)) || 1; function sx(v: number): number { return PAD + ((v - 1) / 9) * (W - PAD - 10); } function sy(v: number): number { return H - PAD - ((v - 1) / 9) * (H - PAD - 10); } function radius(budget: number): number { return 3 + Math.sqrt(Math.max(budget, 0) / maxBudget) * 9; } const midX: number = sx(5.5); const midY: number = sy(5.5); const dots: string = rated.map((p) => { const color: string = p.benefitRating >= 5.5 ? (p.urgency >= 5.5 ? C.green : C.blue) : (p.urgency >= 5.5 ? C.amber : C.sub); return ``; }).join(""); function quadrant(label: string, count: number, budget: number, color: string): string { return `
${count}
${label}
${short(budget)}
`; } function bucket(hiU: boolean, hiB: boolean) { const set = rated.filter((p) => (p.urgency >= 5.5) === hiU && (p.benefitRating >= 5.5) === hiB); return { count: set.length, budget: set.reduce((s, p) => s + p.budget, 0) }; } const qA = bucket(true, true); const qB = bucket(false, true); const qC = bucket(true, false); const qD = bucket(false, false); widget.el.innerHTML = `
${dots} Dringlichkeit → Nutzen ↑
${quadrant("Jetzt umsetzen", qA.count, qA.budget, C.green)} ${quadrant("Einplanen", qB.count, qB.budget, C.blue)} ${quadrant("Hinterfragen", qC.count, qC.budget, C.amber)} ${quadrant("Zurückstellen", qD.count, qD.budget, C.sub)}
${rated.length} bewertete Projekte, Blasengröße = Budget. ${loaded.items.length - rated.length > 0 ? ` ${loaded.items.length - rated.length} ohne Bewertung ausgelassen.` : ""}
`; widget.done(); })();