fix(attendance): timezone-proof print dates; overnight-only time prefixes
Two bugs in the attendance admin print (Tisk on Správa docházky):
1. Viewer-timezone date shift: day rows are built from date-only strings
('2026-06-01'), which new Date() parses as UTC midnight per spec; local
formatting then rendered the PREVIOUS day for any viewer west of UTC —
a June print opened with '31. 5. Neděle' (a May row holding June 1's
punches) and every weekday name + weekend tint shifted by one. Czech
viewers were unaffected (UTC+1/+2), US viewers always hit it.
formatDate / getCzechWeekday / isWeekendDate now parse the calendar day
from the string parts — identical output in every timezone.
2. Every time cell carried a bogus date prefix ('1.6. 08:00'): the
overnight-only guard compared the extracted date part against the raw
wire shift_date ('2026-06-01T02:00:00') — never equal. Now compares
normalized date parts; the prefix fires only for true overnight punches.
Verified end-to-end against the production June payload; +8 regression
tests (timezone-proof by construction — parts-based, no UTC parse).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
77
src/__tests__/attendance-print-tz.test.ts
Normal file
77
src/__tests__/attendance-print-tz.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
getCzechWeekday,
|
||||
isWeekendDate,
|
||||
formatTimeOrDatetimePrint,
|
||||
} from "../admin/utils/attendanceHelpers";
|
||||
import { formatDate } from "../admin/utils/formatters";
|
||||
|
||||
/**
|
||||
* Regression: the attendance admin print builds its day rows from date-ONLY
|
||||
* strings ("2026-06-01"). `new Date("YYYY-MM-DD")` is UTC midnight per the
|
||||
* ECMAScript spec, so formatting it with local getters rendered the PREVIOUS
|
||||
* day for viewers west of UTC — a June print opened with "31. 5. 2026 Neděle"
|
||||
* (a May row) and every weekday name was shifted by one. The helpers now
|
||||
* parse the calendar day from the string parts, which is timezone-proof by
|
||||
* construction: these assertions hold in EVERY zone (on a machine west of
|
||||
* UTC the old implementation fails them).
|
||||
*/
|
||||
describe("attendance print — timezone-proof date rendering", () => {
|
||||
it("formatDate renders a date-only string as that exact calendar day", () => {
|
||||
expect(formatDate("2026-06-01")).toBe("1. 6. 2026");
|
||||
expect(formatDate("2026-06-30")).toBe("30. 6. 2026");
|
||||
});
|
||||
|
||||
it("formatDate keeps naive-datetime (wire format) behavior", () => {
|
||||
expect(formatDate("2026-06-01T02:00:00")).toBe("1. 6. 2026");
|
||||
});
|
||||
|
||||
it("getCzechWeekday names the string's own calendar day", () => {
|
||||
expect(getCzechWeekday("2026-06-01")).toBe("Pondělí");
|
||||
expect(getCzechWeekday("2026-06-06")).toBe("Sobota");
|
||||
expect(getCzechWeekday("2026-06-07")).toBe("Neděle");
|
||||
// naive-datetime wire format resolves to the same day
|
||||
expect(getCzechWeekday("2026-06-01T02:00:00")).toBe("Pondělí");
|
||||
});
|
||||
|
||||
it("isWeekendDate flags the string's own calendar day", () => {
|
||||
expect(isWeekendDate("2026-06-06")).toBe(true); // Saturday
|
||||
expect(isWeekendDate("2026-06-07")).toBe(true); // Sunday
|
||||
expect(isWeekendDate("2026-06-01")).toBe(false); // Monday
|
||||
expect(isWeekendDate("2026-06-06T02:00:00")).toBe(true);
|
||||
});
|
||||
|
||||
it("agrees with a locally-constructed date in the current zone (guards a regression to UTC parsing)", () => {
|
||||
// Local construction is the ground truth for "calendar day" in any zone.
|
||||
const local = new Date(2026, 5, 1, 12).toLocaleDateString("cs-CZ", {
|
||||
weekday: "long",
|
||||
});
|
||||
const expected = local.charAt(0).toUpperCase() + local.slice(1);
|
||||
expect(getCzechWeekday("2026-06-01")).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("attendance print — overnight date prefix on time cells", () => {
|
||||
// shift_date arrives as a naive datetime on the wire ("2026-06-01T02:00:00");
|
||||
// the guard used to compare it raw against the extracted date part, so the
|
||||
// overnight-only "d.m." prefix fired on EVERY cell ("1.6. 08:00").
|
||||
const wireShiftDate = "2026-06-01T02:00:00";
|
||||
|
||||
it("same-day punch renders time only", () => {
|
||||
expect(
|
||||
formatTimeOrDatetimePrint("2026-06-01T08:00:00", wireShiftDate),
|
||||
).toBe("08:00");
|
||||
});
|
||||
|
||||
it("overnight punch keeps the date prefix", () => {
|
||||
expect(
|
||||
formatTimeOrDatetimePrint("2026-06-02T01:30:00", wireShiftDate),
|
||||
).toBe("2.6. 01:30");
|
||||
});
|
||||
|
||||
it("date-only shiftDate also matches", () => {
|
||||
expect(formatTimeOrDatetimePrint("2026-06-01T08:00:00", "2026-06-01")).toBe(
|
||||
"08:00",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user