Prisma truncates a JS Date used in a WHERE filter on a @db.Date column to its UTC date part. Under TZ=Europe/Prague a local-midnight boundary (new Date(y,m,d) = 22:00/23:00Z of the previous day) therefore filtered as the PREVIOUS calendar date. Same class as the dashboard fix; full sweep of every @db.Date filter in the codebase. New shared helper utcMidnightOfLocalDay() in src/utils/date.ts. Fixed (all previously off by one day): - attendance.service: getStatus today+month windows; getWorkfund (last day of each month was double-counted across months); getPrintData (monthly print included prev month's last day); listAttendance (admin month view included prev month's last day AND dropped the selected month's last day); createAttendance duplicate/overlap validation (checked only the PREVIOUS day - same-day duplicates were never caught, neighbors falsely rejected) - invoice-alerts: the 'splatnost za 3 dny' advance alert had NEVER fired (due==today+3 was outside the fetched window); window computation extracted as computeAlertWindow() + pure tests - invoices.service: month list/totals filter dropped invoices issued on the month's last day; getInvoiceStats month/year bounds; markOverdueInvoices boundary; auto paid_date could store yesterday during 00:00-02:00 - received-invoices auto paid_date, issued-orders default order_date: same night-window write hazard, now via the helper - attendance.schema: shift_date hardened to isoDateString (bare YYYY-MM-DD) +9 regression tests (month boundaries, same-day duplicate, last-day-of-month invoice in list+totals, alert window). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
57 lines
2.3 KiB
TypeScript
57 lines
2.3 KiB
TypeScript
/**
|
||
* Centralized date/time helpers.
|
||
*
|
||
* Prisma stores DateTime as UTC in MySQL DATETIME columns.
|
||
* The Date.toJSON override in config/env.ts serializes using local getters,
|
||
* so the frontend always receives local time. These helpers ensure
|
||
* consistent local-time formatting whenever we need a string outside
|
||
* of JSON serialization (e.g., building lookup keys, shift_date strings).
|
||
*/
|
||
|
||
/**
|
||
* UTC midnight of the LOCAL calendar day of `d` (default: now).
|
||
*
|
||
* Prisma truncates a JS Date used against a `@db.Date` column (filter or
|
||
* write) to its **UTC** date part. With TZ=Europe/Prague, a local-midnight
|
||
* Date — `new Date(y, m, d)` — is 22:00/23:00 UTC of the PREVIOUS day, so it
|
||
* filters/stores as the previous calendar date; and a bare `new Date()`
|
||
* written to a `@db.Date` column stores yesterday during the 00:00–02:00
|
||
* Prague window. Always build `@db.Date` values and range boundaries from
|
||
* `Date.UTC(...)` of the LOCAL calendar components — this helper does exactly
|
||
* that for "today" (or any reference Date).
|
||
*/
|
||
export function utcMidnightOfLocalDay(d: Date = new Date()): Date {
|
||
return new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
||
}
|
||
|
||
/** YYYY-MM-DD in local time */
|
||
export function localDateStr(d: Date): string {
|
||
const y = d.getFullYear();
|
||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||
const day = String(d.getDate()).padStart(2, "0");
|
||
return `${y}-${m}-${day}`;
|
||
}
|
||
|
||
/** YYYY-MM in local time */
|
||
export function localMonthStr(d: Date): string {
|
||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
||
}
|
||
|
||
/** HH:MM in local time */
|
||
export function localTimeStr(d: Date): string {
|
||
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
||
}
|
||
|
||
/** DD.MM.YYYY in local time (Czech date format) */
|
||
export function localDateCzStr(d: Date): string {
|
||
return `${String(d.getDate()).padStart(2, "0")}.${String(d.getMonth() + 1).padStart(2, "0")}.${d.getFullYear()}`;
|
||
}
|
||
|
||
/** DD.MM.YYYY HH:MM:SS in local time (Czech datetime format) */
|
||
export function localDateTimeCzStr(d: Date): string {
|
||
const h = String(d.getHours()).padStart(2, "0");
|
||
const min = String(d.getMinutes()).padStart(2, "0");
|
||
const s = String(d.getSeconds()).padStart(2, "0");
|
||
return `${localDateCzStr(d)} ${h}:${min}:${s}`;
|
||
}
|