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>
128 lines
4.6 KiB
TypeScript
128 lines
4.6 KiB
TypeScript
import { z } from "zod";
|
|
import {
|
|
intIdFromForm,
|
|
isoDateString,
|
|
nullableDateTimeString,
|
|
nullableIntIdFromForm,
|
|
numberFromForm,
|
|
numberInRange,
|
|
timeString,
|
|
} from "./common";
|
|
|
|
export const AttendanceNotesSchema = z.object({
|
|
notes: z.string().nullish(),
|
|
});
|
|
|
|
export const AttendanceUpdateAddressSchema = z.object({
|
|
address: z.string().max(500).nullish(),
|
|
punch_action: z.string().optional().default("arrival"),
|
|
});
|
|
|
|
export const AttendanceSwitchProjectSchema = z.object({
|
|
project_id: nullableIntIdFromForm.nullish(),
|
|
});
|
|
|
|
export const AttendanceBalancesSchema = z.object({
|
|
user_id: intIdFromForm,
|
|
year: numberFromForm.optional(),
|
|
action_type: z.enum(["edit", "reset"]),
|
|
vacation_total: numberFromForm.optional(),
|
|
vacation_used: numberFromForm.optional(),
|
|
sick_used: numberFromForm.optional(),
|
|
});
|
|
|
|
export const AttendanceBulkSchema = z.object({
|
|
month: z.string().regex(/^\d{4}-\d{2}$/, "Měsíc je povinný (formát YYYY-MM)"),
|
|
user_ids: z
|
|
.array(intIdFromForm)
|
|
.min(1, "Vyberte alespoň jednoho zaměstnance"),
|
|
arrival_time: timeString.optional().default("08:00"),
|
|
departure_time: timeString.optional().default("16:30"),
|
|
break_start_time: timeString.optional().default("12:00"),
|
|
break_end_time: timeString.optional().default("12:30"),
|
|
});
|
|
|
|
export const AttendanceLeaveSchema = z.object({
|
|
user_id: intIdFromForm.optional(),
|
|
date_from: z.string().min(1, "Datum je povinné"),
|
|
date_to: z.string().optional(),
|
|
leave_type: z
|
|
.enum(["vacation", "sick", "unpaid"])
|
|
.optional()
|
|
.default("vacation"),
|
|
leave_hours: numberFromForm.optional(),
|
|
notes: z.string().nullish(),
|
|
});
|
|
|
|
const ProjectLogSchema = z.object({
|
|
project_id: intIdFromForm,
|
|
hours: z.coerce.number().min(0).default(0),
|
|
minutes: numberInRange(0, 59).default(0),
|
|
});
|
|
|
|
export const AttendancePunchSchema = z.object({
|
|
punch_action: z.string(),
|
|
latitude: numberFromForm.nullish(),
|
|
longitude: numberFromForm.nullish(),
|
|
accuracy: numberFromForm.nullish(),
|
|
address: z.string().max(500).nullish(),
|
|
});
|
|
|
|
// arrival/departure/break fields travel as COMBINED local datetimes
|
|
// ("YYYY-MM-DDTHH:MM:00", built by the admin forms' combineDatetime from a
|
|
// date + time input) — the service parses them with `new Date(...)`. They are
|
|
// NOT bare HH:MM times.
|
|
export const CreateAttendanceSchema = z.object({
|
|
user_id: intIdFromForm.optional(),
|
|
// Must be a bare "YYYY-MM-DD" (parses to UTC midnight) — the service's
|
|
// duplicate-validation window and the stored @db.Date both depend on it.
|
|
// isoDateString leniently strips a trailing time component.
|
|
shift_date: isoDateString,
|
|
arrival_time: nullableDateTimeString.optional(),
|
|
arrival_lat: numberFromForm.nullish(),
|
|
arrival_lng: numberFromForm.nullish(),
|
|
arrival_accuracy: numberFromForm.nullish(),
|
|
arrival_address: z.string().nullish(),
|
|
departure_time: nullableDateTimeString.optional(),
|
|
departure_lat: numberFromForm.nullish(),
|
|
departure_lng: numberFromForm.nullish(),
|
|
departure_accuracy: numberFromForm.nullish(),
|
|
departure_address: z.string().nullish(),
|
|
break_start: nullableDateTimeString.optional(),
|
|
break_end: nullableDateTimeString.optional(),
|
|
notes: z.string().nullish(),
|
|
project_id: nullableIntIdFromForm.nullish(),
|
|
leave_type: z
|
|
.enum(["work", "vacation", "sick", "unpaid"])
|
|
.optional()
|
|
.default("work"),
|
|
leave_hours: numberFromForm.nullish(),
|
|
project_logs: z.array(ProjectLogSchema).optional(),
|
|
});
|
|
|
|
export const UpdateAttendanceSchema = z.object({
|
|
arrival_time: nullableDateTimeString.optional(),
|
|
departure_time: nullableDateTimeString.optional(),
|
|
break_start: nullableDateTimeString.optional(),
|
|
break_end: nullableDateTimeString.optional(),
|
|
notes: z.string().nullish(),
|
|
project_id: nullableIntIdFromForm.optional(),
|
|
leave_type: z.enum(["work", "vacation", "sick", "unpaid"]).optional(),
|
|
leave_hours: numberFromForm.nullish(),
|
|
project_logs: z.array(ProjectLogSchema).optional(),
|
|
});
|
|
|
|
export type AttendanceNotesInput = z.infer<typeof AttendanceNotesSchema>;
|
|
export type AttendanceUpdateAddressInput = z.infer<
|
|
typeof AttendanceUpdateAddressSchema
|
|
>;
|
|
export type AttendanceSwitchProjectInput = z.infer<
|
|
typeof AttendanceSwitchProjectSchema
|
|
>;
|
|
export type AttendanceBalancesInput = z.infer<typeof AttendanceBalancesSchema>;
|
|
export type AttendanceBulkInput = z.infer<typeof AttendanceBulkSchema>;
|
|
export type AttendanceLeaveInput = z.infer<typeof AttendanceLeaveSchema>;
|
|
export type AttendancePunchInput = z.infer<typeof AttendancePunchSchema>;
|
|
export type CreateAttendanceInput = z.infer<typeof CreateAttendanceSchema>;
|
|
export type UpdateAttendanceInput = z.infer<typeof UpdateAttendanceSchema>;
|