import { z } from "zod"; import { assertCan, ServiceError, type ServiceCtx } from "@/server/services/context"; import { requireVisibleWorkOrder } from "@/server/services/work-orders/visibility"; import { audit } from "./common"; /** Manual time corrections (Spec ยง12.2): only with `field:correct_time`, reason mandatory, always audited. */ export const TIME_ENTRY_TYPES = ["travel", "work", "break", "material_procurement", "return_travel", "interruption"] as const; export const timeCorrectionSchema = z .object({ timeEntryId: z.string().min(1).max(64), startedAt: z.coerce.date(), endedAt: z.coerce.date().nullish(), type: z.enum(TIME_ENTRY_TYPES).optional(), reason: z.string().trim().min(3).max(1000), }) .refine((v) => !v.endedAt || v.endedAt.getTime() >= v.startedAt.getTime(), { message: "end before start", path: ["endedAt"] }); export type TimeCorrectionInput = z.input; export async function correctTimeEntry(ctx: ServiceCtx, raw: TimeCorrectionInput) { assertCan(ctx, "field:correct_time"); const parsed = timeCorrectionSchema.safeParse(raw); if (!parsed.success) throw new ServiceError("invalid", "invalid time correction", parsed.error.issues); const input = parsed.data; const entry = await ctx.db.timeEntry.findFirst({ where: { id: input.timeEntryId }, include: { workSession: { select: { workOrderId: true } } }, }); if (!entry) throw new ServiceError("not_found", "time entry not found"); await requireVisibleWorkOrder(ctx, entry.workSession.workOrderId, { id: true }); if (input.startedAt.getTime() > Date.now() + 60_000) throw new ServiceError("invalid", "start in the future"); const before = { type: entry.type, startedAt: entry.startedAt, endedAt: entry.endedAt, corrected: entry.corrected, correctionReason: entry.correctionReason }; const updated = await ctx.db.timeEntry.update({ where: { id: entry.id }, data: { startedAt: input.startedAt, endedAt: input.endedAt === undefined ? entry.endedAt : input.endedAt, type: input.type ?? entry.type, corrected: true, correctionReason: input.reason, correctedById: ctx.userId, }, }); await audit(ctx, "update", "time_entry", entry.id, before, { type: updated.type, startedAt: updated.startedAt, endedAt: updated.endedAt, corrected: true, correctionReason: input.reason, }); return updated; }