feat(zeit): Check-In/Check-Out + Kimai-Sync-Naht (ENTWICKLUN-9)
Arbeitsantritt = bestehender Lock-in (setzt checkedInAt); neu dazu: - Check-Out / Feierabend: dayEntry.checkedOutAt + Endpoint dayEntry.checkOut; Heute-Seite zeigt „Arbeitsantritt HH:MM" und einen „Feierabend / Check-Out"- Button, danach Antritt/Feierabend/Dauer. - Kimai-Anbindung (config-gated, best-effort, nicht blockierend): lib/kimai.ts mit clockIn/clockOut. Check-In startet ein Kimai-Timesheet, Check-Out stoppt es; ID + Sync-Status auf dem dayEntry (kimaiTimesheetId, kimaiSynced). No-Op solange KIMAI_BASE_URL/API_TOKEN/PROJECT_ID/ACTIVITY_ID nicht gesetzt. Migration 0014 (checkedOutAt, kimaiTimesheetId, kimaiSynced). Env-Vars als optional ergänzt + in .env.example dokumentiert. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
9942cd8e88
commit
5bbb4abffa
9 files changed
+3542
-2
No files matched your search
@@ -4,6 +4,7 @@ import { dayEntry, todo, blocker, user, type Todo } from '@cockpit/db';
|
||||
import { DayEntrySchemas } from '@cockpit/shared';
|
||||
import { router, protectedProcedure } from '../trpc.js';
|
||||
import { todayIso, previousWerktag, nextWerktag } from '../lib/werktag.js';
|
||||
import { kimaiClockIn, kimaiClockOut, kimaiEnabled } from '../lib/kimai.js';
|
||||
|
||||
export const dayEntryRouter = router({
|
||||
/**
|
||||
@@ -162,9 +163,54 @@ export const dayEntryRouter = router({
|
||||
return { entry: created, todos: todoRows, carriedOver: stale.length };
|
||||
});
|
||||
|
||||
// Kimai-Sync (best-effort, nicht blockierend): Check-In = Clock-In.
|
||||
if (kimaiEnabled()) {
|
||||
const tsId = await kimaiClockIn({
|
||||
begin: result.entry.checkedInAt ?? new Date(),
|
||||
description: `ebw — ${ctx.user.name}`,
|
||||
});
|
||||
if (tsId !== null) {
|
||||
await ctx.db
|
||||
.update(dayEntry)
|
||||
.set({ kimaiTimesheetId: tsId, kimaiSynced: true })
|
||||
.where(eq(dayEntry.id, result.entry.id));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Check-Out / Arbeitsende: markiert das Ende der Arbeits-Session. Best-effort
|
||||
* Kimai-Clock-Out, falls beim Check-In ein Timesheet gestartet wurde.
|
||||
*/
|
||||
checkOut: protectedProcedure.mutation(async ({ ctx }) => {
|
||||
const date = todayIso();
|
||||
const entry = (
|
||||
await ctx.db
|
||||
.select()
|
||||
.from(dayEntry)
|
||||
.where(and(eq(dayEntry.userId, ctx.user.id), eq(dayEntry.date, date)))
|
||||
.limit(1)
|
||||
)[0];
|
||||
if (!entry) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Du bist heute noch nicht locked in.' });
|
||||
}
|
||||
if (entry.checkedOutAt) return entry;
|
||||
|
||||
const now = new Date();
|
||||
const [updated] = await ctx.db
|
||||
.update(dayEntry)
|
||||
.set({ checkedOutAt: now, updatedAt: now })
|
||||
.where(eq(dayEntry.id, entry.id))
|
||||
.returning();
|
||||
|
||||
if (entry.kimaiTimesheetId !== null && kimaiEnabled()) {
|
||||
await kimaiClockOut({ timesheetId: entry.kimaiTimesheetId });
|
||||
}
|
||||
return updated;
|
||||
}),
|
||||
|
||||
/**
|
||||
* ToDos im Verlauf des Tages ändern (vor Reflexion).
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user