Arbeitstag-Grenze auf 6 Uhr + Feierabend reversibel
Nachtarbeit über Mitternacht brach bisher den Check-Out ("Du bist heute
noch nicht locked in") und sperrte nach versehentlichem Feierabend den Tag.
- Tag = Kalendertag des Check-ins; eine über Mitternacht laufende Session
zaehlt zum Starttag, bis 06:00 des Folgetags (isWorkdaySealed)
- checkOut/today finden den offenen Eintrag statt stur "heute"
- reopen ("Arbeit fortsetzen") hebt einen Feierabend wieder auf
- applyWorkdayBoundary: nahtloser 6-Uhr-Schnitt (Auto-Checkout + Folge-Tag,
offene ToDos wandern mit)
- Reflexion/ToDo-Edit folgen der 6-Uhr-Grenze statt Mitternacht
- Cron apply-workday-boundaries fuer Team-Dashboard-Konsistenz
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b39c67d1aa
commit
39e259f3ac
5 files changed
+308
-49
No files matched your search
@@ -6,6 +6,9 @@ import {
|
||||
previousWerktag,
|
||||
werktageBetween,
|
||||
berlinDayStartMs,
|
||||
addCalendarDays,
|
||||
workdayCutoffMs,
|
||||
isWorkdaySealed,
|
||||
} from './werktag';
|
||||
|
||||
describe('werktag', () => {
|
||||
@@ -41,4 +44,37 @@ describe('werktag', () => {
|
||||
// Winter (CET, +1): 00:00 Berlin = 23:00 UTC am Vortag
|
||||
expect(berlinDayStartMs('2026-01-15')).toBe(Date.parse('2026-01-14T23:00:00Z'));
|
||||
});
|
||||
|
||||
it('addCalendarDays verschiebt Kalendertage (auch über Monatsgrenze)', () => {
|
||||
expect(addCalendarDays('2026-06-07', 1)).toBe('2026-06-08');
|
||||
expect(addCalendarDays('2026-06-30', 1)).toBe('2026-07-01');
|
||||
expect(addCalendarDays('2026-06-07', -1)).toBe('2026-06-06');
|
||||
});
|
||||
|
||||
it('workdayCutoffMs ist 06:00 Berlin am Folgetag (DST-korrekt)', () => {
|
||||
// Sommer (CEST, +2): 06:00 Berlin = 04:00 UTC
|
||||
expect(workdayCutoffMs('2026-06-06')).toBe(Date.parse('2026-06-07T04:00:00Z'));
|
||||
// Winter (CET, +1): 06:00 Berlin = 05:00 UTC
|
||||
expect(workdayCutoffMs('2026-01-15')).toBe(Date.parse('2026-01-16T05:00:00Z'));
|
||||
});
|
||||
|
||||
describe('isWorkdaySealed — der Arbeitstag = Check-in-Kalendertag', () => {
|
||||
// Eintrag mit Datum Sa 2026-06-06 (Check-in irgendwann an dem Tag).
|
||||
const entry = '2026-06-06';
|
||||
it('vor Mitternacht: nicht versiegelt', () => {
|
||||
expect(isWorkdaySealed(entry, new Date('2026-06-06T22:00:00+02:00'))).toBe(false);
|
||||
});
|
||||
it('nach Mitternacht, aber vor 06:00: noch der Arbeitstag (nicht versiegelt)', () => {
|
||||
// 03:00 in der Nacht → gehört noch zum 06.
|
||||
expect(isWorkdaySealed(entry, new Date('2026-06-07T03:00:00+02:00'))).toBe(false);
|
||||
});
|
||||
it('um/nach 06:00 des Folgetags: versiegelt', () => {
|
||||
expect(isWorkdaySealed(entry, new Date('2026-06-07T06:00:00+02:00'))).toBe(true);
|
||||
expect(isWorkdaySealed(entry, new Date('2026-06-07T09:00:00+02:00'))).toBe(true);
|
||||
});
|
||||
it('ein nach Mitternacht begonnener Tag (Datum = 07.) gilt um 06:00 noch nicht als versiegelt', () => {
|
||||
const sameDayStart = '2026-06-07';
|
||||
expect(isWorkdaySealed(sameDayStart, new Date('2026-06-07T06:00:00+02:00'))).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,14 @@ import { isGermanHoliday } from './holidays.js';
|
||||
|
||||
const TIMEZONE = 'Europe/Berlin';
|
||||
|
||||
/**
|
||||
* Uhrzeit (Berlin), zu der ein Arbeitstag „kippt": Wer um 06:00 noch eingestempelt
|
||||
* ist, dessen Tag wird automatisch beendet (siehe `applyWorkdayBoundary`). Bis dahin
|
||||
* gehört eine über Mitternacht laufende Session noch zum Tag, an dem eingestempelt
|
||||
* wurde — der Arbeitstag eines Eintrags = der Kalendertag des Check-ins.
|
||||
*/
|
||||
export const WORKDAY_RESET_HOUR = 6;
|
||||
|
||||
/**
|
||||
* Gibt das aktuelle Datum (oder `now`) als `YYYY-MM-DD` in Europe/Berlin zurück.
|
||||
*/
|
||||
@@ -12,6 +20,32 @@ export function todayIso(now: Date = new Date()): string {
|
||||
return formatIsoDate(now);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verschiebt ein ISO-Datum um `n` Kalendertage (nicht Werktage). DST-robust, weil
|
||||
* `parseIsoDate` von 12:00 UTC ausgeht und in Berlin formatiert wird.
|
||||
*/
|
||||
export function addCalendarDays(iso: string, n: number): string {
|
||||
const d = parseIsoDate(iso);
|
||||
return formatIsoDate(new Date(d.getTime() + n * 24 * 60 * 60 * 1000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Epoch-ms des Zeitpunkts, ab dem der Arbeitstag mit Eintrags-Datum `entryDate`
|
||||
* abgelaufen ist: 06:00 Europe/Berlin am Folgetag (DST-korrekt).
|
||||
*/
|
||||
export function workdayCutoffMs(entryDate: string): number {
|
||||
return berlinDayStartMs(addCalendarDays(entryDate, 1)) + WORKDAY_RESET_HOUR * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* True, wenn der Arbeitstag eines Eintrags (Datum = Check-in-Kalendertag) bereits
|
||||
* „versiegelt" ist, d. h. `now` liegt am/nach 06:00 des Folgetags. Versiegelte Tage
|
||||
* sind read-only und werden nicht mehr als aktiver Tag angezeigt.
|
||||
*/
|
||||
export function isWorkdaySealed(entryDate: string, now: Date = new Date()): boolean {
|
||||
return now.getTime() >= workdayCutoffMs(entryDate);
|
||||
}
|
||||
|
||||
export function formatIsoDate(d: Date): string {
|
||||
const fmt = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: TIMEZONE,
|
||||
|
||||
Reference in new issue
Block a user