// Berichtsdisziplin // Quelle: widget.query("portfolio.projects") — reportDate ist leer, // wenn zum Projekt noch nie ein Bericht erstellt wurde. const STALE_DAYS = 30; // ab hier gilt ein Bericht als überfällig const DUE_DAYS = 14; // ab hier wird er fällig 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", }; const DAY = 86400000; const MAX_PAGES = 3; function esc(s: string): string { return String(s).replace(/[&<>"]/g, (c: string) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """); } function note(text: string): string { return `
${text}
`; } /** "YYYY-MM-DD" als LOKALE Mitternacht lesen. Date.parse würde reine * Datumsangaben als UTC interpretieren — Tagesdifferenzen kippen dann je * nach Zeitzone um einen Tag. */ 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 ageInDays(iso: string): number | null { if (!iso || iso === "") return null; const t: number = dayStart(iso); if (Number.isNaN(t)) return null; const today = new Date(); today.setHours(0, 0, 0, 0); return Math.max(0, Math.round((today.getTime() - t) / DAY)); } 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("Prüfe Berichtsstände …"); (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 active = loaded.items.filter((p) => !p.actualEnd || p.actualEnd === ""); if (active.length === 0) { widget.el.innerHTML = note("Keine laufenden Projekte im Portfolio."); widget.done(); return; } const scored = active.map((p) => ({ p: p, age: ageInDays(p.reportDate) })); const never = scored.filter((x) => x.age === null); const stale = scored.filter((x) => x.age !== null && (x.age as number) > STALE_DAYS); const due = scored.filter((x) => x.age !== null && (x.age as number) > DUE_DAYS && (x.age as number) <= STALE_DAYS); const fresh = scored.filter((x) => x.age !== null && (x.age as number) <= DUE_DAYS); const quote: number = Math.round((fresh.length / active.length) * 100); function tile(label: string, count: number, color: string): string { return `
${count}
${label}
`; } // Sortierung: nie berichtet zuerst, dann nach Alter absteigend. const worst = never .concat(stale.slice().sort((a, b) => (b.age as number) - (a.age as number))) .concat(due.slice().sort((a, b) => (b.age as number) - (a.age as number))) .slice(0, 7); const rows: string = worst.map((x) => { const age = x.age; const color: string = age === null ? C.red : age > STALE_DAYS ? C.red : C.amber; const txt: string = age === null ? "nie berichtet" : "vor " + age + " Tagen"; return `
${esc(x.p.name)} ${esc(x.p.projectLeadName || "ohne Leitung")} ${txt}
`; }).join(""); widget.el.innerHTML = `
${quote} % der ${active.length} laufenden Projekte sind aktuell berichtet (≤ ${DUE_DAYS} Tage)
${tile("aktuell", fresh.length, C.green)} ${tile("fällig", due.length, C.amber)} ${tile("überfällig", stale.length, C.red)} ${tile("nie berichtet", never.length, C.red)}
${worst.length === 0 ? note("Alle Projekte sind aktuell berichtet.") : `
Nachfassliste
${rows}`} ${!loaded.complete ? `
Auswertung über ${loaded.items.length} von ${loaded.total ?? "?"} Projekten.
` : ""}
`; widget.done(); })();