-
{conflictsData?.conflicts && conflictsData.conflicts.length > 0 && (
-
-
- {t("holidays_list")} ({settings.holidays?.filter((h) => h.enabled).length || 0} {t("enabled")}
- )
-
+
+ {/* Header with title and country selector */}
+
+
+
{t("holidays")}
+
{t("holidays_description")}
-
- ) : (
-
- )}
+
+ {/* Holidays list - inner container */}
+ {settings?.countryCode ? (
+
+ {settings.holidays && settings.holidays.length > 0 ? (
+ settings.holidays.map((holiday) => (
+
+ ))
+ ) : (
+
+ {t("no_holidays_found_for_country")}
+
+ )}
+
+ ) : (
+
+
+
+
+
{t("no_holidays_selected")}
+
{t("select_country_to_see_holidays")}
+
+ )}
+
);
diff --git a/apps/web/modules/settings/my-account/out-of-office-view.tsx b/apps/web/modules/settings/my-account/out-of-office-view.tsx
index b1410fc464..7e6ec2a354 100644
--- a/apps/web/modules/settings/my-account/out-of-office-view.tsx
+++ b/apps/web/modules/settings/my-account/out-of-office-view.tsx
@@ -6,7 +6,7 @@ import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { CreateOrEditOutOfOfficeEntryModal } from "@calcom/features/settings/outOfOffice/CreateOrEditOutOfOfficeModal";
import type { BookingRedirectForm } from "@calcom/features/settings/outOfOffice/CreateOrEditOutOfOfficeModal";
-import { HolidaysView } from "@calcom/features/settings/outOfOffice/HolidaysView";
+import { HolidaysView } from "./holidays-view";
import OutOfOfficeEntriesList from "@calcom/features/settings/outOfOffice/OutOfOfficeEntriesList";
import { OutOfOfficeTab } from "@calcom/features/settings/outOfOffice/OutOfOfficeToggleGroup";
diff --git a/packages/features/availability/lib/calculateHolidayBlockedDates.test.ts b/packages/features/availability/lib/calculateHolidayBlockedDates.test.ts
index b8c39800fb..fef817636c 100644
--- a/packages/features/availability/lib/calculateHolidayBlockedDates.test.ts
+++ b/packages/features/availability/lib/calculateHolidayBlockedDates.test.ts
@@ -1,8 +1,9 @@
-import prismaMock from "../../../../tests/libs/__mocks__/prismaMock";
-
import { beforeEach, describe, expect, it, vi } from "vitest";
import dayjs from "@calcom/dayjs";
+import { getHolidayService } from "@calcom/lib/holidays";
+
+import { UserAvailabilityService } from "./getUserAvailability";
vi.mock("@calcom/lib/holidays", () => ({
getHolidayService: vi.fn(() => ({
@@ -10,14 +11,24 @@ vi.mock("@calcom/lib/holidays", () => ({
})),
}));
-import { getHolidayService } from "@calcom/lib/holidays";
+// Helper to create working hours with proper Date types for startTime/endTime
+// Times are stored as Date objects with only time component (1970-01-01)
+const createWorkingHours = (days: number[]) => ({
+ days,
+ startTime: new Date("1970-01-01T09:00:00.000Z"), // 9 AM
+ endTime: new Date("1970-01-01T17:00:00.000Z"), // 5 PM
+});
-import { UserAvailabilityService } from "./getUserAvailability";
+const mockHolidayRepo = {
+ findUserSettingsSelect: vi.fn(),
+};
const mockDependencies = {
oooRepo: {} as never,
bookingRepo: {} as never,
redisClient: {} as never,
+ eventTypeRepo: {} as never,
+ holidayRepo: mockHolidayRepo as never,
};
describe("UserAvailabilityService.calculateHolidayBlockedDates", () => {
@@ -34,17 +45,17 @@ describe("UserAvailabilityService.calculateHolidayBlockedDates", () => {
});
it("should return empty object when user has no holiday settings", async () => {
- prismaMock.userHolidaySettings.findUnique.mockResolvedValue(null);
+ mockHolidayRepo.findUserSettingsSelect.mockResolvedValue(null);
const result = await service.calculateHolidayBlockedDates(
123,
new Date("2025-01-01"),
new Date("2025-01-31"),
- [{ days: [1, 2, 3, 4, 5] }] // Monday to Friday
+ [createWorkingHours([1, 2, 3, 4, 5])] // Monday to Friday, 9am-5pm
);
- expect(prismaMock.userHolidaySettings.findUnique).toHaveBeenCalledWith({
- where: { userId: 123 },
+ expect(mockHolidayRepo.findUserSettingsSelect).toHaveBeenCalledWith({
+ userId: 123,
select: {
countryCode: true,
disabledIds: true,
@@ -54,33 +65,25 @@ describe("UserAvailabilityService.calculateHolidayBlockedDates", () => {
});
it("should return empty object when user has no country selected", async () => {
- prismaMock.userHolidaySettings.findUnique.mockResolvedValue({
- id: 1,
- userId: 123,
+ mockHolidayRepo.findUserSettingsSelect.mockResolvedValue({
countryCode: null,
disabledIds: [],
- createdAt: new Date(),
- updatedAt: new Date(),
});
const result = await service.calculateHolidayBlockedDates(
123,
new Date("2025-01-01"),
new Date("2025-01-31"),
- [{ days: [1, 2, 3, 4, 5] }]
+ [createWorkingHours([1, 2, 3, 4, 5])]
);
expect(result).toEqual({});
});
it("should return empty object when no holidays in date range", async () => {
- prismaMock.userHolidaySettings.findUnique.mockResolvedValue({
- id: 1,
- userId: 123,
+ mockHolidayRepo.findUserSettingsSelect.mockResolvedValue({
countryCode: "US",
disabledIds: [],
- createdAt: new Date(),
- updatedAt: new Date(),
});
mockHolidayService.getHolidayDatesInRange.mockResolvedValue([]);
@@ -89,7 +92,7 @@ describe("UserAvailabilityService.calculateHolidayBlockedDates", () => {
123,
new Date("2025-02-01"),
new Date("2025-02-28"),
- [{ days: [1, 2, 3, 4, 5] }]
+ [createWorkingHours([1, 2, 3, 4, 5])]
);
// Dates are expanded to full day range (startOfDay to endOfDay in UTC)
@@ -108,13 +111,9 @@ describe("UserAvailabilityService.calculateHolidayBlockedDates", () => {
});
it("should return holiday data for dates that match user's working days", async () => {
- prismaMock.userHolidaySettings.findUnique.mockResolvedValue({
- id: 1,
- userId: 123,
+ mockHolidayRepo.findUserSettingsSelect.mockResolvedValue({
countryCode: "US",
disabledIds: [],
- createdAt: new Date(),
- updatedAt: new Date(),
});
// Wednesday, January 1, 2025 - New Year's Day
@@ -134,7 +133,7 @@ describe("UserAvailabilityService.calculateHolidayBlockedDates", () => {
123,
new Date("2025-01-01"),
new Date("2025-01-31"),
- [{ days: [1, 2, 3, 4, 5] }] // Monday(1) to Friday(5) - includes Wednesday(3)
+ [createWorkingHours([1, 2, 3, 4, 5])] // Monday(1) to Friday(5) - includes Wednesday(3)
);
expect(result).toEqual({
@@ -142,19 +141,15 @@ describe("UserAvailabilityService.calculateHolidayBlockedDates", () => {
fromUser: null,
toUser: null,
reason: "New Year's Day",
- emoji: "๐",
+ emoji: "๐",
},
});
});
it("should skip holidays that fall on non-working days", async () => {
- prismaMock.userHolidaySettings.findUnique.mockResolvedValue({
- id: 1,
- userId: 123,
+ mockHolidayRepo.findUserSettingsSelect.mockResolvedValue({
countryCode: "US",
disabledIds: [],
- createdAt: new Date(),
- updatedAt: new Date(),
});
// Saturday, December 25, 2027 - Christmas Day
@@ -178,7 +173,7 @@ describe("UserAvailabilityService.calculateHolidayBlockedDates", () => {
123,
new Date("2027-12-01"),
new Date("2027-12-31"),
- [{ days: [1, 2, 3, 4, 5] }]
+ [createWorkingHours([1, 2, 3, 4, 5])]
);
// Christmas 2027 is on Saturday (day 6), should be skipped
@@ -187,13 +182,9 @@ describe("UserAvailabilityService.calculateHolidayBlockedDates", () => {
});
it("should handle multiple holidays correctly", async () => {
- prismaMock.userHolidaySettings.findUnique.mockResolvedValue({
- id: 1,
- userId: 123,
+ mockHolidayRepo.findUserSettingsSelect.mockResolvedValue({
countryCode: "US",
disabledIds: [],
- createdAt: new Date(),
- updatedAt: new Date(),
});
mockHolidayService.getHolidayDatesInRange.mockResolvedValue([
@@ -221,7 +212,7 @@ describe("UserAvailabilityService.calculateHolidayBlockedDates", () => {
123,
new Date("2025-01-01"),
new Date("2025-01-31"),
- [{ days: [1, 2, 3, 4, 5] }]
+ [createWorkingHours([1, 2, 3, 4, 5])]
);
expect(result).toEqual({
@@ -229,25 +220,21 @@ describe("UserAvailabilityService.calculateHolidayBlockedDates", () => {
fromUser: null,
toUser: null,
reason: "New Year's Day",
- emoji: "๐",
+ emoji: "๐",
},
"2025-01-20": {
fromUser: null,
toUser: null,
reason: "Martin Luther King Jr. Day",
- emoji: "๐",
+ emoji: "โ",
},
});
});
it("should handle availability with multiple schedules", async () => {
- prismaMock.userHolidaySettings.findUnique.mockResolvedValue({
- id: 1,
- userId: 123,
+ mockHolidayRepo.findUserSettingsSelect.mockResolvedValue({
countryCode: "US",
disabledIds: [],
- createdAt: new Date(),
- updatedAt: new Date(),
});
// Thursday, July 4, 2024 - Independence Day
@@ -269,8 +256,8 @@ describe("UserAvailabilityService.calculateHolidayBlockedDates", () => {
new Date("2024-07-01"),
new Date("2024-07-31"),
[
- { days: [1, 3] }, // Monday, Wednesday only
- { days: [2, 4] }, // Tuesday, Thursday - includes the holiday
+ createWorkingHours([1, 3]), // Monday, Wednesday only
+ createWorkingHours([2, 4]), // Tuesday, Thursday - includes the holiday
]
);
@@ -279,19 +266,15 @@ describe("UserAvailabilityService.calculateHolidayBlockedDates", () => {
fromUser: null,
toUser: null,
reason: "Independence Day",
- emoji: "๐",
+ emoji: "๐",
},
});
});
it("should respect disabled holidays", async () => {
- prismaMock.userHolidaySettings.findUnique.mockResolvedValue({
- id: 1,
- userId: 123,
+ mockHolidayRepo.findUserSettingsSelect.mockResolvedValue({
countryCode: "US",
disabledIds: ["christmas_2025"],
- createdAt: new Date(),
- updatedAt: new Date(),
});
// The holiday service should receive the disabledIds and filter them out
@@ -312,7 +295,7 @@ describe("UserAvailabilityService.calculateHolidayBlockedDates", () => {
123,
new Date("2025-01-01"),
new Date("2025-12-31"),
- [{ days: [1, 2, 3, 4, 5] }]
+ [createWorkingHours([1, 2, 3, 4, 5])]
);
// Verify disabledIds were passed to the service
@@ -329,9 +312,8 @@ describe("UserAvailabilityService.calculateHolidayBlockedDates", () => {
fromUser: null,
toUser: null,
reason: "New Year's Day",
- emoji: "๐",
+ emoji: "๐",
},
});
});
});
-
diff --git a/packages/features/availability/lib/getUserAvailability.ts b/packages/features/availability/lib/getUserAvailability.ts
index 9722865cd4..2ad3acd403 100644
--- a/packages/features/availability/lib/getUserAvailability.ts
+++ b/packages/features/availability/lib/getUserAvailability.ts
@@ -11,6 +11,7 @@ import {
} from "@calcom/features/busyTimes/lib/getBusyTimesFromLimits";
import { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes";
import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";
+import type { PrismaHolidayRepository } from "@calcom/features/holidays/repositories/PrismaHolidayRepository";
import type { PrismaOOORepository } from "@calcom/features/ooo/repositories/PrismaOOORepository";
import type { IRedisService } from "@calcom/features/redis/IRedisService";
import type { DateOverride, WorkingHours } from "@calcom/features/schedules/lib/date-ranges";
@@ -19,6 +20,7 @@ import { getWorkingHours } from "@calcom/lib/availability";
import { stringToDayjsZod } from "@calcom/lib/dayjs";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { getHolidayService } from "@calcom/lib/holidays";
+import { getHolidayEmoji } from "@calcom/lib/holidays/getHolidayEmoji";
import { HttpError } from "@calcom/lib/http-error";
import { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits";
import { parseDurationLimit } from "@calcom/lib/intervalLimits/isDurationLimits";
@@ -26,7 +28,6 @@ import { getPeriodStartDatesBetween as getPeriodStartDatesBetweenUtil } from "@c
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { withReporting } from "@calcom/lib/sentryWrapper";
-import prisma from "@calcom/prisma";
import type {
Booking,
Prisma,
@@ -165,6 +166,7 @@ export interface IUserAvailabilityService {
oooRepo: PrismaOOORepository;
bookingRepo: BookingRepository;
redisClient: IRedisService;
+ holidayRepo: PrismaHolidayRepository;
}
export class UserAvailabilityService {
@@ -709,8 +711,8 @@ export class UserAvailabilityService {
endDate: Date,
availability: GetUserAvailabilityParamsDTO["availability"]
): Promise
{
- const holidaySettings = await prisma.userHolidaySettings.findUnique({
- where: { userId },
+ const holidaySettings = await this.dependencies.holidayRepo.findUserSettingsSelect({
+ userId,
select: {
countryCode: true,
disabledIds: true,
@@ -760,7 +762,7 @@ export class UserAvailabilityService {
fromUser: null,
toUser: null,
reason: holiday.name,
- emoji: "๐",
+ emoji: getHolidayEmoji(holiday.name),
};
}
diff --git a/packages/features/di/containers/AvailableSlots.ts b/packages/features/di/containers/AvailableSlots.ts
index 15fc85c5e6..fd3941898f 100644
--- a/packages/features/di/containers/AvailableSlots.ts
+++ b/packages/features/di/containers/AvailableSlots.ts
@@ -12,6 +12,7 @@ import { eventTypeRepositoryModule } from "../modules/EventType";
import { featuresRepositoryModule } from "../modules/Features";
import { filterHostsModule } from "../modules/FilterHosts";
import { getUserAvailabilityModule } from "../modules/GetUserAvailability";
+import { holidayRepositoryModule } from "../modules/Holiday";
import { noSlotsNotificationModule } from "../modules/NoSlotsNotification";
import { oooRepositoryModule } from "../modules/Ooo";
import { qualifiedHostsModule } from "../modules/QualifiedHosts";
@@ -32,6 +33,7 @@ container.load(DI_TOKENS.MEMBERSHIP_REPOSITORY_MODULE, membershipRepositoryModul
container.load(DI_TOKENS.USER_REPOSITORY_MODULE, userRepositoryModule);
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
container.load(DI_TOKENS.EVENT_TYPE_REPOSITORY_MODULE, eventTypeRepositoryModule);
+container.load(DI_TOKENS.HOLIDAY_REPOSITORY_MODULE, holidayRepositoryModule);
container.load(DI_TOKENS.ROUTING_FORM_RESPONSE_REPOSITORY_MODULE, routingFormResponseRepositoryModule);
container.load(DI_TOKENS.FEATURES_REPOSITORY_MODULE, featuresRepositoryModule);
container.load(DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE_MODULE, checkBookingLimitsModule);
diff --git a/packages/features/di/containers/GetUserAvailability.ts b/packages/features/di/containers/GetUserAvailability.ts
index 6bd9b34613..a5bf404961 100644
--- a/packages/features/di/containers/GetUserAvailability.ts
+++ b/packages/features/di/containers/GetUserAvailability.ts
@@ -8,6 +8,7 @@ import { bookingRepositoryModule } from "../modules/Booking";
import { busyTimesModule } from "../modules/BusyTimes";
import { eventTypeRepositoryModule } from "../modules/EventType";
import { getUserAvailabilityModule } from "../modules/GetUserAvailability";
+import { holidayRepositoryModule } from "../modules/Holiday";
import { oooRepositoryModule } from "../modules/Ooo";
const container = createContainer();
@@ -15,6 +16,7 @@ container.load(DI_TOKENS.PRISMA_MODULE, prismaModule);
container.load(DI_TOKENS.OOO_REPOSITORY_MODULE, oooRepositoryModule);
container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule);
container.load(DI_TOKENS.EVENT_TYPE_REPOSITORY_MODULE, eventTypeRepositoryModule);
+container.load(DI_TOKENS.HOLIDAY_REPOSITORY_MODULE, holidayRepositoryModule);
container.load(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE_MODULE, getUserAvailabilityModule);
container.load(DI_TOKENS.BUSY_TIMES_SERVICE_MODULE, busyTimesModule);
container.load(DI_TOKENS.REDIS_CLIENT, redisModule);
diff --git a/packages/features/di/modules/GetUserAvailability.ts b/packages/features/di/modules/GetUserAvailability.ts
index 42a915f8d6..e8125dd9c7 100644
--- a/packages/features/di/modules/GetUserAvailability.ts
+++ b/packages/features/di/modules/GetUserAvailability.ts
@@ -10,4 +10,5 @@ getUserAvailabilityModule.bind(DI_TOKENS.GET_USER_AVAILABILITY_SERVICE).toClass(
bookingRepo: DI_TOKENS.BOOKING_REPOSITORY,
eventTypeRepo: DI_TOKENS.EVENT_TYPE_REPOSITORY,
redisClient: DI_TOKENS.REDIS_CLIENT,
+ holidayRepo: DI_TOKENS.HOLIDAY_REPOSITORY,
} satisfies Record);
diff --git a/packages/features/di/modules/Holiday.ts b/packages/features/di/modules/Holiday.ts
new file mode 100644
index 0000000000..2fd7d973ce
--- /dev/null
+++ b/packages/features/di/modules/Holiday.ts
@@ -0,0 +1,17 @@
+import { DI_TOKENS } from "@calcom/features/di/tokens";
+import { PrismaHolidayRepository } from "@calcom/features/holidays/repositories/PrismaHolidayRepository";
+
+import { type Container, createModule } from "../di";
+
+export const holidayRepositoryModule = createModule();
+const token = DI_TOKENS.HOLIDAY_REPOSITORY;
+const moduleToken = DI_TOKENS.HOLIDAY_REPOSITORY_MODULE;
+holidayRepositoryModule.bind(token).toClass(PrismaHolidayRepository, [DI_TOKENS.PRISMA_CLIENT]);
+
+export const moduleLoader = {
+ token,
+ loadModule: function (container: Container) {
+ container.load(moduleToken, holidayRepositoryModule);
+ },
+};
+
diff --git a/packages/features/di/tokens.ts b/packages/features/di/tokens.ts
index 9b8d4ab543..6302614fed 100644
--- a/packages/features/di/tokens.ts
+++ b/packages/features/di/tokens.ts
@@ -55,6 +55,8 @@ export const DI_TOKENS = {
LUCKY_USER_SERVICE_MODULE: Symbol("LuckyUserServiceModule"),
HOST_REPOSITORY: Symbol("HostRepository"),
HOST_REPOSITORY_MODULE: Symbol("HostRepositoryModule"),
+ HOLIDAY_REPOSITORY: Symbol("HolidayRepository"),
+ HOLIDAY_REPOSITORY_MODULE: Symbol("HolidayRepositoryModule"),
ATTRIBUTE_REPOSITORY: Symbol("AttributeRepository"),
ATTRIBUTE_REPOSITORY_MODULE: Symbol("AttributeRepositoryModule"),
MEMBERSHIP_SERVICE: Symbol("MembershipService"),
diff --git a/packages/features/holidays/repositories/PrismaHolidayRepository.ts b/packages/features/holidays/repositories/PrismaHolidayRepository.ts
new file mode 100644
index 0000000000..07ee355828
--- /dev/null
+++ b/packages/features/holidays/repositories/PrismaHolidayRepository.ts
@@ -0,0 +1,19 @@
+import type { PrismaClient } from "@calcom/prisma";
+import type { Prisma } from "@calcom/prisma/client";
+
+export class PrismaHolidayRepository {
+ constructor(private prismaClient: PrismaClient) {}
+
+ async findUserSettingsSelect({
+ userId,
+ select,
+ }: {
+ userId: number;
+ select: T;
+ }) {
+ return this.prismaClient.userHolidaySettings.findUnique({
+ where: { userId },
+ select,
+ });
+ }
+}
diff --git a/packages/features/settings/outOfOffice/CreateNewOutOfOfficeEntryButton.tsx b/packages/features/settings/outOfOffice/CreateNewOutOfOfficeEntryButton.tsx
index aeaa8c9cc4..9e7ab5a47e 100644
--- a/packages/features/settings/outOfOffice/CreateNewOutOfOfficeEntryButton.tsx
+++ b/packages/features/settings/outOfOffice/CreateNewOutOfOfficeEntryButton.tsx
@@ -32,12 +32,12 @@ const CreateNewOutOfOfficeEntryButton = ({
);
};
diff --git a/packages/features/settings/outOfOffice/OutOfOfficeToggleGroup.tsx b/packages/features/settings/outOfOffice/OutOfOfficeToggleGroup.tsx
index ae50da613a..91c717b8b6 100644
--- a/packages/features/settings/outOfOffice/OutOfOfficeToggleGroup.tsx
+++ b/packages/features/settings/outOfOffice/OutOfOfficeToggleGroup.tsx
@@ -1,11 +1,11 @@
"use client";
import { usePathname, useRouter } from "next/navigation";
-import { useCallback } from "react";
+import { useCallback, useMemo } from "react";
import { useCompatSearchParams } from "@calcom/embed-core/src/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
-import { ToggleGroup } from "@calcom/ui/components/form";
+import { Select, ToggleGroup } from "@calcom/ui/components/form";
export enum OutOfOfficeTab {
MINE = "mine",
@@ -31,22 +31,47 @@ export const OutOfOfficeToggleGroup = () => {
const selectedTab = searchParams?.get("type") ?? OutOfOfficeTab.MINE;
- const toggleGroupOptions = [
- { value: OutOfOfficeTab.MINE, label: t("my_ooo") },
- { value: OutOfOfficeTab.TEAM, label: t("team_ooo") },
- { value: OutOfOfficeTab.HOLIDAYS, label: t("holidays") },
- ];
+ const tabOptions = useMemo(
+ () => [
+ { value: OutOfOfficeTab.MINE, label: t("my_ooo") },
+ { value: OutOfOfficeTab.TEAM, label: t("team_ooo") },
+ { value: OutOfOfficeTab.HOLIDAYS, label: t("holidays") },
+ ],
+ [t]
+ );
+
+ const handleTabChange = useCallback(
+ (value: string | null) => {
+ if (!value) return;
+ const newQuery = createQueryString("type", value);
+ router.push(`${pathname}?${newQuery}`);
+ },
+ [createQueryString, pathname, router]
+ );
+
+ const selectedOption = useMemo(
+ () => tabOptions.find((opt) => opt.value === selectedTab) ?? tabOptions[0],
+ [selectedTab, tabOptions]
+ );
return (
- {
- if (!value) return;
- const newQuery = createQueryString("type", value);
- router.push(`${pathname}?${newQuery}`);
- }}
- options={toggleGroupOptions}
- />
+ <>
+ {/* Mobile: Select dropdown */}
+
+
+ {/* Desktop: Toggle group */}
+
+ >
);
};
diff --git a/packages/lib/holidays/HolidayService.ts b/packages/lib/holidays/HolidayService.ts
index 32e24e920c..455eeffc68 100644
--- a/packages/lib/holidays/HolidayService.ts
+++ b/packages/lib/holidays/HolidayService.ts
@@ -175,8 +175,16 @@ export class HolidayService {
throw new Error("No holiday country selected");
}
- const holidays = await this.getHolidaysForCountry(settings.countryCode);
- if (!holidays.some((h) => h.id === holidayId)) {
+ // Validate against both current and next year holidays (matching getHolidaysWithStatus)
+ const currentYear = dayjs().year();
+ const nextYear = currentYear + 1;
+ const [currentYearHolidays, nextYearHolidays] = await Promise.all([
+ this.getHolidaysForCountry(settings.countryCode, currentYear),
+ this.getHolidaysForCountry(settings.countryCode, nextYear),
+ ]);
+ const allHolidays = [...currentYearHolidays, ...nextYearHolidays];
+
+ if (!allHolidays.some((h) => h.id === holidayId)) {
throw new Error("Holiday not found for this country");
}
@@ -211,8 +219,8 @@ export class HolidayService {
}
const dateRanges = holidayDates.map((h) => ({
- start: dayjs(h.date).startOf("day").toDate(),
- end: dayjs(h.date).endOf("day").toDate(),
+ start: dayjs.utc(h.date).startOf("day").toDate(),
+ end: dayjs.utc(h.date).endOf("day").toDate(),
}));
const bookings = await HolidayRepository.findBookingsInDateRanges({ userId, dateRanges });
@@ -229,8 +237,8 @@ export class HolidayService {
const conflicts: HolidayConflict[] = [];
for (const holidayDate of holidayDates) {
- const holidayStart = dayjs(holidayDate.date).startOf("day").valueOf();
- const holidayEnd = dayjs(holidayDate.date).endOf("day").valueOf();
+ const holidayStart = dayjs.utc(holidayDate.date).startOf("day").valueOf();
+ const holidayEnd = dayjs.utc(holidayDate.date).endOf("day").valueOf();
const conflictingBookings = bookingsWithTimestamps.filter(
(booking) => booking.startTimestamp < holidayEnd && booking.endTimestamp > holidayStart
diff --git a/packages/lib/holidays/getHolidayEmoji.ts b/packages/lib/holidays/getHolidayEmoji.ts
new file mode 100644
index 0000000000..3fa487f625
--- /dev/null
+++ b/packages/lib/holidays/getHolidayEmoji.ts
@@ -0,0 +1,98 @@
+const HOLIDAY_EMOJI_MAP: Array<{ keywords: string[]; emoji: string }> = [
+ { keywords: ["christmas", "xmas", "noรซl", "navidad", "natal", "weihnacht"], emoji: "๐" },
+ { keywords: ["chinese new year", "lunar new year", "spring festival"], emoji: "๐" },
+ { keywords: ["new year", "aรฑo nuevo", "nouvel an", "neujahr", "capodanno"], emoji: "๐" },
+ { keywords: ["boxing day", "st. stephen", "saint stephen", "stephen's day"], emoji: "๐" },
+ { keywords: ["epiphany", "three kings", "reyes magos"], emoji: "๐" },
+ { keywords: ["easter", "pascua", "pรขques", "pasqua", "ostern"], emoji: "๐ฃ" },
+ { keywords: ["good friday", "viernes santo", "vendredi saint", "karfreitag"], emoji: "โ๏ธ" },
+ { keywords: ["ascension", "himmelfahrt", "ascensiรณn"], emoji: "โ๏ธ" },
+ { keywords: ["pentecost", "whit", "pfingsten", "pentecรดte"], emoji: "๐๏ธ" },
+ { keywords: ["assumption", "assomption", "asunciรณn", "mariรค himmelfahrt"], emoji: "โ๏ธ" },
+ { keywords: ["palm sunday"], emoji: "๐ฟ" },
+ { keywords: ["ash wednesday"], emoji: "โ๏ธ" },
+ { keywords: ["mother", "dรญa de la madre", "fรชte des mรจres", "muttertag"], emoji: "๐" },
+ { keywords: ["father", "dรญa del padre", "fรชte des pรจres", "vatertag"], emoji: "๐" },
+ { keywords: ["thanksgiving"], emoji: "๐ฆ" },
+ { keywords: ["halloween", "all saints", "todos los santos", "allerheiligen", "toussaint"], emoji: "๐" },
+ { keywords: ["valentine", "san valentรญn", "saint valentin", "valentinstag"], emoji: "๐" },
+ { keywords: ["martin luther king"], emoji: "โ" },
+ { keywords: ["presidents", "prรคsidenten"], emoji: "๐๏ธ" },
+ { keywords: ["juneteenth"], emoji: "โ" },
+ { keywords: ["columbus", "indigenous"], emoji: "๐" },
+ { keywords: ["independence", "independencia", "4th of july", "fourth of july", "july 4"], emoji: "๐" },
+ { keywords: ["bastille", "14 juillet", "quatorze juillet"], emoji: "๐" },
+ { keywords: ["guy fawkes", "bonfire night"], emoji: "๐" },
+ { keywords: ["national day", "dรญa nacional", "fรชte nationale", "nationalfeiertag"], emoji: "๐๏ธ" },
+ { keywords: ["republic day", "dรญa de la repรบblica", "tag der republik"], emoji: "๐๏ธ" },
+ { keywords: ["constitution", "constituciรณn", "verfassung"], emoji: "๐" },
+ { keywords: ["memorial", "remembrance", "veterans", "armistice", "volkstrauertag"], emoji: "๐๏ธ" },
+ { keywords: ["liberation", "liberaciรณn", "victory", "befreiung"], emoji: "๐๏ธ" },
+ { keywords: ["revolution", "revoluciรณn"], emoji: "โ" },
+ { keywords: ["unity", "unification", "einheit", "german unity"], emoji: "๐ค" },
+ { keywords: ["flag day", "dรญa de la bandera"], emoji: "๐ฉ" },
+ { keywords: ["labor", "labour", "workers", "trabajo", "travail", "may day", "tag der arbeit"], emoji: "๐ท" },
+ { keywords: ["diwali", "deepavali", "dipavali"], emoji: "๐ช" },
+ { keywords: ["holi"], emoji: "๐จ" },
+ { keywords: ["dussehra", "dasara", "vijayadashami"], emoji: "๐น" },
+ { keywords: ["ganesh", "ganesha", "vinayaka"], emoji: "๐" },
+ { keywords: ["navratri", "navaratri"], emoji: "๐" },
+ { keywords: ["raksha bandhan", "rakhi"], emoji: "๐" },
+ { keywords: ["janmashtami", "krishna"], emoji: "๐ช" },
+ { keywords: ["makar sankranti", "pongal", "lohri"], emoji: "๐ช" },
+ { keywords: ["ugadi", "gudi padwa"], emoji: "๐ธ" },
+ { keywords: ["onam"], emoji: "๐ถ" },
+ { keywords: ["eid al-fitr", "eid ul-fitr", "eid-ul-fitr"], emoji: "๐" },
+ { keywords: ["eid al-adha", "eid ul-adha", "eid-ul-adha", "bakrid"], emoji: "๐" },
+ { keywords: ["ramadan", "ramazan"], emoji: "๐" },
+ { keywords: ["muharram"], emoji: "๐" },
+ { keywords: ["milad", "mawlid", "prophet"], emoji: "๐" },
+ { keywords: ["hanukkah", "chanukah", "hanukka"], emoji: "๐" },
+ { keywords: ["passover", "pesach", "pessah"], emoji: "๐ท" },
+ { keywords: ["rosh hashanah", "rosh hashana"], emoji: "๐" },
+ { keywords: ["yom kippur"], emoji: "๐" },
+ { keywords: ["sukkot", "succot"], emoji: "๐ฟ" },
+ { keywords: ["purim"], emoji: "๐ญ" },
+ { keywords: ["shavuot"], emoji: "๐" },
+ { keywords: ["vesak", "buddha", "bodhi"], emoji: "๐ชท" },
+ { keywords: ["mid-autumn", "moon festival", "mooncake"], emoji: "๐ฅฎ" },
+ { keywords: ["dragon boat", "duanwu"], emoji: "๐ฒ" },
+ { keywords: ["qingming", "tomb sweeping"], emoji: "๐ชฆ" },
+ { keywords: ["golden week"], emoji: "๐ธ" },
+ { keywords: ["obon", "bon festival"], emoji: "๐ฎ" },
+ { keywords: ["children's day", "kodomo no hi"], emoji: "๐" },
+ { keywords: ["coming of age", "seijin no hi"], emoji: "๐" },
+ { keywords: ["carnival", "carnaval", "mardi gras"], emoji: "๐ญ" },
+ { keywords: ["st. patrick", "saint patrick", "san patricio"], emoji: "โ๏ธ" },
+ { keywords: ["oktoberfest"], emoji: "๐บ" },
+ { keywords: ["summer solstice", "midsummer"], emoji: "โ๏ธ" },
+ { keywords: ["winter solstice"], emoji: "โ๏ธ" },
+ { keywords: ["spring equinox"], emoji: "๐ธ" },
+ { keywords: ["autumn equinox", "fall equinox"], emoji: "๐" },
+ { keywords: ["bank holiday"], emoji: "๐ฆ" },
+ { keywords: ["public holiday"], emoji: "๐
" },
+ { keywords: ["school", "education"], emoji: "๐" },
+ { keywords: ["election", "voting"], emoji: "๐ณ๏ธ" },
+ { keywords: ["harvest", "cosecha"], emoji: "๐พ" },
+ { keywords: ["peace", "paz"], emoji: "๐๏ธ" },
+ { keywords: ["women", "mujer", "femme"], emoji: "๐ฉ" },
+ { keywords: ["youth", "juventud"], emoji: "๐ฆ" },
+ { keywords: ["king", "queen", "royal", "coronation"], emoji: "๐" },
+ { keywords: ["birthday", "cumpleaรฑos", "anniversaire"], emoji: "๐" },
+];
+
+const DEFAULT_HOLIDAY_EMOJI = "๐";
+
+export function getHolidayEmoji(holidayName: string): string {
+ const lowerName = holidayName.toLowerCase();
+
+ for (const { keywords, emoji } of HOLIDAY_EMOJI_MAP) {
+ if (keywords.some((kw) => lowerName.includes(kw))) {
+ return emoji;
+ }
+ }
+
+ return DEFAULT_HOLIDAY_EMOJI;
+}
+
+export { HOLIDAY_EMOJI_MAP, DEFAULT_HOLIDAY_EMOJI };
diff --git a/packages/platform/libraries/repositories.ts b/packages/platform/libraries/repositories.ts
index f1897e02bc..8da6dcf9db 100644
--- a/packages/platform/libraries/repositories.ts
+++ b/packages/platform/libraries/repositories.ts
@@ -1,5 +1,6 @@
export { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
export { PrismaOOORepository } from "@calcom/features/ooo/repositories/PrismaOOORepository";
+export { PrismaHolidayRepository } from "@calcom/features/holidays/repositories/PrismaHolidayRepository";
export { ScheduleRepository as PrismaScheduleRepository } from "@calcom/features/schedules/repositories/ScheduleRepository";
export { BookingRepository as PrismaBookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
export { EventTypeRepository as PrismaEventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";
diff --git a/packages/types/environment.d.ts b/packages/types/environment.d.ts
index 04b3eba0a4..392e60ce67 100644
--- a/packages/types/environment.d.ts
+++ b/packages/types/environment.d.ts
@@ -6,8 +6,6 @@ declare namespace NodeJS {
readonly GOOGLE_API_CREDENTIALS: string | undefined;
/** Google Calendar API key for fetching public holiday calendars */
readonly GOOGLE_CALENDAR_API_KEY: string | undefined;
- /** Number of days to cache holiday data from Google Calendar (default: 7) */
- readonly HOLIDAY_CACHE_DAYS: string | undefined;
/** @deprecated use `NEXT_PUBLIC_WEBAPP_URL` */
readonly BASE_URL: string | undefined;
/** @deprecated use `NEXT_PUBLIC_WEBAPP_URL` */