diff --git a/apps/web/playwright/out-of-office.e2e.ts b/apps/web/playwright/out-of-office.e2e.ts index 41bfe24554..c3154308e1 100644 --- a/apps/web/playwright/out-of-office.e2e.ts +++ b/apps/web/playwright/out-of-office.e2e.ts @@ -197,10 +197,121 @@ test.describe("Out of office", () => { await expect(page.getByTestId("away-emoji")).toBeTruthy(); }); + + test("User can create Entry for past", async ({ page, users }) => { + const user = await users.create({ name: "userOne" }); + + await user.apiLogin(); + + await page.goto("/settings/my-account/out-of-office"); + + await page.getByTestId("add_entry_ooo").click(); + + await page.locator('[id="date"]').click(); + + await selectToAndFromDates(page, "13", "22", true); + + // send request + await saveAndWaitForResponse(page); + + const ooo = await prisma.outOfOfficeEntry.findMany({ + where: { + userId: user.id, + }, + orderBy: { + createdAt: "desc", + }, + select: { + start: true, + end: true, + }, + take: 1, + }); + + const latestEntry = ooo[0]; + + const currentDate = dayjs(); + const fromDate = dayjs(latestEntry.start); + const toDate = dayjs(latestEntry.end); + + expect(toDate.isBefore(currentDate)).toBe(true); + expect(fromDate.isBefore(currentDate)).toBe(true); + }); + + test("User can create overriding entries", async ({ page, users }) => { + const user = await users.create({ name: "userOne" }); + + await user.apiLogin(); + + await page.goto("/settings/my-account/out-of-office"); + + await page.getByTestId("add_entry_ooo").click(); + + await page.locator('[id="date"]').click(); + + await selectToAndFromDates(page, "13", "22"); + + // send request + await saveAndWaitForResponse(page); + await expect(page.locator(`data-testid=table-redirect-n-a`)).toBeVisible(); + + // add another entry + await page.getByTestId("add_entry_ooo").click(); + + await page.locator('[id="date"]').click(); + + await selectToAndFromDates(page, "11", "24"); + + // send request + await saveAndWaitForResponse(page); + + await expect(page.locator(`data-testid=table-redirect-n-a`)).toHaveCount(2); + }); + + test("User cannot create duplicate entries", async ({ page, users }) => { + const user = await users.create({ name: "userOne" }); + + await user.apiLogin(); + + await page.goto("/settings/my-account/out-of-office"); + + await page.getByTestId("add_entry_ooo").click(); + + await page.locator('[id="date"]').click(); + + await selectToAndFromDates(page, "13", "22"); + + // send request + await saveAndWaitForResponse(page); + await expect(page.locator(`data-testid=table-redirect-n-a`)).toBeVisible(); + + // add another entry + await page.getByTestId("add_entry_ooo").click(); + + await page.locator('[id="date"]').click(); + + await selectToAndFromDates(page, "13", "22"); + + // send request + await saveAndWaitForResponse(page, 409); + }); }); -async function saveAndWaitForResponse(page: Page) { +async function saveAndWaitForResponse(page: Page, expectedStatusCode = 200) { await submitAndWaitForResponse(page, "/api/trpc/viewer/outOfOfficeCreateOrUpdate?batch=1", { action: () => page.getByTestId("create-or-edit-entry-ooo-redirect").click(), + expectedStatusCode, }); } + +async function selectToAndFromDates(page: Page, fromDate: string, toDate: string, isRangeInPast = false) { + // deselects by default selected range start day + await page.locator(`button[name="day"].rdp-day_range_start`).click(); + + const month = isRangeInPast ? "previous" : "next"; + + await page.locator(`button[name="${month}-month"]`).click(); + + await page.locator(`button[name="day"]:has-text("${fromDate}")`).nth(0).click(); + await page.locator(`button[name="day"]:has-text("${toDate}")`).nth(0).click(); +} diff --git a/packages/features/settings/outOfOffice/CreateOrEditOutOfOfficeModal.tsx b/packages/features/settings/outOfOffice/CreateOrEditOutOfOfficeModal.tsx index 843856a19d..874375b891 100644 --- a/packages/features/settings/outOfOffice/CreateOrEditOutOfOfficeModal.tsx +++ b/packages/features/settings/outOfOffice/CreateOrEditOutOfOfficeModal.tsx @@ -162,6 +162,7 @@ export const CreateOrEditOutOfOfficeEntryModal = ({ control={control} render={({ field: { onChange, value } }) => ( { onChange(values); diff --git a/packages/trpc/server/routers/loggedInViewer/outOfOffice.handler.ts b/packages/trpc/server/routers/loggedInViewer/outOfOffice.handler.ts index 992c3dea58..683f27ee89 100644 --- a/packages/trpc/server/routers/loggedInViewer/outOfOffice.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/outOfOffice.handler.ts @@ -38,11 +38,6 @@ export const outOfOfficeCreateOrUpdate = async ({ ctx, input }: TBookingRedirect throw new TRPCError({ code: "BAD_REQUEST", message: "start_date_must_be_before_end_date" }); } - // If start date is before to today throw error - if (inputStartTime.isBefore(dayjs().startOf("day").subtract(1, "day"))) { - throw new TRPCError({ code: "BAD_REQUEST", message: "start_date_must_be_in_the_future" }); - } - let toUserId: number | null = null; if (input.toTeamUserId) { @@ -73,51 +68,6 @@ export const outOfOfficeCreateOrUpdate = async ({ ctx, input }: TBookingRedirect toUserId = user?.id; } - // Validate if OOO entry for these dates already exists - const outOfOfficeEntry = await prisma.outOfOfficeEntry.findFirst({ - where: { - AND: [ - { userId: ctx.user.id }, - { - uuid: { - not: input.uuid ?? "", - }, - }, - { - OR: [ - { - start: { - lt: inputEndTime.toISOString(), //existing start is less than or equal to input end time - }, - end: { - gt: inputStartTime.toISOString(), //existing end is greater than or equal to input start time - }, - }, - { - //existing start is within the new input range - start: { - gt: inputStartTime.toISOString(), - lt: inputEndTime.toISOString(), - }, - }, - { - //existing end is within the new input range - end: { - gt: inputStartTime.toISOString(), - lt: inputEndTime.toISOString(), - }, - }, - ], - }, - ], - }, - }); - - // don't allow overlapping entries - if (outOfOfficeEntry) { - throw new TRPCError({ code: "CONFLICT", message: "out_of_office_entry_already_exists" }); - } - if (!input.reasonId) { throw new TRPCError({ code: "BAD_REQUEST", message: "reason_id_required" }); } @@ -155,9 +105,22 @@ export const outOfOfficeCreateOrUpdate = async ({ ctx, input }: TBookingRedirect if (existingOutOfOfficeEntry) { throw new TRPCError({ code: "BAD_REQUEST", message: "booking_redirect_infinite_not_allowed" }); } + const startTimeUtc = dayjs.utc(startDate).add(input.offset, "minute").startOf("day").toISOString(); const endTimeUtc = dayjs.utc(endDate).add(input.offset, "minute").endOf("day").toISOString(); + const isDuplicateOutOfOfficeEntry = await prisma.outOfOfficeEntry.findFirst({ + where: { + userId: ctx.user.id, + start: startTimeUtc, + end: endTimeUtc, + }, + }); + + if (isDuplicateOutOfOfficeEntry) { + throw new TRPCError({ code: "CONFLICT", message: "out_of_office_entry_already_exists" }); + } + // Get the existing redirected user from existing out of office entry to send that user appropriate email. const previousOutOfOfficeEntry = await prisma.outOfOfficeEntry.findUnique({ where: { diff --git a/packages/ui/components/form/date-range-picker/Calendar.tsx b/packages/ui/components/form/date-range-picker/Calendar.tsx index 7c646692a8..cf9be58363 100644 --- a/packages/ui/components/form/date-range-picker/Calendar.tsx +++ b/packages/ui/components/form/date-range-picker/Calendar.tsx @@ -21,7 +21,7 @@ function Calendar({ }: CalendarProps) { return ( void; disabled?: boolean; - minDate?: Date; + minDate?: Date | null; maxDate?: Date; }; @@ -34,6 +34,7 @@ export function DatePickerWithRange({ onDatesChange({ startDate: onChangeValues?.from, endDate: onChangeValues?.to }); } } + const fromDate = minDate ?? new Date(); return (
@@ -63,7 +64,8 @@ export function DatePickerWithRange({ sideOffset={4}>