238 lines
6.3 KiB
TypeScript
238 lines
6.3 KiB
TypeScript
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");
|
|
});
|