feat(team-view): horizontale Team-Timeline (ENTWICKLUN-8)
Neue Seite /team (Tab „Team", g t): horizontale Tages-Timeline mit einer Spur
pro Person. Zeit-Blöcke werden nach Start/Ende auf einer Stundenachse
positioniert, eingefärbt nach Kategorie; überlappende Blöcke landen auf
parallelen Sub-Spuren ("parallele Zeitstränge"). Laufende Blöcke (heute) ticken
live und sind rot umrandet. Datums-Navigation (Tag vor/zurück, Picker, „Heute"),
Hover-Tooltip mit Label/Zeitraum/Dauer, Kategorie-Legende.
Endpoint time.teamTimeline({date?}) liefert pro Person die Roh-Einträge
(Start/Ende/Kategorie/Label); Achse & Sub-Spuren rechnet das Frontend.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
9bf20fbabd
commit
516b35bf15
6 files changed
+369
-2
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
import { and, eq, isNull, desc, sql, inArray } from 'drizzle-orm';
|
||||
import { timeEntry, dayEntry, todo, type TimeEntry } from '@cockpit/db';
|
||||
import { and, eq, isNull, asc, desc, sql, inArray } from 'drizzle-orm';
|
||||
import { timeEntry, dayEntry, todo, user, type TimeEntry } from '@cockpit/db';
|
||||
import { TimeEntrySchemas } from '@cockpit/shared';
|
||||
import { router, protectedProcedure } from '../trpc.js';
|
||||
import { todayIso, berlinDayStartMs } from '../lib/werktag.js';
|
||||
@@ -345,4 +345,72 @@ export const timeRouter = router({
|
||||
active: b.active,
|
||||
}));
|
||||
}),
|
||||
|
||||
/**
|
||||
* Team-Timeline: alle Zeit-Einträge eines Tages, pro Person eine Spur.
|
||||
* Liefert Rohdaten (Start/Ende/Kategorie/Label); Achse & parallele Stränge
|
||||
* (überlappende Blöcke) rendert das Frontend.
|
||||
*/
|
||||
teamTimeline: protectedProcedure
|
||||
.input(TimeEntrySchemas.teamTimelineInput)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const today = todayIso();
|
||||
const date = input.date ?? today;
|
||||
const isToday = date === today;
|
||||
const onDate = (col: typeof timeEntry.startedAt | typeof timeEntry.endedAt) =>
|
||||
sql`(${col} AT TIME ZONE 'Europe/Berlin')::date = ${date}`;
|
||||
const cond = isToday
|
||||
? sql`(${onDate(timeEntry.startedAt)} OR ${onDate(timeEntry.endedAt)} OR ${timeEntry.endedAt} IS NULL)`
|
||||
: sql`(${onDate(timeEntry.startedAt)} OR ${onDate(timeEntry.endedAt)})`;
|
||||
|
||||
const rows = await ctx.db
|
||||
.select()
|
||||
.from(timeEntry)
|
||||
.where(cond)
|
||||
.orderBy(asc(timeEntry.startedAt));
|
||||
|
||||
const todoIds = Array.from(new Set(rows.map((r) => r.todoId).filter((x): x is string => !!x)));
|
||||
const titles = new Map<string, string>();
|
||||
if (todoIds.length > 0) {
|
||||
const tr = await ctx.db
|
||||
.select({ id: todo.id, title: todo.title })
|
||||
.from(todo)
|
||||
.where(inArray(todo.id, todoIds));
|
||||
for (const t of tr) titles.set(t.id, t.title);
|
||||
}
|
||||
|
||||
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]));
|
||||
|
||||
type Entry = {
|
||||
id: string;
|
||||
start: string;
|
||||
end: string | null;
|
||||
category: string;
|
||||
description: string | null;
|
||||
todoTitle: string | null;
|
||||
};
|
||||
const byUser = new Map<string, Entry[]>();
|
||||
for (const r of rows) {
|
||||
const arr = byUser.get(r.userId) ?? [];
|
||||
arr.push({
|
||||
id: r.id,
|
||||
start: r.startedAt.toISOString(),
|
||||
end: r.endedAt ? r.endedAt.toISOString() : null,
|
||||
category: r.category,
|
||||
description: r.description,
|
||||
todoTitle: r.todoId ? (titles.get(r.todoId) ?? null) : null,
|
||||
});
|
||||
byUser.set(r.userId, arr);
|
||||
}
|
||||
|
||||
const lanes = Array.from(byUser.entries())
|
||||
.map(([userId, entries]) => ({ userId, name: nameById.get(userId) ?? '—', entries }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return { date, lanes };
|
||||
}),
|
||||
});
|
||||
Reference in new issue
Block a user