diff --git a/apps/api/v2/src/modules/atoms/controllers/atoms.schedules.controller.ts b/apps/api/v2/src/modules/atoms/controllers/atoms.schedules.controller.ts index a30a94fbaa8654..7527b44e2f24b7 100644 --- a/apps/api/v2/src/modules/atoms/controllers/atoms.schedules.controller.ts +++ b/apps/api/v2/src/modules/atoms/controllers/atoms.schedules.controller.ts @@ -11,6 +11,7 @@ import { Param, ParseIntPipe, Patch, + Post, Query, UseGuards, Version, @@ -21,8 +22,11 @@ import { ApiExcludeController as DocsExcludeController, ApiOperation, } from "@nestjs/swagger"; +import { z } from "zod"; import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants"; +import type { CreateScheduleHandlerReturn, CreateScheduleSchema } from "@calcom/platform-libraries/schedules"; +import { createScheduleHandler } from "@calcom/platform-libraries/schedules"; import { FindDetailedScheduleByIdReturnType } from "@calcom/platform-libraries/schedules"; import { ApiResponse, UpdateAtomScheduleDto } from "@calcom/platform-types"; @@ -63,6 +67,7 @@ export class AtomsSchedulesController { data: schedule, }; } + @Patch("schedules/:scheduleId") @Permissions([SCHEDULE_WRITE]) @UseGuards(ApiAuthGuard) @@ -83,4 +88,20 @@ export class AtomsSchedulesController { data: updatedSchedule, }; } + + @Post("schedules/create") + @Permissions([SCHEDULE_WRITE]) + @UseGuards(ApiAuthGuard) + @ApiOperation({ summary: "Create atom schedule" }) + async createSchedule( + @GetUser() user: UserWithProfile, + @Body() bodySchedule: z.infer + ): Promise> { + const createdSchedule = await createScheduleHandler({ input: bodySchedule, ctx: { user } }); + + return { + status: SUCCESS_STATUS, + data: createdSchedule, + }; + } } diff --git a/docs/mint.json b/docs/mint.json index c7aa3d45b87a4e..37883103adab8b 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -219,7 +219,8 @@ "platform/atoms/calendar-settings", "platform/atoms/payment-form", "platform/atoms/conferencing-apps", - "platform/atoms/calendar-view" + "platform/atoms/calendar-view", + "platform/atoms/create-schedule" ] }, { diff --git a/docs/platform/atoms/create-schedule.mdx b/docs/platform/atoms/create-schedule.mdx new file mode 100644 index 00000000000000..f780889bdfb739 --- /dev/null +++ b/docs/platform/atoms/create-schedule.mdx @@ -0,0 +1,127 @@ +--- +title: "Create schedule" +--- + +The Create Schedule atom provides a simple dialog interface for users to create new availability +schedules. Fully customizable with callback support for handling successful schedule creation. + +Below code snippet can be used to render the Create schedule atom + +```js +import { CreateSchedule } from "@calcom/atoms"; + +export default function CreateSchedule() { + return ( + <> + + + ) +} +``` + +For a demonstration of the Create schedule atom, please refer to the video below. + +

+ + + +

+ +For developers who don't want the dialog-based interface, we provide a `CreateScheduleForm` atom to integrate the schedule +creation form directly into your own UI. This headless approach gives you full flexibility to +handle layout, styling, and user flow exactly how you need it. + +Below code snippet can be used to render the Create schedule form atom + +```js +import { CreateScheduleForm } from "@calcom/atoms"; + +export default function CreateScheduleForm() { + return ( + <> + + + ) +} +``` + +For a demonstration of the Create schedule form, please refer to the video below. + +

+ + + +

+ +We offer all kinds of customizations to the Create schedule atom via props. Below is a list of props that can be passed to the atom. + +

+ +| Name | Required | Description | +| :--------------- | :------- | :----------------------------------------------------------------------- | +| name | No | The label for the create schedule button | +| customClassNames | No | To pass in custom classnames from outside for styling the atom | +| onSuccess | No | Callback function that handles successful creation of schedule | +| onError | No | Callback function to handles errors at the time of schedule creation | +| disableToasts | No | Boolean value that determines whether the toasts are displayed or not | + + +Along with the props, create schedule atom accepts custom styles via the **customClassNames** prop. Below is a list of props that fall under this **customClassNames** prop. +

+| Name | Description | +|:---------------------------|:-----------------------------------------------------------------------| +| createScheduleButton | Adds styling to the create button | +| inputField | Adds styling to the container of the name input field | +| formWrapper | Adds styling to the whole form | +| actionsButtons | Object containing classnames for the submit, cancel buttons and container inside the create schedule atom | + +Similar to the create schedule atom, the create schedule form also offer all kinds of customizations via props. Below is a list of props that can be passed to the atom. +

+| customClassNames | No | To pass in custom classnames from outside for styling the atom | +| onSuccess | No | Callback function that handles successful creation of schedule | +| onError | No | Callback function to handles errors at the time of schedule creation | +| disableToasts | No | Boolean value that determines whether the toasts are displayed or not | + +Along with the props, create schedule form also accepts custom styles via the **customClassNames** prop. Below is a list of props that fall under this **customClassNames** prop. + +| Name | Description | +|:---------------------------|:-----------------------------------------------------------------------| +| formWrapper | Adds styling to the whole form | +| inputField | Adds styling to the container of the name input field | +| actionsButtons | Object containing classnames for the submit, cancel buttons and container inside the create schedule atom | + + + + Please ensure all custom classnames are valid Tailwind CSS classnames. Note that sometimes the custom + classnames may fail to override the styling with the classnames that you might have passed via props. That + is because the clsx utility function that we use to override classnames inside our components has some + limitations. A simple get around to solve this issue is to just prefix your classnames with ! property just + before passing in any classname. + diff --git a/packages/platform/atoms/create-schedule/CreateScheduleForm.tsx b/packages/platform/atoms/create-schedule/CreateScheduleForm.tsx new file mode 100644 index 00000000000000..485e3c5b378b1a --- /dev/null +++ b/packages/platform/atoms/create-schedule/CreateScheduleForm.tsx @@ -0,0 +1,108 @@ +import { useForm } from "react-hook-form"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { ApiErrorResponse } from "@calcom/platform-types"; +import { Button } from "@calcom/ui/components/button"; +import { Form } from "@calcom/ui/components/form"; +import { InputField } from "@calcom/ui/components/form"; + +import { useAtomCreateSchedule } from "../hooks/schedules/useAtomCreateSchedule"; +import { AtomsWrapper } from "../src/components/atoms-wrapper"; +import { useToast } from "../src/components/ui/use-toast"; +import { cn } from "../src/lib/utils"; + +export type ActionButtonsClassNames = { + container?: string; + continue?: string; + close?: string; +}; + +export const CreateScheduleForm = ({ + onSuccess, + onError, + onCancel, + customClassNames, + disableToasts = false, +}: { + onSuccess?: (scheduleId: number) => void; + onError?: (err: ApiErrorResponse) => void; + onCancel?: () => void; + customClassNames?: { + formWrapper?: string; + inputField?: string; + actionsButtons?: ActionButtonsClassNames; + }; + disableToasts?: boolean; +}) => { + const { toast } = useToast(); + const { t } = useLocale(); + const form = useForm<{ + name: string; + }>(); + const { register } = form; + + const { mutateAsync: createSchedule, isPending: isCreateSchedulePending } = useAtomCreateSchedule({ + onSuccess: (res) => { + if (!disableToasts) { + toast({ + description: t("schedule_created_successfully", { scheduleName: res.data.schedule.name }), + }); + } + form.reset(); + onSuccess?.(res.data.schedule.id); + }, + onError: (err) => { + onError?.(err); + if (!disableToasts) { + toast({ + description: `Could not create schedule: ${err.error.message}`, + }); + } + }, + }); + + return ( + +
{ + await createSchedule(values); + }}> + (!v || v.trim() === "" ? null : v), + })} + /> +
+ {onCancel && ( + + )} + +
+ +
+ ); +}; diff --git a/packages/platform/atoms/create-schedule/index.tsx b/packages/platform/atoms/create-schedule/index.tsx new file mode 100644 index 00000000000000..00eca07d8bbd6d --- /dev/null +++ b/packages/platform/atoms/create-schedule/index.tsx @@ -0,0 +1 @@ +export { CreateSchedulePlatformWrapper } from "./wrappers/CreateSchedulePlatformWrapper"; diff --git a/packages/platform/atoms/create-schedule/wrappers/CreateSchedulePlatformWrapper.tsx b/packages/platform/atoms/create-schedule/wrappers/CreateSchedulePlatformWrapper.tsx new file mode 100644 index 00000000000000..3ff3cf63161f97 --- /dev/null +++ b/packages/platform/atoms/create-schedule/wrappers/CreateSchedulePlatformWrapper.tsx @@ -0,0 +1,69 @@ +import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; +import { useState } from "react"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { ApiErrorResponse } from "@calcom/platform-types"; +import { Button } from "@calcom/ui/components/button"; + +import { AtomsWrapper } from "../../src/components/atoms-wrapper"; +import { CreateScheduleForm } from "../CreateScheduleForm"; +import { ActionButtonsClassNames } from "../CreateScheduleForm"; + +export const CreateSchedulePlatformWrapper = ({ + name, + customClassNames, + onSuccess, + onError, + disableToasts = false, +}: { + name?: string; + onSuccess?: (scheduleId: number) => void; + onError?: (err: ApiErrorResponse) => void; + customClassNames?: { + createScheduleButton?: string; + inputField?: string; + formWrapper?: string; + actionsButtons?: ActionButtonsClassNames; + }; + disableToasts?: boolean; +}) => { + const { t } = useLocale(); + const [isDialogOpen, setIsDialogOpen] = useState(false); + + return ( + + + + + + + { + setIsDialogOpen(false); + onSuccess?.(scheduleId); + }} + onError={onError} + onCancel={() => setIsDialogOpen(false)} + disableToasts={disableToasts} + /> + + + + ); +}; diff --git a/packages/platform/atoms/hooks/schedules/useAtomCreateSchedule.ts b/packages/platform/atoms/hooks/schedules/useAtomCreateSchedule.ts new file mode 100644 index 00000000000000..1ee4f61bd62a60 --- /dev/null +++ b/packages/platform/atoms/hooks/schedules/useAtomCreateSchedule.ts @@ -0,0 +1,46 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import type { ApiResponse, ApiErrorResponse, ApiSuccessResponse } from "@calcom/platform-types"; +import type { CreateScheduleHandlerReturn } from "@calcom/trpc/server/routers/viewer/availability/schedule/create.handler"; +import { TCreateInputSchema as CreateScheduleSchema } from "@calcom/trpc/server/routers/viewer/availability/schedule/create.schema"; + +import http from "../../lib/http"; +import { QUERY_KEY as ScheduleQueryKey } from "./useAtomSchedule"; + +interface useAtomCreateScheduleOptions { + onSuccess?: (res: ApiSuccessResponse) => void; + onError?: (err: ApiErrorResponse) => void; +} + +export const useAtomCreateSchedule = ( + { onSuccess, onError }: useAtomCreateScheduleOptions = { + onSuccess: () => { + return; + }, + onError: () => { + return; + }, + } +) => { + const queryClient = useQueryClient(); + + return useMutation, unknown, CreateScheduleSchema>({ + mutationFn: async (body: CreateScheduleSchema) => { + const url = `atoms/schedules/create`; + const response = await http.post>(url, body); + return response.data; + }, + onSuccess: (data) => { + if (data.status === SUCCESS_STATUS) { + onSuccess?.(data); + queryClient.invalidateQueries({ queryKey: [ScheduleQueryKey] }); + } else { + onError?.(data); + } + }, + onError: (err) => { + onError?.(err as ApiErrorResponse); + }, + }); +}; diff --git a/packages/platform/atoms/index.ts b/packages/platform/atoms/index.ts index 7622eb877f3a52..c0062ee9b64147 100644 --- a/packages/platform/atoms/index.ts +++ b/packages/platform/atoms/index.ts @@ -41,4 +41,8 @@ export { useCreateTeamEventType } from "./hooks/event-types/private/useCreateTea export { useOrganizationBookings } from "./hooks/organizations/bookings/useOrganizationBookings"; export { useOrganizationUserBookings } from "./hooks/organizations/bookings/useOrganizationUserBookings"; + export { CalendarViewPlatformWrapper as CalendarView } from "./calendar-view/index"; + +export { CreateSchedulePlatformWrapper as CreateSchedule } from "./create-schedule/index"; +export { CreateScheduleForm } from "./create-schedule/CreateScheduleForm"; diff --git a/packages/platform/libraries/schedules.ts b/packages/platform/libraries/schedules.ts index ba3967e88fdf31..578c4ede61b53d 100644 --- a/packages/platform/libraries/schedules.ts +++ b/packages/platform/libraries/schedules.ts @@ -5,3 +5,9 @@ export { export { updateSchedule, type UpdateScheduleResponse } from "@calcom/lib/schedules/updateSchedule"; export { UserAvailabilityService } from "@calcom/features/availability/lib/getUserAvailability"; + +export { + createHandler as createScheduleHandler, + type CreateScheduleHandlerReturn, +} from "@calcom/trpc/server/routers/viewer/availability/schedule/create.handler"; +export { ZCreateInputSchema as CreateScheduleSchema } from "@calcom/trpc/server/routers/viewer/availability/schedule/create.schema"; diff --git a/packages/trpc/server/routers/viewer/availability/schedule/create.handler.ts b/packages/trpc/server/routers/viewer/availability/schedule/create.handler.ts index d5859a3188cb21..083d2cf61c8922 100644 --- a/packages/trpc/server/routers/viewer/availability/schedule/create.handler.ts +++ b/packages/trpc/server/routers/viewer/availability/schedule/create.handler.ts @@ -9,11 +9,13 @@ import type { TCreateInputSchema } from "./create.schema"; type CreateOptions = { ctx: { - user: NonNullable; + user: Pick, "id" | "timeZone" | "defaultScheduleId">; }; input: TCreateInputSchema; }; +export type CreateScheduleHandlerReturn = Awaited>; + export const createHandler = async ({ input, ctx }: CreateOptions) => { const { user } = ctx; if (input.eventTypeId) {