import { z } from "zod"; import { TRPCError } from "@trpc/server"; import { getEntitlementBalance as getEntitlementBalanceUseCase, getEntitlementBalanceDetail as getEntitlementBalanceDetailUseCase, getEntitlementSync, getEntitlementYearSummary as getEntitlementYearSummaryUseCase, getEntitlementYearSummaryDetail as getEntitlementYearSummaryDetailUseCase, setEntitlement as setEntitlementUseCase, bulkSetEntitlements as bulkSetEntitlementsUseCase, type ReadEntitlementBalanceDeps, } from "@capakraken/application"; import { createAuditEntry } from "../lib/audit.js"; import { loadResourceHolidayContext } from "../lib/resource-holiday-context.js"; import { countCalendarDaysInPeriod, countVacationChargeableDays } from "../lib/vacation-day-count.js"; import { countVacationChargeableDaysFromSnapshot, parseVacationSnapshotDateList, } from "../lib/vacation-deduction-snapshot.js"; import type { TRPCContext } from "../trpc.js"; import { buildVacationPreview } from "./vacation-read-support.js"; export const EntitlementBalanceInputSchema = z.object({ resourceId: z.string(), year: z.number().int().min(2000).max(2100).default(new Date().getFullYear()), }); export const EntitlementGetInputSchema = z.object({ resourceId: z.string(), year: z.number().int(), }); export const EntitlementSetInputSchema = z.object({ resourceId: z.string(), year: z.number().int(), entitledDays: z.number().min(0).max(365), }); export const EntitlementBulkSetInputSchema = z.object({ year: z.number().int(), entitledDays: z.number().min(0).max(365), resourceIds: z.array(z.string()).optional(), }); export const EntitlementYearSummaryInputSchema = z.object({ year: z.number().int().min(2000).max(2100).default(new Date().getFullYear()), chapter: z.string().optional(), }); export const EntitlementYearSummaryDetailInputSchema = EntitlementYearSummaryInputSchema.extend({ resourceName: z.string().optional(), }); function buildReadDeps(db: TRPCContext["db"]): ReadEntitlementBalanceDeps { return { loadResourceHolidayContext: (innerDb, resourceId, periodStart, periodEnd) => loadResourceHolidayContext(innerDb as TRPCContext["db"], resourceId, periodStart, periodEnd), countCalendarDaysInPeriod, countVacationChargeableDays, countVacationChargeableDaysFromSnapshot, parseVacationSnapshotDateList, buildVacationPreview, }; } type EntitlementReadContext = Pick; type EntitlementWriteContext = Pick; export async function getEntitlementBalance( ctx: EntitlementReadContext, input: z.infer, ) { if (ctx.dbUser) { const allowedRoles = ["ADMIN", "MANAGER", "CONTROLLER"]; if (!allowedRoles.includes(ctx.dbUser.systemRole)) { const resource = await ctx.db.resource.findUnique({ where: { id: input.resourceId }, select: { userId: true }, }); if (!resource || resource.userId !== ctx.dbUser.id) { throw new TRPCError({ code: "FORBIDDEN", message: "You can only view your own vacation balance", }); } } } return getEntitlementBalanceUseCase(ctx.db, input, buildReadDeps(ctx.db)); } export async function getEntitlementBalanceDetail( ctx: EntitlementReadContext, input: z.infer, ) { if (ctx.dbUser) { const allowedRoles = ["ADMIN", "MANAGER", "CONTROLLER"]; if (!allowedRoles.includes(ctx.dbUser.systemRole)) { const resource = await ctx.db.resource.findUnique({ where: { id: input.resourceId }, select: { userId: true }, }); if (!resource || resource.userId !== ctx.dbUser.id) { throw new TRPCError({ code: "FORBIDDEN", message: "You can only view your own vacation balance", }); } } } return getEntitlementBalanceDetailUseCase(ctx.db, input, buildReadDeps(ctx.db)); } export async function getEntitlement( ctx: Pick, input: z.infer, ) { return getEntitlementSync(ctx.db, input, buildReadDeps(ctx.db)); } export async function setEntitlement( ctx: EntitlementWriteContext, input: z.infer, ) { const { result, existing } = await setEntitlementUseCase(ctx.db, input); if (existing) { void createAuditEntry({ db: ctx.db, entityType: "VacationEntitlement", entityId: result.id, entityName: `Entitlement ${input.resourceId} / ${input.year}`, action: "UPDATE", ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}), before: existing as unknown as Record, after: result as unknown as Record, source: "ui", summary: `Updated entitlement from ${existing.entitledDays} to ${input.entitledDays} days (${input.year})`, }); } else { void createAuditEntry({ db: ctx.db, entityType: "VacationEntitlement", entityId: result.id, entityName: `Entitlement ${input.resourceId} / ${input.year}`, action: "CREATE", ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}), after: result as unknown as Record, source: "ui", summary: `Set entitlement to ${input.entitledDays} days (${input.year})`, }); } return result; } export async function bulkSetEntitlements( ctx: EntitlementWriteContext, input: z.infer, ) { const result = await bulkSetEntitlementsUseCase(ctx.db, input); void createAuditEntry({ db: ctx.db, entityType: "VacationEntitlement", entityId: `bulk-${input.year}`, entityName: `Bulk Entitlement ${input.year}`, action: "UPDATE", ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}), after: { year: input.year, entitledDays: input.entitledDays, resourceCount: result.updated } as unknown as Record, source: "ui", summary: `Bulk set entitlement to ${input.entitledDays} days for ${result.updated} resources (${input.year})`, }); return result; } export async function getEntitlementYearSummary( ctx: Pick, input: z.infer, ) { return getEntitlementYearSummaryUseCase(ctx.db, input, buildReadDeps(ctx.db)); } export async function getEntitlementYearSummaryDetail( ctx: Pick, input: z.infer, ) { return getEntitlementYearSummaryDetailUseCase(ctx.db, input, buildReadDeps(ctx.db)); }