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>
This commit is contained in:
1 parent
4879556905
commit
c8fc024ca0
21 files changed
+2025
-39
No files matched your search
@@ -0,0 +1,97 @@
|
||||
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);
|
||||
}
|
||||
Reference in new issue
Block a user