diff --git a/apps/api/src/routers/dashboard.ts b/apps/api/src/routers/dashboard.ts index a7a4061..6900721 100644 --- a/apps/api/src/routers/dashboard.ts +++ b/apps/api/src/routers/dashboard.ts @@ -1,5 +1,5 @@ -import { and, eq, ne, isNull, lt, count, desc } from 'drizzle-orm'; -import { area, project, blocker, dayEntry, user, risk, type Area, type Project } from '@cockpit/db'; +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'; @@ -7,8 +7,16 @@ 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] = - await Promise.all([ + 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)), @@ -52,6 +60,28 @@ export const dashboardRouter = router({ 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(); @@ -98,6 +128,10 @@ export const dashboardRouter = router({ }, 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 })), diff --git a/apps/web/src/routes/dashboard.tsx b/apps/web/src/routes/dashboard.tsx index b6fe805..61e146d 100644 --- a/apps/web/src/routes/dashboard.tsx +++ b/apps/web/src/routes/dashboard.tsx @@ -104,6 +104,45 @@ export function DashboardRoute() { )} + {q.data.meilensteine.length > 0 && ( +
+

Anstehende Meilensteine

+ + +
    + {q.data.meilensteine.map((m) => ( +
  • +
    +

    {m.title}

    +
    + + {formatDate(m.due!)} + {m.ueberfaellig && ' · überfällig'} + + · + {relativeDays(m.due!, q.data.date)} +
    +
    + + {m.projectName} → + +
  • + ))} +
+
+
+
+ )} + {q.data.kritischeModi.length > 0 && (

Projekte in Eskalation / Taskforce

@@ -364,5 +403,18 @@ function formatDate(input: string | Date): string { }).format(dt); } +// Relative Tagesangabe für Meilensteine ("heute", "in 3 Tagen", "vor 2 Tagen"). +function relativeDays(dueIso: string, todayIso: string): string { + const due = new Date(`${dueIso}T00:00:00`); + const today = new Date(`${todayIso}T00:00:00`); + if (Number.isNaN(due.getTime()) || Number.isNaN(today.getTime())) return ''; + const diff = Math.round((due.getTime() - today.getTime()) / 86_400_000); + if (diff === 0) return 'heute'; + if (diff === 1) return 'morgen'; + if (diff === -1) return 'gestern'; + if (diff > 0) return `in ${diff} Tagen`; + return `vor ${Math.abs(diff)} Tagen`; +} + // Button als referenz behalten (Linter) void Button;