Historie: nachträglich reflektieren bis 3 Tage nach dem Tag
ci / quality (push) Failing after 36s
ci / build (push) Has been skipped
deploy / deploy (push) Successful in 55s

Reflexion/ToDo-Bearbeitung war nur bis 06:00 des Folgetags moeglich. Jetzt
3-Tage-Nachfrist (isEditableWithinGrace) und ein Bearbeiten-Weg aus der Historie:

- werktag: isEditableWithinGrace(entryDate, now, 3) + Tests
- submitReflection/editTodoTitle: Sperre auf die 3-Tage-Nachfrist gelockert
- neue Query dayEntry.ownForDate(date): eigener Tageseintrag + editable-Flag
- reflexion.tsx liest ?date= und laedt vergangene Tage; Felder werden mit der
  bestehenden Reflexion vorbefuellt
- historie.tsx: "Bearbeiten"-Button am eigenen Eintrag (wenn <= 3 Tage)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Raffael SheikhandClaude Opus 4.8 committed 2026-06-07 13:13:38 +02:00
1 parent ccd5c42709
commit b340b45110
5 files changed
+172 -50

No files matched your search

+14
View File
@@ -9,6 +9,7 @@ import {
addCalendarDays,
workdayCutoffMs,
isWorkdaySealed,
isEditableWithinGrace,
} from './werktag';
describe('werktag', () => {
@@ -77,4 +78,17 @@ describe('werktag', () => {
expect(isWorkdaySealed(sameDayStart, new Date('2026-06-07T06:00:00+02:00'))).toBe(false);
});
});
describe('isEditableWithinGrace — 3-Tage-Nachfrist', () => {
const entry = '2026-06-05'; // Fr
it('am Eintragstag selbst editierbar', () => {
expect(isEditableWithinGrace(entry, new Date('2026-06-05T20:00:00+02:00'))).toBe(true);
});
it('bis Ende des 3. Tages danach (08.) editierbar', () => {
expect(isEditableWithinGrace(entry, new Date('2026-06-08T23:00:00+02:00'))).toBe(true);
});
it('ab dem 4. Tag (09.) gesperrt', () => {
expect(isEditableWithinGrace(entry, new Date('2026-06-09T00:30:00+02:00'))).toBe(false);
});
});
});
+13
View File
@@ -46,6 +46,19 @@ export function isWorkdaySealed(entryDate: string, now: Date = new Date()): bool
return now.getTime() >= workdayCutoffMs(entryDate);
}
/**
* True, solange ein Eintrag mit Datum `entryDate` noch nachträglich bearbeitet
* werden darf: bis zum Ende des `graceDays`-ten Kalendertages danach (Default 3).
* Genutzt für die Reflexion in der Historie (Tag selbst + 3 Tage Nachfrist).
*/
export function isEditableWithinGrace(
entryDate: string,
now: Date = new Date(),
graceDays = 3,
): boolean {
return now.getTime() < berlinDayStartMs(addCalendarDays(entryDate, graceDays + 1));
}
export function formatIsoDate(d: Date): string {
const fmt = new Intl.DateTimeFormat('en-CA', {
timeZone: TIMEZONE,
+32 -7
View File
@@ -1,4 +1,5 @@
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { and, eq, gt, gte, isNull, lt, lte, asc, desc, inArray } from 'drizzle-orm';
import { dayEntry, todo, blocker, user, type Db, type DayEntry, type Todo } from '@cockpit/db';
import { DayEntrySchemas } from '@cockpit/shared';
@@ -9,6 +10,7 @@ import {
addCalendarDays,
workdayCutoffMs,
isWorkdaySealed,
isEditableWithinGrace,
} from '../lib/werktag.js';
import { kimaiClockIn, kimaiClockOut, kimaiEnabled } from '../lib/kimai.js';
@@ -421,10 +423,10 @@ export const dayEntryRouter = router({
if (!existing) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'ToDo nicht gefunden.' });
}
if (isWorkdaySealed(existing.date, new Date())) {
if (!isEditableWithinGrace(existing.date, new Date())) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'ToDos eines Vortags können nicht mehr bearbeitet werden.',
message: 'ToDos können nur bis 3 Tage nach dem Tag bearbeitet werden.',
});
}
const [updated] = await ctx.db
@@ -520,13 +522,12 @@ export const dayEntryRouter = router({
if (!entry) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Tageseintrag nicht gefunden.' });
}
// Editierbar, solange der Arbeitstag noch läuft. Damit kann mehrmals am Tag
// (auch nach Mitternacht bei Nachtarbeit) aktualisiert werden; ab 06:00 des
// Folgetags ist der Tag final und kann nicht mehr verändert werden.
if (isWorkdaySealed(entry.date, new Date())) {
// Reflexion ist bis 3 Kalendertage nach dem Eintragstag nachträglich
// editierbar (Nachfrist für die Historie). Danach final.
if (!isEditableWithinGrace(entry.date, new Date())) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Reflexion eines Vortags kann nicht mehr bearbeitet werden.',
message: 'Reflexion kann nur bis 3 Tage nach dem Tag bearbeitet werden.',
});
}
@@ -758,6 +759,30 @@ export const dayEntryRouter = router({
todos: counts.get(e.id) ?? { done: 0, open: 0, dropped: 0 },
}));
}),
/**
* Eigener Tageseintrag eines bestimmten Datums — zum nachträglichen Reflektieren
* aus der Historie. `editable` zeigt, ob die 3-Tage-Nachfrist noch läuft.
*/
ownForDate: protectedProcedure
.input(z.object({ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/) }))
.query(async ({ ctx, input }) => {
const entry =
(
await ctx.db
.select()
.from(dayEntry)
.where(and(eq(dayEntry.userId, ctx.user.id), eq(dayEntry.date, input.date)))
.limit(1)
)[0] ?? null;
if (!entry) return { entry: null, todos: [] as Todo[], editable: false };
const todos = await ctx.db
.select()
.from(todo)
.where(eq(todo.dayEntryId, entry.id))
.orderBy(todo.order);
return { entry, todos, editable: isEditableWithinGrace(entry.date, new Date()) };
}),
});
// Helper für Cron-Endpoint