feat(api): explain holiday-aware vacation deductions

This commit is contained in:
2026-03-31 22:42:00 +02:00
parent 8acfbf8c3e
commit cb363ca5b3
7 changed files with 857 additions and 99 deletions
+21 -80
View File
@@ -5,12 +5,12 @@ import { findUniqueOrThrow } from "../db/helpers.js";
import { RESOURCE_BRIEF_SELECT } from "../db/selects.js";
import { anonymizeResource, anonymizeUser, getAnonymizationDirectory } from "../lib/anonymization.js";
import { loadResourceHolidayContext } from "../lib/resource-holiday-context.js";
import {
VACATION_BALANCE_TYPES,
type VacationChargeableInput,
} from "../lib/vacation-deduction-snapshot.js";
import { countCalendarDaysInPeriod, countVacationChargeableDays } from "../lib/vacation-day-count.js";
import { protectedProcedure, type TRPCContext } from "../trpc.js";
import {
buildVacationPreview,
findVacationResourceChapter,
listChapterVacationOverlaps,
} from "./vacation-read-support.js";
type VacationReadContext = Pick<TRPCContext, "db" | "dbUser">;
@@ -138,53 +138,13 @@ export const vacationReadProcedures = {
input.startDate,
input.endDate,
);
const vacation: Pick<VacationChargeableInput, "startDate" | "endDate" | "isHalfDay"> = {
return buildVacationPreview({
type: input.type,
startDate: input.startDate,
endDate: input.endDate,
isHalfDay: input.isHalfDay ?? false,
};
const requestedDays = countCalendarDaysInPeriod(vacation);
const effectiveDays = VACATION_BALANCE_TYPES.has(input.type)
? countVacationChargeableDays({
vacation,
countryCode: holidayContext.countryCode,
federalState: holidayContext.federalState,
metroCityName: holidayContext.metroCityName,
calendarHolidayStrings: holidayContext.calendarHolidayStrings,
publicHolidayStrings: holidayContext.publicHolidayStrings,
})
: requestedDays;
const publicHolidayDates = [...new Set([
...holidayContext.calendarHolidayStrings,
...holidayContext.publicHolidayStrings,
])].sort();
const holidayDetails = publicHolidayDates.map((date) => ({
date,
source:
holidayContext.calendarHolidayStrings.includes(date) && holidayContext.publicHolidayStrings.includes(date)
? "CALENDAR_AND_LEGACY"
: holidayContext.calendarHolidayStrings.includes(date)
? "CALENDAR"
: "LEGACY_PUBLIC_HOLIDAY",
}));
return {
requestedDays,
effectiveDays,
deductedDays: VACATION_BALANCE_TYPES.has(input.type) ? effectiveDays : 0,
publicHolidayDates,
holidayDetails,
holidayContext: {
countryCode: holidayContext.countryCode ?? null,
countryName: holidayContext.countryName ?? null,
federalState: holidayContext.federalState ?? null,
metroCityName: holidayContext.metroCityName ?? null,
sources: {
hasCalendarHolidays: holidayContext.calendarHolidayStrings.length > 0,
hasLegacyPublicHolidayEntries: holidayContext.publicHolidayStrings.length > 0,
},
},
};
holidayContext,
});
}),
list: protectedProcedure
@@ -316,27 +276,16 @@ export const vacationReadProcedures = {
.query(async ({ ctx, input }) => {
await assertCanReadVacationResource(ctx, input.resourceId);
const resource = await ctx.db.resource.findUnique({
where: { id: input.resourceId },
select: { chapter: true },
});
if (!resource?.chapter) {
const chapter = await findVacationResourceChapter(ctx.db, input.resourceId);
if (!chapter) {
return [];
}
return ctx.db.vacation.findMany({
where: {
resource: { chapter: resource.chapter },
resourceId: { not: input.resourceId },
status: { in: [VacationStatus.APPROVED, VacationStatus.PENDING] },
startDate: { lte: input.endDate },
endDate: { gte: input.startDate },
},
include: {
resource: { select: RESOURCE_BRIEF_SELECT },
},
orderBy: { startDate: "asc" },
take: 20,
return listChapterVacationOverlaps(ctx.db, {
chapter,
resourceId: input.resourceId,
startDate: input.startDate,
endDate: input.endDate,
});
}),
@@ -372,19 +321,11 @@ export const vacationReadProcedures = {
});
}
const overlaps = await ctx.db.vacation.findMany({
where: {
resource: { chapter: resource.chapter },
resourceId: { not: input.resourceId },
status: { in: [VacationStatus.APPROVED, VacationStatus.PENDING] },
startDate: { lte: input.endDate },
endDate: { gte: input.startDate },
},
include: {
resource: { select: RESOURCE_BRIEF_SELECT },
},
orderBy: { startDate: "asc" },
take: 20,
const overlaps = await listChapterVacationOverlaps(ctx.db, {
chapter: resource.chapter,
resourceId: input.resourceId,
startDate: input.startDate,
endDate: input.endDate,
});
return mapTeamOverlapDetail({