Compare commits
2 Commits
d859b5bbf7
...
89656ac2c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89656ac2c6 | ||
|
|
94030230e7 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "app-ts",
|
||||
"version": "2.4.42",
|
||||
"version": "2.4.43",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "app-ts",
|
||||
"version": "2.4.42",
|
||||
"version": "2.4.43",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.102.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-ts",
|
||||
"version": "2.4.42",
|
||||
"version": "2.4.43",
|
||||
"description": "",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
|
||||
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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -173,7 +173,10 @@ export const formatTimeOrDatetimePrint = (
|
||||
const timeDate = dateMatch
|
||||
? `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`
|
||||
: "";
|
||||
if (timeDate && timeDate !== shiftDate) {
|
||||
// Compare DATE PARTS: shiftDate arrives as a naive datetime off the wire
|
||||
// ("2026-06-01T02:00:00"), so a raw string compare never matched and the
|
||||
// overnight-only date prefix fired on every single time cell.
|
||||
if (timeDate && timeDate !== normalizeDateStr(shiftDate)) {
|
||||
const datePart = `${parseInt(dateMatch![3])}.${parseInt(dateMatch![2])}.`;
|
||||
return `${datePart} ${extractTime(datetime)}`;
|
||||
}
|
||||
@@ -203,9 +206,23 @@ export const calculateWorkMinutesPrint = (record: AttendanceRecord): number => {
|
||||
// Print template helpers (mzda-style counting)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Parse the calendar day of a date-only or naive-datetime string as a LOCAL
|
||||
* Date. `new Date("YYYY-MM-DD")` is UTC midnight per the ECMAScript spec —
|
||||
* in timezones west of UTC it renders as the PREVIOUS day, so a US viewer's
|
||||
* June print started with "31. 5. Neděle" and every weekday name was shifted.
|
||||
* Building the Date from its string parts keeps the calendar day identical
|
||||
* in every timezone.
|
||||
*/
|
||||
const localDayFromString = (dateStr: string): Date => {
|
||||
const m = String(dateStr).match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
||||
return new Date(dateStr);
|
||||
};
|
||||
|
||||
/** Returns the Czech weekday name with the first letter capitalized. */
|
||||
export const getCzechWeekday = (dateStr: string): string => {
|
||||
const wd = new Date(dateStr).toLocaleDateString("cs-CZ", {
|
||||
const wd = localDayFromString(dateStr).toLocaleDateString("cs-CZ", {
|
||||
weekday: "long",
|
||||
});
|
||||
return wd.charAt(0).toUpperCase() + wd.slice(1);
|
||||
@@ -213,7 +230,7 @@ export const getCzechWeekday = (dateStr: string): string => {
|
||||
|
||||
/** True if the date is a Saturday or Sunday. */
|
||||
export const isWeekendDate = (dateStr: string): boolean => {
|
||||
const dow = new Date(dateStr).getDay();
|
||||
const dow = localDayFromString(dateStr).getDay();
|
||||
return dow === 0 || dow === 6;
|
||||
};
|
||||
|
||||
|
||||
@@ -33,7 +33,15 @@ export function formatMultiCurrency(
|
||||
|
||||
export function formatDate(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return "—";
|
||||
const d = new Date(dateStr);
|
||||
// Date-ONLY strings ("2026-06-01") parse as UTC midnight per the spec, which
|
||||
// renders as the PREVIOUS day in timezones west of UTC — a US viewer's June
|
||||
// attendance print started with 31. 5. Parse the parts instead so the
|
||||
// calendar day is identical in every timezone. Datetime strings keep the
|
||||
// local-parse behavior (naive wire times are already local).
|
||||
const m = String(dateStr).match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
const d = m
|
||||
? new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]))
|
||||
: new Date(dateStr);
|
||||
return d.toLocaleDateString("cs-CZ");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user