feat(historie): team-weite Tages-Historie (ENTWICKLUN-10)
Neue Seite /historie: pro vergangenem Tag sieht man, was jede/r gemacht hat —
Reflexion (Ampel/Trend/Text) + ToDos (erledigt/offen/verworfen), plus Liste
„ohne Eintrag". Datums-Navigation (vorheriger/nächster Werktag, Datum-Picker);
Default = vorheriger Werktag. Heutige Reflexionen bleiben privat (nur ToDos).
- Endpoint dayEntry.historyForDate({date?}) liefert Einträge + prev/next-Werktag.
- TopNav-Tab „Historie" (g i), Link „Ganze Historie →" im Dashboard-Feed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c3d3c4500e
commit
9bf20fbabd
7 files changed
+259
-5
No files matched your search
@@ -1,9 +1,9 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { and, eq, isNull, lt, asc, desc, inArray } from 'drizzle-orm';
|
||||
import { dayEntry, todo, blocker, type Todo } from '@cockpit/db';
|
||||
import { dayEntry, todo, blocker, user, type Todo } from '@cockpit/db';
|
||||
import { DayEntrySchemas } from '@cockpit/shared';
|
||||
import { router, protectedProcedure } from '../trpc.js';
|
||||
import { todayIso } from '../lib/werktag.js';
|
||||
import { todayIso, previousWerktag, nextWerktag } from '../lib/werktag.js';
|
||||
|
||||
export const dayEntryRouter = router({
|
||||
/**
|
||||
@@ -383,6 +383,71 @@ export const dayEntryRouter = router({
|
||||
return entries;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Team-weite Historie eines (vergangenen) Tages: was hat jede/r gemacht —
|
||||
* Reflexion (Ampel/Trend/Text) + ToDos. Default-Datum = vorheriger Werktag.
|
||||
* Datums-Navigation (prev/next Werktag) wird mitgeliefert.
|
||||
*/
|
||||
historyForDate: protectedProcedure
|
||||
.input(DayEntrySchemas.historyForDateInput)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const today = todayIso();
|
||||
const date = input.date ?? previousWerktag(today);
|
||||
const isToday = date === today;
|
||||
|
||||
const entries = await ctx.db.select().from(dayEntry).where(eq(dayEntry.date, date));
|
||||
const userRows = await ctx.db
|
||||
.select({ id: user.id, name: user.name })
|
||||
.from(user)
|
||||
.where(eq(user.active, true));
|
||||
const nameById = new Map(userRows.map((u) => [u.id, u.name]));
|
||||
|
||||
const ids = entries.map((e) => e.id);
|
||||
const todoRows = ids.length
|
||||
? await ctx.db.select().from(todo).where(inArray(todo.dayEntryId, ids)).orderBy(asc(todo.order))
|
||||
: [];
|
||||
const todosByEntry = new Map<string, Todo[]>();
|
||||
for (const t of todoRows) {
|
||||
const arr = todosByEntry.get(t.dayEntryId) ?? [];
|
||||
arr.push(t);
|
||||
todosByEntry.set(t.dayEntryId, arr);
|
||||
}
|
||||
|
||||
const list = entries
|
||||
.map((e) => ({
|
||||
userId: e.userId,
|
||||
name: nameById.get(e.userId) ?? '—',
|
||||
status: e.status,
|
||||
ampel: e.ampel,
|
||||
trend: e.trend,
|
||||
// Heutige Reflexion bleibt privat bis morgen; ToDos sind teamweit sichtbar.
|
||||
reflectionText: isToday ? null : e.reflectionText,
|
||||
url: e.url,
|
||||
todos: (todosByEntry.get(e.id) ?? []).map((t) => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
status: t.status,
|
||||
dropReason: t.dropReason,
|
||||
})),
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const withEntry = new Set(entries.map((e) => e.userId));
|
||||
const ohneEintrag = userRows
|
||||
.filter((u) => !withEntry.has(u.id))
|
||||
.map((u) => u.name)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
const nw = nextWerktag(date);
|
||||
return {
|
||||
date,
|
||||
prevDate: previousWerktag(date),
|
||||
nextDate: nw < today ? nw : null,
|
||||
entries: list,
|
||||
ohneEintrag,
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* „Heute fehlt"-Liste (Anwesenheits-Übersicht).
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user