import { test } from "node:test"; import assert from "node:assert/strict"; import { calculateLeaveDays, calculateWeek, dateOnly, dateSchema, getTimeBalance, scheduledMinutes, todayKey, vacationBalance, weekStart, } from "../src/lib/staff/calculations"; const profile = { employmentStart: dateOnly("2026-01-01"), employmentEnd: null, trackingStart: dateOnly("2026-01-01"), openingMinutes: 120, workPatterns: [ { effectiveFrom: dateOnly("2026-01-01"), dailyMinutes: [360, 360, 0, 360, 0, 0, 0], }, ], dayOverrides: [], }; const holidays = new Set(["2026-01-06"]); const days = (date: string, vacationUnits: number, creditMinutes = 360) => [ { date: dateOnly(date), vacationUnits, creditMinutes }, ]; test("Teilzeiturlaub verbraucht nur feste Arbeitstage, Feiertage werden übersprungen", () => { const result = calculateLeaveDays(profile, holidays, { kind: "VACATION", startDate: "2026-01-05", endDate: "2026-01-11", halfDay: false, }); assert.deepEqual( result.map((d) => d.date), ["2026-01-05", "2026-01-08"], ); assert.equal( result.reduce((s, d) => s + d.vacationUnits, 0), 4, ); }); test("Halber Urlaubstag entspricht halbem Tages-Soll", () => { assert.deepEqual( calculateLeaveDays(profile, new Set(), { kind: "VACATION", startDate: "2026-01-05", endDate: "2026-01-05", halfDay: true, }), [{ date: "2026-01-05", vacationUnits: 1, creditMinutes: 180 }], ); assert.throws(() => calculateLeaveDays(profile, holidays, { kind: "VACATION", startDate: "2026-01-05", endDate: "2026-01-08", halfDay: true, }), ); }); test("Tagesausnahmen und datierte Modelle verändern nur den vorgesehenen Zeitraum", () => { const schedule = { ...profile, dayOverrides: [{ date: dateOnly("2026-01-07"), minutes: 240 }], workPatterns: [ ...profile.workPatterns, { effectiveFrom: dateOnly("2026-02-01"), dailyMinutes: [480, 0, 480, 0, 0, 0, 0], }, ], }; assert.equal(scheduledMinutes(schedule, "2026-01-07", holidays), 240); assert.equal(scheduledMinutes(schedule, "2026-01-05", holidays), 360); assert.equal(scheduledMinutes(schedule, "2026-02-02", holidays), 480); assert.equal(scheduledMinutes(schedule, "2026-01-06", holidays), 0); assert.equal( scheduledMinutes( { ...schedule, employmentEnd: dateOnly("2026-01-31") }, "2026-02-02", holidays, ), 0, ); }); test("Urlaub über Jahreswechsel wird auf die Jahreskonten verteilt", () => { const leave = [ { kind: "VACATION", status: "APPROVED", days: [...days("2026-12-31", 2), ...days("2027-01-04", 1)], }, ]; assert.equal( vacationBalance( { year: 2026, entitlementUnits: 40, carryUnits: 2 }, leave, "2026-10-01", ).planned, 2, ); assert.equal( vacationBalance( { year: 2027, entitlementUnits: 40, carryUnits: 0 }, leave, "2026-10-01", ).planned, 1, ); }); test("Offene Stornierung bleibt gebucht, Ablehnung und Stornierung verbrauchen nichts", () => { const leave = [ { kind: "VACATION", status: "CANCEL_REQUESTED", days: days("2026-01-05", 2), }, { kind: "VACATION", status: "PENDING", days: days("2026-01-08", 1) }, ...["CANCELLED", "REJECTED"].map((status) => ({ kind: "VACATION", status, days: days("2026-01-12", 2), })), ]; assert.deepEqual( vacationBalance( { year: 2026, entitlementUnits: 40, carryUnits: 4 }, leave, "2026-01-07", ), { entitlement: 44, taken: 2, planned: 0, pending: 1, available: 42 }, ); }); test("Mehrere Arbeitsblöcke abzüglich Pausen, Urlaub und Krankheit ergeben die Wochenbilanz", () => { const entries = [ { date: dateOnly("2026-01-05"), startMinute: 480, endMinute: 720, breakMinutes: 30, }, { date: dateOnly("2026-01-05"), startMinute: 780, endMinute: 960, breakMinutes: 0, }, ]; const leave = [ { kind: "SICKNESS", status: "APPROVED", days: days("2026-01-08", 0) }, ]; const result = calculateWeek(profile, holidays, "2026-01-05", entries, leave); assert.equal(result[0].worked, 390); assert.equal(result[0].balance, 30); assert.equal(result[1].expected, 0); assert.equal(result[3].credit, 360); assert.equal(result[3].balance, 0); }); test("Nur genehmigte Snapshots zählen; Zukunftskorrekturen zählen erst ab Datum", () => { const snapshot = [ { date: "2026-01-05", expected: 360, worked: 420, credit: 0, timeOff: 0, balance: 60, }, ]; const result = getTimeBalance( profile, [ { status: "APPROVED", dailySnapshot: snapshot }, { status: "SUBMITTED", dailySnapshot: snapshot }, ], [{ date: dateOnly("2026-02-01"), minutes: 100 }], [], "2026-01-10", ); assert.equal(result.confirmed, 180); }); test("Freizeitausgleich wird reserviert und nach Wochenbuchung genau einmal abgezogen", () => { const leave = [ { kind: "TIME_OFF", status: "APPROVED", days: days("2026-01-05", 0, 60) }, ]; const initial = getTimeBalance(profile, [], [], leave, "2026-01-04"); assert.deepEqual(initial, { confirmed: 120, reserved: 60, available: 60 }); const snapshot = [ { date: "2026-01-05", expected: 360, worked: 300, credit: 0, timeOff: 60, balance: -60, }, ]; assert.deepEqual( getTimeBalance( profile, [{ status: "APPROVED", dailySnapshot: snapshot }], [], leave, "2026-01-06", ), { confirmed: 60, reserved: 0, available: 60 }, ); const computed = calculateWeek( profile, new Set(), "2026-01-05", [ { date: dateOnly("2026-01-05"), startMinute: 480, endMinute: 780, breakMinutes: 0, }, ], leave, ); assert.equal(computed[0].credit, 0); assert.equal(computed[0].balance, -60); }); test("Ungültige Kalendertage werden abgewiesen; Tagesdatum wird in Berlin bestimmt", () => { assert.equal(dateSchema.safeParse("2026-02-30").success, false); assert.equal(dateSchema.safeParse("2028-02-29").success, true); assert.equal(todayKey(new Date("2026-01-04T23:30:00Z")), "2026-01-05"); assert.equal(weekStart("2026-01-11"), "2026-01-05"); });