import { and, eq, isNotNull, isNull, count } from 'drizzle-orm'; import { area, project, blocker, dayEntry, user, 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] = 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)) .where(and(isNotNull(dayEntry.publishedAt), eq(dayEntry.status, 'full'))) .orderBy(dayEntry.publishedAt) .limit(50), ]); 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' }, 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); }