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:
@@ -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