feat: past and overlapping ooo (#17373)

* feat: past and overlapping ooo

* use existing props

* added e2e test

* disallow duplicates added tests

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
Amit Sharma
2024-11-24 18:58:22 +00:00
committed by GitHub
co-authored by Peer Richelsen
parent 3936a0ae94
commit 5af10a2f61
5 changed files with 131 additions and 54 deletions
+112 -1
View File
@@ -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();
}
@@ -162,6 +162,7 @@ export const CreateOrEditOutOfOfficeEntryModal = ({
control={control}
render={({ field: { onChange, value } }) => (
<DateRangePicker
minDate={null}
dates={{ startDate: value.startDate, endDate: value.endDate }}
onDatesChange={(values) => {
onChange(values);
@@ -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: {
@@ -21,7 +21,7 @@ function Calendar({
}: CalendarProps) {
return (
<DayPicker
fromDate={fromDate || new Date()}
fromDate={fromDate}
toDate={toDate || undefined}
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
@@ -14,7 +14,7 @@ type DatePickerWithRangeProps = {
dates: { startDate: Date; endDate?: Date };
onDatesChange: ({ startDate, endDate }: { startDate?: Date; endDate?: Date }) => 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 (
<div className={cn("grid gap-2", className)}>
@@ -63,7 +64,8 @@ export function DatePickerWithRange({
sideOffset={4}>
<Calendar
initialFocus
fromDate={minDate}
//When explicitly null, we want past dates to be shown as well, otherwise show only dates passed or from current date
fromDate={minDate === null ? undefined : fromDate}
toDate={maxDate}
mode="range"
defaultMonth={dates?.startDate}