diff --git a/apps/api/v2/src/modules/atoms/atoms.module.ts b/apps/api/v2/src/modules/atoms/atoms.module.ts index ef8c41baa8..26c97f16c4 100644 --- a/apps/api/v2/src/modules/atoms/atoms.module.ts +++ b/apps/api/v2/src/modules/atoms/atoms.module.ts @@ -3,13 +3,14 @@ import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06 import { AtomsController } from "@/modules/atoms/controllers/atoms.controller"; import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; +import { OrganizationsModule } from "@/modules/organizations/organizations.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { UsersService } from "@/modules/users/services/users.service"; import { UsersRepository } from "@/modules/users/users.repository"; import { Module } from "@nestjs/common"; @Module({ - imports: [PrismaModule, EventTypesModule_2024_06_14], + imports: [PrismaModule, EventTypesModule_2024_06_14, OrganizationsModule], providers: [ EventTypesAtomService, MembershipsRepository, diff --git a/apps/api/v2/src/modules/atoms/controllers/atoms.controller.ts b/apps/api/v2/src/modules/atoms/controllers/atoms.controller.ts index 55f49fc2ee..6dc0a29306 100644 --- a/apps/api/v2/src/modules/atoms/controllers/atoms.controller.ts +++ b/apps/api/v2/src/modules/atoms/controllers/atoms.controller.ts @@ -64,4 +64,20 @@ export class AtomsController { data: eventType, }; } + + @Patch("/organizations/:organizationId/teams/:teamId/event-types/:eventTypeId") + @Version(VERSION_NEUTRAL) + @UseGuards(ApiAuthGuard) + async updateAtomTeamEventType( + @GetUser() user: UserWithProfile, + @Param("eventTypeId", ParseIntPipe) eventTypeId: number, + @Param("teamId", ParseIntPipe) teamId: number, + @Body() body: UpdateEventTypeReturn + ): Promise> { + const eventType = await this.eventTypesService.updateTeamEventType(eventTypeId, body, user, teamId); + return { + status: SUCCESS_STATUS, + data: eventType, + }; + } } diff --git a/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts b/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts index 1abd9657d3..1e93278406 100644 --- a/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts +++ b/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts @@ -1,10 +1,11 @@ import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; +import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { UsersService } from "@/modules/users/services/users.service"; import { UserWithProfile } from "@/modules/users/users.repository"; -import { Injectable, NotFoundException } from "@nestjs/common"; +import { Injectable, NotFoundException, ForbiddenException } from "@nestjs/common"; import { updateEventType, @@ -21,11 +22,11 @@ export class EventTypesAtomService { private readonly usersService: UsersService, private readonly dbWrite: PrismaWriteService, private readonly dbRead: PrismaReadService, - private readonly eventTypeService: EventTypesService_2024_06_14 + private readonly eventTypeService: EventTypesService_2024_06_14, + private readonly teamEventTypeService: OrganizationsEventTypesService ) {} async getUserEventType(user: UserWithProfile, eventTypeId: number) { - this.eventTypeService.checkUserOwnsEventType(user.id, { id: eventTypeId, userId: user.id }); const organizationId = this.usersService.getUserMainOrgId(user); const isUserOrganizationAdmin = organizationId @@ -45,12 +46,22 @@ export class EventTypesAtomService { throw new NotFoundException(`Event type with id ${eventTypeId} not found`); } - this.eventTypeService.checkUserOwnsEventType(user.id, eventType.eventType); + if (eventType?.team?.id) { + await this.checkTeamOwnsEventType(user.id, eventType.eventType.id, eventType.team.id); + } else { + this.eventTypeService.checkUserOwnsEventType(user.id, eventType.eventType); + } + return eventType; } - async updateEventType(eventTypeId: number, body: TUpdateEventTypeInputSchema, user: UserWithProfile) { - this.eventTypeService.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId); + async updateTeamEventType( + eventTypeId: number, + body: TUpdateEventTypeInputSchema, + user: UserWithProfile, + teamId: number + ) { + await this.checkCanUpdateTeamEventType(user.id, eventTypeId, teamId, body.scheduleId); const eventTypeUser = await this.eventTypeService.getUserToUpdateEvent(user); const bookingFields = [...(body.bookingFields || [])]; @@ -77,4 +88,62 @@ export class EventTypesAtomService { return eventType.eventType; } + + async updateEventType(eventTypeId: number, body: TUpdateEventTypeInputSchema, user: UserWithProfile) { + await this.eventTypeService.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId); + const eventTypeUser = await this.eventTypeService.getUserToUpdateEvent(user); + const bookingFields = [...(body.bookingFields || [])]; + + if ( + !bookingFields.find((field) => field.type === "email") && + !bookingFields.find((field) => field.type === "phone") + ) { + bookingFields.push(systemBeforeFieldEmail); + } + + const eventType = await updateEventType({ + input: { id: eventTypeId, ...body, bookingFields }, + ctx: { + user: eventTypeUser, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + prisma: this.dbWrite.prisma, + }, + }); + + if (!eventType) { + throw new NotFoundException(`Event type with id ${eventTypeId} not found`); + } + + return eventType.eventType; + } + + async checkCanUpdateTeamEventType(userId: number, eventTypeId: number, teamId: number, scheduleId: number) { + await this.checkTeamOwnsEventType(userId, eventTypeId, teamId); + await this.teamEventTypeService.validateEventTypeExists(teamId, eventTypeId); + await this.eventTypeService.checkUserOwnsSchedule(userId, scheduleId); + } + + async checkTeamOwnsEventType(userId: number, eventTypeId: number, teamId: number) { + const membership = await this.dbRead.prisma.membership.findFirst({ + where: { + userId, + teamId, + accepted: true, + OR: [{ role: "ADMIN" }, { role: "OWNER" }], + }, + select: { + team: { + select: { + eventTypes: true, + }, + }, + }, + }); + if (!membership?.team?.eventTypes?.some((item) => item.id === eventTypeId)) { + throw new ForbiddenException( + `Access denied. Either the team with ID=${teamId} does not own the event type with ID=${eventTypeId}, or your MEMBER role does not have permission to access this resource.` + ); + } + } } diff --git a/apps/api/v2/src/modules/organizations/organizations.module.ts b/apps/api/v2/src/modules/organizations/organizations.module.ts index 410db2c8d5..dc39adb9de 100644 --- a/apps/api/v2/src/modules/organizations/organizations.module.ts +++ b/apps/api/v2/src/modules/organizations/organizations.module.ts @@ -100,6 +100,7 @@ import { Module } from "@nestjs/common"; OrganizationsWebhooksService, WebhooksRepository, WebhooksService, + OrganizationsEventTypesService, ], controllers: [ OrganizationsTeamsController, diff --git a/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx b/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx index 4552170a44..366e9aaf03 100644 --- a/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx +++ b/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx @@ -21,6 +21,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { SchedulingType } from "@calcom/prisma/enums"; import { Label, Select, SettingsToggle } from "@calcom/ui"; +export type EventTeamAssignmentTabBaseProps = Pick; + export const mapMemberToChildrenOption = ( member: EventTypeSetupProps["teamMembers"][number], slug: string, @@ -427,11 +429,7 @@ const Hosts = ({ ); }; -export const EventTeamAssignmentTab = ({ - team, - teamMembers, - eventType, -}: Pick) => { +export const EventTeamAssignmentTab = ({ team, teamMembers, eventType }: EventTeamAssignmentTabBaseProps) => { const { t } = useLocale(); const schedulingTypeOptions: { diff --git a/packages/platform/atoms/event-types/hooks/useAtomUpdateEventType.ts b/packages/platform/atoms/event-types/hooks/useAtomUpdateEventType.ts index af48087052..2aebcb6c18 100644 --- a/packages/platform/atoms/event-types/hooks/useAtomUpdateEventType.ts +++ b/packages/platform/atoms/event-types/hooks/useAtomUpdateEventType.ts @@ -5,6 +5,7 @@ import { V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants"; import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types"; import type { EventType } from "@calcom/prisma/client"; +import { useAtomsContext } from "../../hooks/useAtomsContext"; import http from "../../lib/http"; export const QUERY_KEY = "use-event-by-id"; @@ -12,6 +13,7 @@ export type UseAtomUpdateEventTypeProps = { onSuccess?: () => void; onError?: (err: Error) => void; onSettled?: () => void; + teamId?: number; }; export const useAtomUpdateEventType = ({ onSuccess = () => { @@ -23,14 +25,20 @@ export const useAtomUpdateEventType = ({ onSettled = () => { return; }, + teamId, }: UseAtomUpdateEventTypeProps) => { + const { organizationId } = useAtomsContext(); return useMutation({ onSuccess, onError, onSettled, mutationFn: (data: EventTypeUpdateInput) => { if (!data.id) throw new Error("Event type id is required"); - const pathname = `/atoms/${V2_ENDPOINTS.eventTypes}/${data.id}`; + let pathname = `/atoms/${V2_ENDPOINTS.eventTypes}/${data.id}`; + + if (teamId) { + pathname = `/atoms/organizations/${organizationId}/teams/${teamId}/${V2_ENDPOINTS.eventTypes}/${data.id}`; + } return http?.patch>(pathname, data).then((res) => { if (res.data.status === SUCCESS_STATUS) { return (res.data as ApiSuccessResponse).data; diff --git a/packages/platform/atoms/event-types/wrappers/EventTeamAssignmentTabPlatformWrapper.tsx b/packages/platform/atoms/event-types/wrappers/EventTeamAssignmentTabPlatformWrapper.tsx new file mode 100644 index 0000000000..be3ead1f4d --- /dev/null +++ b/packages/platform/atoms/event-types/wrappers/EventTeamAssignmentTabPlatformWrapper.tsx @@ -0,0 +1,10 @@ +import { + EventTeamAssignmentTab, + type EventTeamAssignmentTabBaseProps, +} from "@calcom/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab"; + +const EventTeamAssignmentTabPlatformWrapper = (props: EventTeamAssignmentTabBaseProps) => { + return ; +}; + +export default EventTeamAssignmentTabPlatformWrapper; diff --git a/packages/platform/atoms/event-types/wrappers/EventTeamAssignmentTabWebWrapper.tsx b/packages/platform/atoms/event-types/wrappers/EventTeamAssignmentTabWebWrapper.tsx new file mode 100644 index 0000000000..0e9f234677 --- /dev/null +++ b/packages/platform/atoms/event-types/wrappers/EventTeamAssignmentTabWebWrapper.tsx @@ -0,0 +1,10 @@ +import { + EventTeamAssignmentTab, + type EventTeamAssignmentTabBaseProps, +} from "@calcom/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab"; + +const EventTeamAssignmentTabWebWrapper = (props: EventTeamAssignmentTabBaseProps) => { + return ; +}; + +export default EventTeamAssignmentTabWebWrapper; diff --git a/packages/platform/atoms/event-types/wrappers/EventTypePlatformWrapper.tsx b/packages/platform/atoms/event-types/wrappers/EventTypePlatformWrapper.tsx index df42425ded..453c2c4413 100644 --- a/packages/platform/atoms/event-types/wrappers/EventTypePlatformWrapper.tsx +++ b/packages/platform/atoms/event-types/wrappers/EventTypePlatformWrapper.tsx @@ -24,6 +24,7 @@ import EventAvailabilityTabPlatformWrapper from "./EventAvailabilityTabPlatformW import EventLimitsTabPlatformWrapper from "./EventLimitsTabPlatformWrapper"; import EventRecurringTabPlatformWrapper from "./EventRecurringTabPlatformWrapper"; import SetupTab from "./EventSetupTabPlatformWrapper"; +import EventTeamAssignmentTabPlatformWrapper from "./EventTeamAssignmentTabPlatformWrapper"; export type PlatformTabs = keyof Omit; @@ -99,9 +100,13 @@ const EventType = ({ toast({ description: message ? t(message) : t(err.message) }); onError?.(currentValues, err); }, + teamId: team?.id, }); - const { form, handleSubmit } = useEventTypeForm({ eventType, onSubmit: updateMutation.mutate }); + const { form, handleSubmit } = useEventTypeForm({ + eventType, + onSubmit: (data) => updateMutation.mutate(data), + }); const slug = form.watch("slug") ?? eventType.slug; const showToast = (message: string, variant: "success" | "warning" | "error") => { @@ -125,7 +130,11 @@ const EventType = ({ ) : ( <> ), - team: <>, + team: tabs.includes("team") ? ( + + ) : ( + <> + ), advanced: tabs.includes("advanced") ? ( import("./EventAvailabilityTabWebWrapper").then((mod) => mod) ); -const EventTeamAssignmentTab = dynamic(() => - import("@calcom/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab").then( - (mod) => mod.EventTeamAssignmentTab - ) -); +const EventTeamAssignmentTab = dynamic(() => import("./EventTeamAssignmentTabWebWrapper").then((mod) => mod)); const EventLimitsTab = dynamic(() => // import web wrapper when it's ready diff --git a/packages/platform/examples/base/src/pages/event-types.tsx b/packages/platform/examples/base/src/pages/event-types.tsx index 38c0070a57..4cc36c6acb 100644 --- a/packages/platform/examples/base/src/pages/event-types.tsx +++ b/packages/platform/examples/base/src/pages/event-types.tsx @@ -98,9 +98,8 @@ export default function Bookings(props: { calUsername: string; calEmail: string customClassNames={{ atomsWrapper: "!w-[50vw] !m-auto" }} allowDelete={true} id={eventTypeId} - tabs={["setup", "limits", "recurring", "advanced", "availability"]} + tabs={["setup", "limits", "recurring", "advanced", "availability", "team"]} onSuccess={(eventType) => { - console.log(eventType); setEventTypeId(null); refetch(); }}