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:
co-authored by
Peer Richelsen
parent
3936a0ae94
commit
5af10a2f61
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user