Files
app/src/utils/date.ts
BOHA 975a555af5 fix(dates): @db.Date filters built from local midnight queried the wrong day
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>
2026-06-10 11:29:28 +02:00

57 lines
2.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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:0002: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}`;
}