import { and, eq, ne, isNull, isNotNull, lt, count, asc, desc } from 'drizzle-orm'; import { area, project, blocker, dayEntry, user, risk, milestone, type Area, type Project } from '@cockpit/db'; import { router, protectedProcedure } from '../trpc.js'; import { todayIso, previousWerktag } from '../lib/werktag.js'; import { aggregateAreaAmpel } from '../lib/ampel.js'; export const dashboardRouter = router({ overview: protectedProcedure.query(async ({ ctx }) => { const todayDate = todayIso(); const [ areas, allProjects, openBlockers, todayPresence, totalActiveUsers, feedRows, showstopperRows, milestoneRows, ] = await Promise.all([ ctx.db.select().from(area).orderBy(area.order), ctx.db.select().from(project).where(eq(project.archiviert, false)), ctx.db.select().from(blocker).where(isNull(blocker.resolvedAt)), ctx.db .select({ userId: dayEntry.userId, status: dayEntry.status }) .from(dayEntry) .where(eq(dayEntry.date, todayDate)), ctx.db.select({ c: count() }).from(user).where(eq(user.active, true)), ctx.db .select({ id: dayEntry.id, userId: dayEntry.userId, userName: user.name, date: dayEntry.date, ampel: dayEntry.ampel, trend: dayEntry.trend, reflectionText: dayEntry.reflectionText, publishedAt: dayEntry.publishedAt, }) .from(dayEntry) .innerJoin(user, eq(user.id, dayEntry.userId)) // „Gestern im Team": volle Reflexionen vor heute (heutige bleiben privat // bis morgen). Unabhängig vom publish-Cron. .where(and(lt(dayEntry.date, todayDate), eq(dayEntry.status, 'full'))) .orderBy(desc(dayEntry.date), desc(dayEntry.reflectedAt)) .limit(50), ctx.db .select({ id: risk.id, title: risk.title, projectId: risk.projectId, projectName: project.name, }) .from(risk) .innerJoin(project, eq(project.id, risk.projectId)) .where( and( eq(risk.showstopper, true), eq(risk.kind, 'risiko'), ne(risk.status, 'geschlossen'), eq(project.archiviert, false), ), ), // Anstehende Meilensteine (offen/überfällig, mit Datum) projektübergreifend. ctx.db .select({ id: milestone.id, title: milestone.title, due: milestone.due, status: milestone.status, projectId: milestone.projectId, projectName: project.name, areaId: project.areaId, }) .from(milestone) .innerJoin(project, eq(project.id, milestone.projectId)) .where( and( ne(milestone.status, 'done'), isNotNull(milestone.due), eq(project.archiviert, false), ), ) .orderBy(asc(milestone.due)) .limit(15), ]); const projectsByArea = new Map(); for (const p of allProjects) { const list = projectsByArea.get(p.areaId) ?? []; list.push(p); projectsByArea.set(p.areaId, list); } const areaSummaries = areas.map((a: Area) => { const projs = projectsByArea.get(a.id) ?? []; const agg = aggregateAreaAmpel(projs); return { id: a.id, slug: a.slug, name: a.name, kind: a.kind, ampel: agg.ampel, counts: agg.counts, }; }); const presentToday = new Set(todayPresence.map((p) => p.userId)); const usersAll = await ctx.db .select({ id: user.id, name: user.name }) .from(user) .where(eq(user.active, true)); const fehlende = usersAll.filter((u) => !presentToday.has(u.id)); return { date: todayDate, areas: areaSummaries, blockers: openBlockers.map((b) => ({ id: b.id, title: b.title, openedAt: b.openedAt, areaId: b.areaId, projectId: b.projectId, })), heuteQuote: { gepingt: presentToday.size, gesamt: totalActiveUsers[0]?.c ?? 0, fehlende, }, jourfixe: { nextDate: nextJourfixe(todayDate), label: 'Di 11:00' }, showstoppers: showstopperRows, meilensteine: milestoneRows.map((m) => ({ ...m, ueberfaellig: m.due != null && m.due < todayDate, })), kritischeModi: allProjects .filter((p) => p.modus === 'eskalation' || p.modus === 'taskforce') .map((p) => ({ id: p.id, name: p.name, areaId: p.areaId, modus: p.modus })), pflichtMinimumOffen: allProjects .filter((p) => Object.values(p.pflichtMinimumState).some((v) => v === false)) .map((p) => ({ id: p.id, name: p.name, areaId: p.areaId })), feed: feedRows, gestern: previousWerktag(todayDate), }; }), }); function nextJourfixe(todayIsoDate: string): string { // Nächster Dienstag (Default Jourfixe). Wenn heute Dienstag, dann heute. const d = new Date(`${todayIsoDate}T00:00:00Z`); const wd = d.getUTCDay(); // 0=So..6=Sa const offset = (2 - wd + 7) % 7 || 0; // 2=Di d.setUTCDate(d.getUTCDate() + offset); return d.toISOString().slice(0, 10); }