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>
This commit is contained in:
BOHA
2026-06-10 11:29:28 +02:00
parent 6a22195c7d
commit 975a555af5
9 changed files with 361 additions and 45 deletions

View File

@@ -8,6 +8,22 @@
* 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();