// Budget-Konzentration (Pareto) // Quelle: widget.query("portfolio.projects") // Balken = Einzelbudget der größten Projekte, Linie = kumulierter Anteil. const CURRENCY = "EUR"; // an die Mandantenwährung anpassen const TOP_N = 12; const isDark: boolean = widget.data.theme === "dark"; const C = { text: isDark ? "#f1f5f9" : "#1e293b", sub: "#94a3b8", line: isDark ? "#334155" : "#e2e8f0", blue: "#3b82f6", amber: "#f59e0b", red: "#ef4444", green: "#22c55e", }; 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("Berechne Budgetverteilung …"); (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 funded = loaded.items.filter((p) => p.budget > 0).sort((a, b) => b.budget - a.budget); if (funded.length === 0) { widget.el.innerHTML = note("Für kein Projekt ist ein Budget hinterlegt."); widget.done(); return; } const total: number = funded.reduce((s, p) => s + p.budget, 0); // Wie viele Projekte machen 80 Prozent des Budgets aus? let running = 0; let count80 = 0; for (let i = 0; i < funded.length; i++) { running += funded[i].budget; count80 = i + 1; if (running / total >= 0.8) break; } const shareOfCount: number = Math.round((count80 / funded.length) * 100); const shown = funded.slice(0, TOP_N); const maxBudget: number = shown[0].budget; let cum = 0; const rows: string = shown.map((p, i) => { cum += p.budget; const cumPct: number = (cum / total) * 100; const w: number = (p.budget / maxBudget) * 100; const inTop80: boolean = i < count80; return `
${esc(p.name)} ${short(p.budget)} ${cumPct.toFixed(0)}%
`; }).join(""); widget.el.innerHTML = `
${count80} von ${funded.length} Projekten binden 80 % des Budgets (${shareOfCount} % der Projekte, ${short(total)} gesamt)
ProjektBudget Betragkum.
${rows} ${funded.length > TOP_N ? `
${funded.length - TOP_N} weitere Projekte mit kleinerem Budget.
` : ""} ${!loaded.complete ? `
Auswertung über ${loaded.items.length} von ${loaded.total ?? "?"} Projekten.
` : ""}
`; widget.done(); })();