import { Prisma, VacationType, type PrismaClient } from "@capakraken/db"; import { DEFAULT_CALCULATION_RULES } from "@capakraken/engine"; import type { AbsenceDay, CalculationRule } from "@capakraken/shared"; import { logger } from "../lib/logger.js"; function isMissingOptionalTableError(error: unknown, tableHints: string[]): boolean { if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code !== "P2021") { return false; } const table = typeof error.meta?.table === "string" ? error.meta.table.toLowerCase() : ""; const message = error.message.toLowerCase(); return tableHints.some((hint) => table.includes(hint) || message.includes(hint)); } if (typeof error !== "object" || error === null || !("code" in error)) { return false; } const candidate = error as { code?: unknown; message?: unknown; meta?: { table?: unknown }; }; const code = typeof candidate.code === "string" ? candidate.code : ""; if (code !== "P2021") { return false; } const table = typeof candidate.meta?.table === "string" ? candidate.meta.table.toLowerCase() : ""; const message = typeof candidate.message === "string" ? candidate.message.toLowerCase() : ""; return tableHints.some((hint) => table.includes(hint) || message.includes(hint)); } export async function loadTimelineCalculationRules( db: PrismaClient, ): Promise { const calculationRuleModel = (db as PrismaClient & { calculationRule?: { findMany?: (args: unknown) => Promise }; }).calculationRule; if (!calculationRuleModel || typeof calculationRuleModel.findMany !== "function") { return DEFAULT_CALCULATION_RULES; } try { const rules = await calculationRuleModel.findMany({ where: { isActive: true }, orderBy: [{ priority: "desc" }], }); if (rules.length > 0) { return rules as unknown as CalculationRule[]; } } catch (error) { if (!isMissingOptionalTableError(error, ["calculationrule", "calculation_rule", "calculation_rules"])) { logger.error({ err: error }, "Failed to load active calculation rules for timeline"); throw error; } } return DEFAULT_CALCULATION_RULES; } export async function buildTimelineAbsenceDays( db: PrismaClient, resourceId: string, startDate: Date, endDate: Date, ): Promise<{ absenceDays: AbsenceDay[]; legacyVacationDates: Date[] }> { const absenceDays: AbsenceDay[] = []; const legacyVacationDates: Date[] = []; try { const vacations = await db.vacation.findMany({ where: { resourceId, status: "APPROVED", startDate: { lte: endDate }, endDate: { gte: startDate }, }, select: { startDate: true, endDate: true, type: true, isHalfDay: true }, }); for (const vacation of vacations) { const cur = new Date(vacation.startDate); cur.setUTCHours(0, 0, 0, 0); const vacationEnd = new Date(vacation.endDate); vacationEnd.setUTCHours(0, 0, 0, 0); const triggerType = vacation.type === VacationType.SICK ? "SICK" as const : vacation.type === VacationType.PUBLIC_HOLIDAY ? "PUBLIC_HOLIDAY" as const : "VACATION" as const; while (cur <= vacationEnd) { absenceDays.push({ date: new Date(cur), type: triggerType, ...(vacation.isHalfDay ? { isHalfDay: true } : {}), }); if (triggerType === "VACATION") { legacyVacationDates.push(new Date(cur)); } cur.setUTCDate(cur.getUTCDate() + 1); } } } catch (error) { if (!isMissingOptionalTableError(error, ["vacation", "vacations"])) { logger.error( { err: error, resourceId, startDate, endDate }, "Failed to load timeline absence days", ); throw error; } } return { absenceDays, legacyVacationDates }; }