Files
everythingbutwork/apps/api/src/routers/dashboard.ts
T
dejanandClaude Opus 4.7 c8fc024ca0
ci / quality (push) Has been cancelled
ci / build (push) Has been cancelled
Puls → Dashboard umbenennen + Projekt-Beschreibung
UI/Routing
- Tab + Route: /puls → /dashboard, Heading „Unternehmens-Puls" → „Dashboard"
- Shortcut: g p → g d (Cheatsheet entsprechend)
- Breadcrumbs, 404-Hinweistext, alle internen Links angepasst

Code-Layout
- apps/web/src/routes/puls.tsx → dashboard.tsx (Komponente DashboardRoute)
- apps/api/src/routers/puls.ts → dashboard.ts (dashboardRouter)
- tRPC-Aufrufe: trpc.puls.* / utils.puls.* → trpc.dashboard.* / utils.dashboard.*
- Auth-Redirects (/api/auth/login, /callback, dev-login) zeigen jetzt auf /dashboard

Projekt-Beschreibung
- Migration 0007_project_description: project.description text (nullable)
- Shared Zod: ProjectSchemas.createProjectInput.description (max 2000, optional)
- Backend: project.create mappt input.description
- Frontend Dialog „Neues Projekt": Textarea (3 Zeilen, optional)
- Projekt-Detail: zeigt Beschreibung als whitespace-pre-wrap unter Subtitle

README
- Routen-Tabelle, Shortcut-Liste, Verzeichnis-Baum, DB-Tabellen, Migration-Range,
  Frontend-Routenliste aktualisiert

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 17:51:08 +02:00

98 lines
3.4 KiB
TypeScript

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<string, Project[]>();
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);
}