diff --git a/apps/web/playwright/bookings-list.e2e.ts b/apps/web/playwright/bookings-list.e2e.ts index de5ccb55a3..52404b8718 100644 --- a/apps/web/playwright/bookings-list.e2e.ts +++ b/apps/web/playwright/bookings-list.e2e.ts @@ -1,9 +1,15 @@ import { expect } from "@playwright/test"; +import { randomString } from "@calcom/lib/random"; +import slugify from "@calcom/lib/slugify"; +import prisma from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/client"; +import { SchedulingType } from "@calcom/prisma/enums"; +import { createTeamEventType } from "./fixtures/users"; import type { Fixtures } from "./lib/fixtures"; import { test } from "./lib/fixtures"; +import { assertBookingVisibleFor, bookTeamEvent, bookUserEvent } from "./lib/testUtils"; test.afterEach(({ users }) => users.deleteAll()); @@ -61,6 +67,137 @@ test.describe("Bookings", () => { secondUpcomingBooking.locator(`text=${bookingWhereFirstUserIsOrganizer!.title}`) ).toBeVisible(); }); + test("collective eventType booking should be visible to team admin", async ({ page, users }) => { + const { owner1, owner2, commonUser, team1_teammate1 } = await createTeams(users); + + const { team: team1 } = await owner1.getFirstTeamMembership(); + const scenario = { + schedulingType: SchedulingType.COLLECTIVE, + teamEventTitle: `collective-team-event`, + teamEventSlug: slugify(`collective-team-event-${randomString(5)}`), + }; + + const eventType = await createTeamEventType(owner1, team1, scenario); + const { id: eventId, slug: evnetSlug } = eventType; + + await prisma.host.createMany({ + data: [ + { + userId: commonUser.id, + eventTypeId: eventId, + isFixed: true, + }, + { + userId: team1_teammate1.id, + eventTypeId: eventId, + isFixed: true, + }, + ], + }); + + // to test if booking is visible to team admin/owner even if he is not part of the booking + await prisma.host.deleteMany({ + where: { + userId: owner1.id, + eventTypeId: eventId, + }, + }); + + await bookTeamEvent(page, team1, eventType); + + // booking should be visible for the team host even though he is not part of the booking + await assertBookingVisibleFor(owner1, page, eventType, true); + // booking should not be visible for other team host + await assertBookingVisibleFor(owner2, page, eventType, false); + // booking should be visible for commonUser as he is part of the booking + await assertBookingVisibleFor(commonUser, page, eventType, true); + // booking should be visible for team1_teammate1 as he is part of the booking + await assertBookingVisibleFor(team1_teammate1, page, eventType, true); + }); + test("round-robin eventType booking should be visible to team admin", async ({ page, users }) => { + const { owner1, owner2, commonUser, team1_teammate1 } = await createTeams(users); + + const { team: team1 } = await owner1.getFirstTeamMembership(); + const scenario = { + schedulingType: SchedulingType.ROUND_ROBIN, + teamEventTitle: `round-robin-team-event`, + teamEventSlug: slugify(`round-robin-team-event-${randomString(5)}`), + }; + + const eventType = await createTeamEventType(owner1, team1, scenario); + const { id: eventId } = eventType; + + await prisma.host.createMany({ + data: [ + { + userId: commonUser.id, + eventTypeId: eventId, + }, + { + userId: team1_teammate1.id, + eventTypeId: eventId, + }, + ], + }); + + await prisma.host.deleteMany({ + where: { + userId: owner1.id, + eventTypeId: eventId, + }, + }); + + await bookTeamEvent(page, team1, eventType); + + // booking should be visible for the team host even though he is not part of the booking + await assertBookingVisibleFor(owner1, page, eventType, true); + // bookings should not be visible for other team host + await assertBookingVisibleFor(owner2, page, eventType, false); + }); + test("managed eventType booking should be visible to team admin", async ({ page, users }) => { + const { owner1, owner2, commonUser, team1_teammate1 } = await createTeams(users); + + const { team: team1 } = await owner1.getFirstTeamMembership(); + + await owner1.apiLogin(); + await page.goto(`/event-types?dialog=new&eventPage=team%2F${team1.slug}&teamId=${team1.id}`); + await page.getByTestId("managed-event-type").click(); + await page.getByTestId("event-type-quick-chat").click(); + await page.getByTestId("event-type-quick-chat").fill("managed-event"); + await page.getByRole("button", { name: "Continue" }).click(); + await page.getByTestId("vertical-tab-assignment").click(); + + await page.getByRole("combobox", { name: "assignment-dropdown" }).fill("commonUser"); + await page.getByRole("combobox", { name: "assignment-dropdown" }).press("Enter"); + await page.getByRole("combobox", { name: "assignment-dropdown" }).fill("team1_teammate1"); + await page.getByRole("combobox", { name: "assignment-dropdown" }).press("Enter"); + await page.getByTestId("update-eventtype").click(); + + const eventType = await owner1.getFirstTeamEvent(team1.id); + + await bookUserEvent(page, commonUser, eventType); + + // booking should be visible for the team host even though he is not part of the booking + await assertBookingVisibleFor(owner1, page, eventType, true); + // booking should not be visible for other team host + await assertBookingVisibleFor(owner2, page, eventType, false); + // booking should be visible for commonUser as he is part of the booking + await assertBookingVisibleFor(commonUser, page, eventType, true); + // booking should not be visible for team1_teammate1 as we booked for commonUser + await assertBookingVisibleFor(team1_teammate1, page, eventType, false); + }); + test("individual team members booking should not be visible to team admin", async ({ page, users }) => { + const { owner1, owner2, commonUser, team1_teammate1 } = await createTeams(users); + + const eventType = await commonUser.getFirstEventAsOwner(); + await bookUserEvent(page, commonUser, eventType); + // non-team bookings should not be visible for the team host + await assertBookingVisibleFor(owner1, page, eventType, false); + // non-team bookings should not be visible for other team host + await assertBookingVisibleFor(owner2, page, eventType, false); + // booking should be visible for commonUser as he is part of the booking + await assertBookingVisibleFor(commonUser, page, eventType, true); + }); }); }); @@ -111,3 +248,75 @@ async function createBooking({ }, }); } + +const createTeams = async (userFixture: Fixtures["users"]) => { + const owner1 = await userFixture.create({ name: "team-owner-1" }); + const owner2 = await userFixture.create({ name: "team-owner-2" }); + const commonUser = await userFixture.create({ name: "commonUser" }); + const team1_teammate1 = await userFixture.create({ name: "team1_teammate1" }); + const team2_teammate1 = await userFixture.create({ name: "team2_teammate1" }); + const teamOne = await prisma.team.create({ + data: { + name: "bookings-test-team-1", + slug: slugify(`bookings-test-team-2-${randomString(5)}`), + }, + }); + + const teamTwo = await prisma.team.create({ + data: { + name: "bookings-test-team-2", + slug: slugify(`bookings-test-team-2-${randomString(5)}`), + }, + }); + + // create memberships + await prisma.membership.createMany({ + data: [ + { + userId: owner1.id, + teamId: teamOne.id, + accepted: true, + role: "OWNER", + }, + { + userId: commonUser.id, + teamId: teamOne.id, + accepted: true, + role: "MEMBER", + }, + { + userId: team1_teammate1.id, + teamId: teamOne.id, + accepted: true, + role: "MEMBER", + }, + { + userId: owner2.id, + teamId: teamTwo.id, + accepted: true, + role: "OWNER", + }, + { + userId: commonUser.id, + teamId: teamTwo.id, + accepted: true, + role: "MEMBER", + }, + { + userId: team2_teammate1.id, + teamId: teamTwo.id, + accepted: true, + role: "MEMBER", + }, + ], + }); + return { + owner1, + owner2, + commonUser, + team1_teammate1, + team2_teammate1, + teamOne, + teamTwo, + }; +}; diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index faa18a20ed..1089e1d6df 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -58,7 +58,7 @@ const createTeamWorkflow = async (user: { id: number }, team: { id: number }) => }); }; -const createTeamEventType = async ( +export const createTeamEventType = async ( user: { id: number }, team: { id: number }, scenario?: { diff --git a/apps/web/playwright/lib/testUtils.ts b/apps/web/playwright/lib/testUtils.ts index 4c2440aa48..256ecad04c 100644 --- a/apps/web/playwright/lib/testUtils.ts +++ b/apps/web/playwright/lib/testUtils.ts @@ -1,4 +1,5 @@ import type { Frame, Page } from "@playwright/test"; +import type { APIResponse } from "@playwright/test"; import { expect } from "@playwright/test"; import { createHash } from "crypto"; import EventEmitter from "events"; @@ -369,3 +370,53 @@ export async function doOnOrgDomain( // When App directory is there, this is the 404 page text. We should work on fixing the 404 page as it changed due to app directory. export const NotFoundPageTextAppDir = "This page does not exist."; // export const NotFoundPageText = "ERROR 404"; + +export type Team = { + id: number; + slug: string | null; + name: string; +}; + +export type EventType = { + id: number; + title: string; + slug: string; +}; +export type User = { + id: number; + name: string | null; + username: string | null; +}; + +export async function bookTeamEvent(page: Page, team: Team, eventType: EventType) { + await page.goto(`/team/${team.slug}/${eventType.slug}/`); + await bookEventOnThisPage(page); + const bookingTitle = `${eventType.title} between ${team.name} and ${testName}`; + await assertBookingIsCorrect(page, bookingTitle); +} +export async function bookUserEvent(page: Page, user: User, eventType: EventType) { + await page.goto(`/${user.username}/${eventType.slug}/`); + await bookEventOnThisPage(page); + const bookingTitle = `${eventType.title} between ${user.name} and ${testName}`; + await assertBookingIsCorrect(page, bookingTitle); +} + +export async function assertBookingIsCorrect(page: Page, bookingTitle: string) { + await expect(page.locator("[data-testid=booking-title]")).toHaveText(bookingTitle); + // The booker should be in the attendee list + await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName); +} + +export const assertBookingVisibleFor = async ( + user: { apiLogin: () => Promise }, + pageFixture: Fixtures["page"], + eventType: EventType, + shouldBeVisible: boolean +) => { + await user.apiLogin(); + await pageFixture.goto(`/bookings/upcoming`); + const upcomingBookingList = pageFixture.locator('[data-testid="booking-item"]'); + shouldBeVisible + ? await expect(upcomingBookingList.locator(`text=${eventType.title}`)).toBeVisible() + : await expect(upcomingBookingList.locator(`text=${eventType.title}`)).toBeHidden(); +}; diff --git a/apps/web/playwright/organization/booking-list.e2e.ts b/apps/web/playwright/organization/booking-list.e2e.ts new file mode 100644 index 0000000000..05cefbe0a3 --- /dev/null +++ b/apps/web/playwright/organization/booking-list.e2e.ts @@ -0,0 +1,391 @@ +import type { Page } from "@playwright/test"; +import { expect } from "@playwright/test"; + +import { randomString } from "@calcom/lib/random"; +import slugify from "@calcom/lib/slugify"; +import prisma from "@calcom/prisma"; +import { MembershipRole, SchedulingType } from "@calcom/prisma/enums"; + +import { createTeamEventType } from "../fixtures/users"; +import { test } from "../lib/fixtures"; +import type { Fixtures } from "../lib/fixtures"; +import { + assertBookingVisibleFor, + bookTimeSlot, + doOnOrgDomain, + NotFoundPageTextAppDir, + selectFirstAvailableTimeSlotNextMonth, + testName, +} from "../lib/testUtils"; + +test.describe("Bookings list for organizations", () => { + test.afterEach(({ orgs, users }) => { + orgs.deleteAll(); + users.deleteAll(); + }); + + test.describe("Team Event", () => { + test("Collective EventType booking list", async ({ page, users, orgs }) => { + const { org, orgOwner, orgAdmin, team1owner, team2owner, commonUser, team1_teammate1 } = + await createOrg(orgs, users); + + const { team: team1 } = await team1owner.getFirstTeamMembership(); + const scenario = { + schedulingType: SchedulingType.COLLECTIVE, + teamEventTitle: `collective-team-event`, + teamEventSlug: slugify(`collective-team-event-${randomString(5)}`), + }; + const eventType = await createTeamEventType(team1owner, team1, scenario); + const { id: eventId, slug: evnetSlug } = eventType; + + await prisma.host.createMany({ + data: [ + { + userId: commonUser.id, + eventTypeId: eventId, + isFixed: true, + }, + { + userId: team1_teammate1.id, + eventTypeId: eventId, + isFixed: true, + }, + ], + }); + + // to test if booking is visible to team admin/owner even if he is not part of the booking + await prisma.host.deleteMany({ + where: { + userId: team1owner.id, + eventTypeId: eventId, + }, + }); + + await expectPageToBeNotFound({ page, url: `/team/${team1.slug}/${eventType.slug}` }); + await doOnOrgDomain( + { + orgSlug: org.slug, + page, + }, + async () => { + await bookTeamEvent({ page, team: team1, event: eventType }); + } + ); + // booking should be visible for the Org OWNER even though he is not part of the booking + await assertBookingVisibleFor(orgOwner, page, eventType, true); + // booking should be visible for the Org ADMIN even though he is not part of the booking + await assertBookingVisibleFor(orgAdmin, page, eventType, true); + // booking should be visible for the team host even though he is not part of the booking + await assertBookingVisibleFor(team1owner, page, eventType, true); + // booking should not be visible for other team host, because its a team1 team-event + await assertBookingVisibleFor(team2owner, page, eventType, false); + // booking should be visible for commonUser as he is part of the booking + await assertBookingVisibleFor(commonUser, page, eventType, true); + // booking should be visible for team1_teammate1 as he is part of the booking + await assertBookingVisibleFor(team1_teammate1, page, eventType, true); + }); + + test("Round Robin EventType booking list", async ({ page, users, orgs }) => { + const { org, orgOwner, orgAdmin, team1owner, team2owner, commonUser, team1_teammate1 } = + await createOrg(orgs, users); + + const { team: team1 } = await team1owner.getFirstTeamMembership(); + const scenario = { + schedulingType: SchedulingType.ROUND_ROBIN, + teamEventTitle: `round-robin-team-event`, + teamEventSlug: slugify(`round-robin-team-event-${randomString(5)}`), + }; + + const eventType = await createTeamEventType(team1owner, team1, scenario); + const { id: eventId } = eventType; + await prisma.host.createMany({ + data: [ + { + userId: commonUser.id, + eventTypeId: eventId, + }, + { + userId: team1_teammate1.id, + eventTypeId: eventId, + }, + ], + }); + + await prisma.host.deleteMany({ + where: { + userId: team1owner.id, + eventTypeId: eventId, + }, + }); + + await expectPageToBeNotFound({ page, url: `/team/${team1.slug}/${eventType.slug}` }); + + await doOnOrgDomain( + { + orgSlug: org.slug, + page, + }, + async () => { + await bookTeamEvent({ page, team: team1, event: eventType }); + } + ); + // booking should be visible for the Org OWNER even though he is not part of the booking + await assertBookingVisibleFor(orgOwner, page, eventType, true); + // booking should be visible for the Org ADMIN even though he is not part of the booking + await assertBookingVisibleFor(orgAdmin, page, eventType, true); + // booking should be visible for the team host even though he is not part of the booking + await assertBookingVisibleFor(team1owner, page, eventType, true); + // booking should not be visible for other team host + await assertBookingVisibleFor(team2owner, page, eventType, false); + }); + + test("Managed EventType booking list", async ({ page, users, orgs }) => { + const { org, orgOwner, orgAdmin, team1owner, team2owner, commonUser, team1_teammate1 } = + await createOrg(orgs, users); + + const { team: team1 } = await team1owner.getFirstTeamMembership(); + + await team1owner.apiLogin(); + await page.goto(`/event-types?dialog=new&eventPage=team%2F${team1.slug}&teamId=${team1.id}`); + await page.getByTestId("managed-event-type").click(); + await page.getByTestId("event-type-quick-chat").click(); + await page.getByTestId("event-type-quick-chat").fill("managed-event"); + await page.getByRole("button", { name: "Continue" }).click(); + await page.getByTestId("vertical-tab-assignment").click(); + + await page.getByRole("combobox", { name: "assignment-dropdown" }).fill("commonUser"); + await page.getByRole("combobox", { name: "assignment-dropdown" }).press("Enter"); + await page.getByRole("combobox", { name: "assignment-dropdown" }).fill("team1_teammate1"); + await page.getByRole("combobox", { name: "assignment-dropdown" }).press("Enter"); + await page.getByTestId("update-eventtype").click(); + + const eventType = await team1owner.getFirstTeamEvent(team1.id); + + await expectPageToBeNotFound({ page, url: `/${commonUser.username}/${eventType.slug}` }); + + await doOnOrgDomain( + { + orgSlug: org.slug, + page, + }, + async () => { + await bookUserEvent({ page, user: commonUser, event: eventType }); + } + ); + + // booking should be visible for the Org OWNER even though he is not part of the booking + await assertBookingVisibleFor(orgOwner, page, eventType, true); + // booking should be visible for the Org ADMIN even though he is not part of the booking + await assertBookingVisibleFor(orgAdmin, page, eventType, true); + // booking should be visible for the team host even though he is not part of the booking + await assertBookingVisibleFor(team1owner, page, eventType, true); + // booking should not be visible for other team host + await assertBookingVisibleFor(team2owner, page, eventType, false); + }); + }); + + test.describe("User Event", () => { + test("User personal booking list", async ({ page, users, orgs }) => { + const { org, orgOwner, orgAdmin, team1owner, team2owner, commonUser, team1_teammate1 } = + await createOrg(orgs, users); + + const event = await commonUser.getFirstEventAsOwner(); + + await page.goto(`/${commonUser.username}/${event.slug}`); + // Shouldn't be servable on the non-org domain + await expect(page.locator(`text=${NotFoundPageTextAppDir}`)).toBeVisible(); + + await doOnOrgDomain( + { + orgSlug: org.slug, + page, + }, + async () => { + await bookUserEvent({ page, user: commonUser, event }); + } + ); + // booking should be visible for the Org OWNER + await assertBookingVisibleFor(orgOwner, page, event, true); + // booking should be visible for the Org ADMIN + await assertBookingVisibleFor(orgAdmin, page, event, true); + // booking should be visible for the team host + await assertBookingVisibleFor(team1owner, page, event, true); + // booking should be visible for other team host because commonUser is part of both teams + await assertBookingVisibleFor(team2owner, page, event, true); + // booking should be visible for commonUser as he is part of the booking + await assertBookingVisibleFor(commonUser, page, event, true); + // booking should not be visible for team1_teammate1 + await assertBookingVisibleFor(team1_teammate1, page, event, false); + }); + }); +}); + +async function bookUserEvent({ + page, + user, + event, +}: { + page: Page; + user: { + username: string | null; + name: string | null; + }; + event: { slug: string; title: string }; +}) { + await page.goto(`/${user.username}/${event.slug}`); + + await selectFirstAvailableTimeSlotNextMonth(page); + await bookTimeSlot(page); + await expect(page.getByTestId("success-page")).toBeVisible(); + + // The title of the booking + const BookingTitle = `${event.title} between ${user.name} and ${testName}`; + await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle); + // The booker should be in the attendee list + await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName); +} + +async function bookTeamEvent({ + page, + team, + event, +}: { + page: Page; + team: { + slug: string | null; + name: string | null; + }; + event: { slug: string; title: string }; +}) { + // Note that even though the default way to access a team booking in an organization is to not use /team in the URL, but it isn't testable with playwright as the rewrite is taken care of by Next.js config which can't handle on the fly org slug's handling + // So, we are using /team in the URL to access the team booking + // There are separate tests to verify that the next.config.js rewrites are working + // Also there are additional checkly tests that verify absolute e2e flow. They are in __checks__/organization.spec.ts + await page.goto(`/team/${team.slug}/${event.slug}`); + + await selectFirstAvailableTimeSlotNextMonth(page); + await bookTimeSlot(page); + await expect(page.getByTestId("success-page")).toBeVisible(); + + // The title of the booking + const BookingTitle = `${event.title} between ${team.name} and ${testName}`; + await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle); + // The booker should be in the attendee list + await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName); +} + +async function expectPageToBeNotFound({ page, url }: { page: Page; url: string }) { + await page.goto(`${url}`); + await expect(page.locator(`text=${NotFoundPageTextAppDir}`)).toBeVisible(); +} + +const createOrg = async (orgs: Fixtures["orgs"], userFixture: Fixtures["users"]) => { + const org = await orgs.create({ + name: "TestOrg", + }); + const orgOwner = await userFixture.create({ + name: "org-owner", + organizationId: org.id, + roleInOrganization: MembershipRole.OWNER, + }); + + const orgAdmin = await userFixture.create({ + name: "org-admin", + organizationId: org.id, + roleInOrganization: MembershipRole.ADMIN, + }); + const team1owner = await userFixture.create({ + name: "team-owner-1", + organizationId: org.id, + roleInOrganization: MembershipRole.MEMBER, + }); + const team2owner = await userFixture.create({ + name: "team-owner-2", + organizationId: org.id, + roleInOrganization: MembershipRole.MEMBER, + }); + const commonUser = await userFixture.create({ + name: "commonUser", + organizationId: org.id, + roleInOrganization: MembershipRole.MEMBER, + }); + const team1_teammate1 = await userFixture.create({ + name: "team1_teammate1", + organizationId: org.id, + roleInOrganization: MembershipRole.MEMBER, + }); + const team2_teammate1 = await userFixture.create({ + name: "team2_teammate1", + organizationId: org.id, + roleInOrganization: MembershipRole.MEMBER, + }); + const teamOne = await prisma.team.create({ + data: { + name: "bookings-test-team-1", + slug: slugify(`bookings-test-team-2-${randomString(5)}`), + parentId: org.id, + }, + }); + + const teamTwo = await prisma.team.create({ + data: { + name: "bookings-test-team-2", + slug: slugify(`bookings-test-team-2-${randomString(5)}`), + parentId: org.id, + }, + }); + + // create memberships + await prisma.membership.createMany({ + data: [ + { + userId: team1owner.id, + teamId: teamOne.id, + accepted: true, + role: "OWNER", + }, + { + userId: commonUser.id, + teamId: teamOne.id, + accepted: true, + role: "MEMBER", + }, + { + userId: team1_teammate1.id, + teamId: teamOne.id, + accepted: true, + role: "MEMBER", + }, + { + userId: team2owner.id, + teamId: teamTwo.id, + accepted: true, + role: "OWNER", + }, + { + userId: commonUser.id, + teamId: teamTwo.id, + accepted: true, + role: "MEMBER", + }, + { + userId: team2_teammate1.id, + teamId: teamTwo.id, + accepted: true, + role: "MEMBER", + }, + ], + }); + return { + org, + orgOwner, + orgAdmin, + team1owner, + team2owner, + commonUser, + team1_teammate1, + team2_teammate1, + teamOne, + teamTwo, + }; +}; diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index 6f522ab22d..c0ef454383 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -189,7 +189,6 @@ async function getBookings({ ], }, }; - const filtersCombined: Prisma.BookingWhereInput[] = !filters ? [] : Object.keys(filters) @@ -262,7 +261,8 @@ async function getBookings({ // Note that because we are applying `take` to individual queries, we will usually get more bookings then we need. It is okay to have more bookings faster than having what we need slower bookingsQueryUserId, bookingsQueryAttendees, - bookingsQueryTeamMember, + bookingsQueryTeamEvents, + bookingsQueryTeamUsersPersonalEvents, bookingsQuerySeatReference, ////////////////////////// @@ -300,9 +300,28 @@ async function getBookings({ take: take + 1, skip, }), + // get team bookings (managed + collective + round-robin) prisma.booking.findMany({ where: { OR: [ + // all (collective + round-robin) team bookings should be visible to org-owner and org-admins + { + eventType: { + team: { + parent: { + members: { + some: { + userId: user.id, + role: { + in: ["ADMIN", "OWNER"], + }, + }, + }, + }, + }, + }, + }, + // all (collective + round-robin) team bookings should be visible to team-owner and team-admins { eventType: { team: { @@ -317,6 +336,43 @@ async function getBookings({ }, }, }, + + // all managed team bookings should be visible to team-owner and team-admins + { + eventType: { + parent: { + team: { + members: { + some: { + userId: user.id, + role: { + in: ["ADMIN", "OWNER"], + }, + }, + }, + }, + }, + }, + }, + // all managed team bookings should be visible to org-owner and org-admins + { + eventType: { + parent: { + team: { + parent: { + members: { + some: { + userId: user.id, + role: { + in: ["ADMIN", "OWNER"], + }, + }, + }, + }, + }, + }, + }, + }, ], AND: [passedBookingsStatusFilter, ...filtersCombined], }, @@ -324,6 +380,84 @@ async function getBookings({ take: take + 1, skip, }), + // only get personal bookings + prisma.booking.findMany({ + where: { + AND: [ + // only show personal booking to org-owner and org-admins + { + user: { + teams: { + some: { + team: { + parentId: { gt: 0 }, + }, + }, + }, + }, + }, + { + OR: [ + { + user: { + teams: { + some: { + team: { + parent: { + members: { + some: { + userId: user.id, + role: { + in: ["ADMIN", "OWNER"], + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + user: { + teams: { + some: { + team: { + members: { + some: { + userId: user.id, + role: { + in: ["ADMIN", "OWNER"], + }, + }, + }, + }, + }, + }, + }, + }, + ], + }, + // filter out all (collective + round-robin) event bookings + { + eventType: { + teamId: null, + }, + }, + // filter out all (managed) event bookings + { + eventType: { + parentId: null, + }, + }, + passedBookingsStatusFilter, + ...filtersCombined, + ], + }, + orderBy, + take: take + 1, + skip, + }), prisma.booking.findMany({ where: { OR: [ @@ -407,7 +541,8 @@ async function getBookings({ // It's going to mess up the orderBy as we are concatenating independent queries results bookingsQueryUserId .concat(bookingsQueryAttendees) - .concat(bookingsQueryTeamMember) + .concat(bookingsQueryTeamEvents) + .concat(bookingsQueryTeamUsersPersonalEvents) .concat(bookingsQuerySeatReference) );