diff --git a/apps/api/src/routers/time.ts b/apps/api/src/routers/time.ts index e37a14a..2818f59 100644 --- a/apps/api/src/routers/time.ts +++ b/apps/api/src/routers/time.ts @@ -420,4 +420,38 @@ export const timeRouter = router({ return { date, lanes }; }), + + /** + * Getrackte Zeit auf ein Projekt (time_entry mit bezugKind='project'): + * Gesamtsumme + Aufschlüsselung pro Person. bezugId wurde bislang erfasst, + * aber nirgends ausgewertet — das speist die „Getrackte Zeit"-Sektion im + * Projekt-Detail. + */ + byProject: protectedProcedure + .input(z.object({ projectId: z.string().uuid() })) + .query(async ({ ctx, input }) => { + const rows = await ctx.db + .select({ + userId: timeEntry.userId, + seconds: sql`COALESCE(SUM(${timeEntry.durationSeconds}), 0)`, + }) + .from(timeEntry) + .where(and(eq(timeEntry.bezugKind, 'project'), eq(timeEntry.bezugId, input.projectId))) + .groupBy(timeEntry.userId); + + const userRows = await ctx.db.select({ id: user.id, name: user.name }).from(user); + const nameById = new Map(userRows.map((u) => [u.id, u.name])); + + const perUser = rows + .map((r) => ({ + userId: r.userId, + name: nameById.get(r.userId) ?? '—', + seconds: Number(r.seconds), + })) + .filter((r) => r.seconds > 0) + .sort((a, b) => b.seconds - a.seconds); + const totalSeconds = perUser.reduce((sum, r) => sum + r.seconds, 0); + + return { totalSeconds, perUser }; + }), }); diff --git a/apps/web/src/routes/projekt.tsx b/apps/web/src/routes/projekt.tsx index 33e641e..6fc5e66 100644 --- a/apps/web/src/routes/projekt.tsx +++ b/apps/web/src/routes/projekt.tsx @@ -12,6 +12,7 @@ import { Modal } from '../components/ui/Modal'; import { useConfirm } from '../components/ui/ConfirmProvider'; import { useCurrentUser } from '../hooks/useCurrentUser'; import { voice } from '../lib/voice'; +import { formatDurationCompact } from '../lib/format-duration'; import { toast } from 'sonner'; import { clsx } from 'clsx'; import { LEAD_STATUS_META, type LeadStatus } from '../lib/lead-meta'; @@ -191,6 +192,8 @@ export function ProjektRoute() { + + {p.dodText && ( @@ -584,6 +587,37 @@ function Zieldreieck({ flex }: { flex: ZieldreieckFlex | null }) { ); } +// --- Getrackte Zeit --------------------------------------------------------- +function ZeitSection({ projectId }: { projectId: string }) { + const q = trpc.time.byProject.useQuery({ projectId }); + if (!q.data || q.data.totalSeconds === 0) return null; + return ( + + +

Getrackte Zeit

+
+ +

+ Gesamt:{' '} + + {formatDurationCompact(q.data.totalSeconds)} + +

+
    + {q.data.perUser.map((u) => ( +
  • + {u.name} + + {formatDurationCompact(u.seconds)} + +
  • + ))} +
+
+
+ ); +} + // --- Phasen-Stepper + Gate-Review ------------------------------------------- function PhaseSection({ projectId, phase }: { projectId: string; phase: ProjectPhase }) { const utils = trpc.useUtils();