feat: list schedules atom (#24205)

* feat: add api v2 endpoint to fetch all user schedules

* chore: update typing

* feat: custom hook to fetch all user schedules

* refactor: shift logic to transform schedules into function of its own

* init: list schedules atom

* feat: api endpoints for list schedules atom

* refactor: accept redirect url as prop

* fix: pass redirect url prop

* integrate list schedules atom with availability settings

* refactor: extract types to be reused in api v2 endpoints

* skip availability settings page for the time being until we have api v2 endpoints in prod

* feat: add docs for list schedules atom

* fixup

* export Schedule type

* make sure we always have a default schedule if user deletes his default schedule

* chore: implement code rabbit feedback

* chore: implement PR feedback

* fix: resolve merge conflicts

* fix: import path

* update platform libraries

* fix: type check

* resolve  merge conflicts

* update atoms export

* update platform libraries schedule

* chore: arrange atoms in alphabetical order

* update atoms controller to include endpoints for list schedules atom

* add create atom scheule atom

* fix: invalidate schedules on new schedule creation

* chore: add changesets
This commit is contained in:
Rajiv Sahal
2025-10-15 17:57:22 +02:00
committed by GitHub
parent 3402f79912
commit 0757b00db7
21 changed files with 483 additions and 24 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@calcom/atoms": minor
---
This PR adds an atom to list a user's schedule. Furthermore, there was also another atom introduced which can be used to create a new schedule for a user.
@@ -25,6 +25,11 @@ import {
import { z } from "zod";
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import type {
GetAvailabilityListHandlerReturn,
DuplicateScheduleHandlerReturn,
} from "@calcom/platform-libraries/schedules";
import { getAvailabilityListHandler, duplicateScheduleHandler } from "@calcom/platform-libraries/schedules";
import type { CreateScheduleHandlerReturn, CreateScheduleSchema } from "@calcom/platform-libraries/schedules";
import { createScheduleHandler } from "@calcom/platform-libraries/schedules";
import { FindDetailedScheduleByIdReturnType } from "@calcom/platform-libraries/schedules";
@@ -68,6 +73,21 @@ export class AtomsSchedulesController {
};
}
@Get("/schedules/all")
@Version(VERSION_NEUTRAL)
@UseGuards(ApiAuthGuard)
@Permissions([SCHEDULE_READ])
async getAllUserSchedules(
@GetUser() user: UserWithProfile
): Promise<ApiResponse<Awaited<GetAvailabilityListHandlerReturn>>> {
const userSchedules = await getAvailabilityListHandler({ ctx: { user } });
return {
status: SUCCESS_STATUS,
data: userSchedules,
};
}
@Patch("schedules/:scheduleId")
@Permissions([SCHEDULE_WRITE])
@UseGuards(ApiAuthGuard)
@@ -104,4 +124,20 @@ export class AtomsSchedulesController {
data: createdSchedule,
};
}
@Post("schedules/:scheduleId/duplicate")
@Permissions([SCHEDULE_WRITE])
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "Duplicate existing schedule" })
async duplicateExistingSchedule(
@GetUser() user: UserWithProfile,
@Param("scheduleId", ParseIntPipe) scheduleId: number
): Promise<ApiResponse<Awaited<DuplicateScheduleHandlerReturn>>> {
const duplicatedSchedule = await duplicateScheduleHandler({ ctx: { user }, input: { scheduleId } });
return {
status: SUCCESS_STATUS,
data: duplicatedSchedule,
};
}
}
@@ -9,6 +9,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
import { getScheduleListItemData } from "@calcom/lib/schedules/transformers/getScheduleListItemData";
import { MembershipRole } from "@calcom/prisma/enums";
import { availabilityRouter } from "@calcom/trpc/server/routers/viewer/availability/_router";
@@ -56,15 +57,7 @@ const Page = async ({ searchParams: _searchParams }: PageProps) => {
// This is because the data is cached and as a result the data is converted to a string
const availabilities = {
...cachedAvailabilities,
schedules: cachedAvailabilities.schedules.map((schedule) => ({
...schedule,
availability: schedule.availability.map((avail) => ({
...avail,
startTime: new Date(avail.startTime),
endTime: new Date(avail.endTime),
date: avail.date ? new Date(avail.date) : null,
})),
})),
schedules: cachedAvailabilities.schedules.map((schedule) => getScheduleListItemData(schedule)),
};
const organizationId = session?.user?.profile?.organizationId ?? session?.user.org?.id;
@@ -141,6 +141,7 @@ export function AvailabilityList({ availabilities }: AvailabilityListProps) {
<ul className="divide-subtle divide-y" data-testid="schedules" ref={animationParentRef}>
{availabilities.schedules.map((schedule) => (
<ScheduleListItem
redirectUrl={`/availability/${schedule.id}`}
displayOptions={{
hour12: user?.timeFormat ? user.timeFormat === 12 : undefined,
timeZone: user?.timeZone,
+8 -7
View File
@@ -209,18 +209,19 @@
"group": "Atoms",
"icon": "atom",
"pages": [
"platform/atoms/cal-provider",
"platform/atoms/google-calendar-connect",
"platform/atoms/outlook-calendar-connect",
"platform/atoms/apple-calendar-connect",
"platform/atoms/availability-settings",
"platform/atoms/booker",
"platform/atoms/event-type",
"platform/atoms/cal-provider",
"platform/atoms/calendar-settings",
"platform/atoms/payment-form",
"platform/atoms/conferencing-apps",
"platform/atoms/calendar-view",
"platform/atoms/create-schedule"
"platform/atoms/conferencing-apps",
"platform/atoms/create-schedule",
"platform/atoms/event-type",
"platform/atoms/google-calendar-connect",
"platform/atoms/list-schedules",
"platform/atoms/outlook-calendar-connect",
"platform/atoms/payment-form"
]
},
{
+116
View File
@@ -0,0 +1,116 @@
---
title: "List schedules"
---
The List schedules atom displays all of a user's availability schedules and provides management actions such as delete, duplicate, and edit capabilities for each schedule.
Below code snippet can be used to render the List schedules atom
```js
import { ListSchedules } from "@calcom/atoms";
export default function ListSchedules() {
return (
<>
<ListSchedules getRedirectUrl={(scheduleId) => `/availability/${scheduleId}`} />
</>
);
}
```
For a demonstration of the List schedules atom, please refer to the video below.
<p></p>
<iframe
height="315"
style={{ width: "100%", maxWidth: "560px" }}
src="https://www.loom.com/embed/8e020a4848d543548c519d3a7cd532c6?sid=e6ccad23-8372-402a-8fd3-c0180bc9113c"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen="true"
></iframe>
<p></p>
Below is a list of props that can be passed to the List schedules atom.
<p></p>
| Name | Required | Description |
| :------------- | :------- | :---------------------------------------------------------------------------------------------------------------- |
| getRedirectUrl | No | Callback function that determines where the user should be redirected when they click on a schedule from the atom |
## Combining list schedules atom with availability settings atom
The List schedules atom works best when combined with the Availability settings atom to provide a seamless experience. In order to combine both of atom, here are the steps we recommend:
<Steps>
<Step title="Setup list schedules atom">
Create a page for demostrating the list schedules atom.
Below code snippet can be used to setup the list schedules atom on a new page
```js
import { ListSchedules } from "@calcom/atoms";
export default function ListSchedules() {
return (
<>
<ListSchedules getRedirectUrl={(scheduleId) => `/availability/${scheduleId}`} />
</>
);
}
```
`getRedirectUrl` prop is a callback function that determines where the user should be redirected when they click on a schedule from the atom. Say if you want to display your availability settings atom on the page `/availability/:scheduleId`, you need to make sure you return the exact same url when you call the `getRedirectUrl` function.
</Step>
<Step title="Setup availability settings atom">
Create a page for demonstrating the availability settings atom. This should be a dynamic route that accepts a schedule id as a query parameter, and then the schedule id gets passed to the availability settings atom.
Below code snippet can be used to obtain the `scheduleId` and display the availability settings atom
```js
import { AvailabilitySettings } from "@calcom/atoms";
import { useRouter } from "next/router";
export default function Availability() {
const router = useRouter();
return (
<>
<AvailabilitySettings
id={router.query.scheduleId as string}
onUpdateSuccess={() => {
console.log("Updated schedule successfully");
}}
/>
</>
)
}
```
</Step>
</Steps>
-Below video shows a demonstration of how we combined the list schedules atom and availability settings atom in one of our example app to create a seamless availability management experience.
<p></p>
<iframe
height="315"
style={{ width: "100%", maxWidth: "560px" }}
src="https://www.loom.com/embed/76b65330e61e47eb95b7072ed3a42ec8?sid=9373ec6f-ba55-431e-98de-5dd3fb252a8a"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen="true"
></iframe>
<p></p>
<Info>
Link to the example app can be found
[here](https://github.com/calcom/cal.com/tree/main/packages/platform/examples/base)
</Info>
@@ -26,6 +26,7 @@ export function ScheduleListItem({
updateDefault,
isDeletable,
duplicateFunction,
redirectUrl,
}: {
schedule: RouterOutputs["viewer"]["availability"]["list"]["schedules"][number];
deleteFunction: ({ scheduleId }: { scheduleId: number }) => void;
@@ -37,6 +38,7 @@ export function ScheduleListItem({
isDeletable: boolean;
updateDefault: ({ scheduleId, isDefault }: { scheduleId: number; isDefault: boolean }) => void;
duplicateFunction: ({ scheduleId }: { scheduleId: number }) => void;
redirectUrl: string;
}) {
const { t, i18n } = useLocale();
@@ -44,10 +46,7 @@ export function ScheduleListItem({
<li key={schedule.id}>
<div className="hover:bg-muted flex items-center justify-between px-3 py-5 transition sm:px-4">
<div className="group flex w-full items-center justify-between ">
<Link
href={`/availability/${schedule.id}`}
className="flex-grow truncate text-sm"
title={schedule.name}>
<Link href={redirectUrl} className="flex-grow truncate text-sm" title={schedule.name}>
<div className="space-x-2 rtl:space-x-reverse">
<span className="text-emphasis truncate font-medium">{schedule.name}</span>
{schedule.isDefault && (
@@ -0,0 +1,26 @@
export type Schedule = {
isDefault: boolean;
id: number;
name: string;
timeZone: string | null;
availability: {
id: number;
userId: number | null;
startTime: Date;
endTime: Date;
eventTypeId: number | null;
date: Date | null;
days: number[];
scheduleId: number | null;
}[];
};
export const getScheduleListItemData = (schedule: Schedule) => ({
...schedule,
availability: schedule.availability.map((avail) => ({
...avail,
startTime: new Date(avail.startTime),
endTime: new Date(avail.endTime),
date: avail.date ? new Date(avail.date) : null,
})),
});
@@ -6,6 +6,7 @@ import type { CreateScheduleHandlerReturn } from "@calcom/trpc/server/routers/vi
import { TCreateInputSchema as CreateScheduleSchema } from "@calcom/trpc/server/routers/viewer/availability/schedule/create.schema";
import http from "../../lib/http";
import { QUERY_KEY as SchedulesQueryKey } from "./useAtomGetAllSchedules";
import { QUERY_KEY as ScheduleQueryKey } from "./useAtomSchedule";
interface useAtomCreateScheduleOptions {
@@ -35,6 +36,7 @@ export const useAtomCreateSchedule = (
if (data.status === SUCCESS_STATUS) {
onSuccess?.(data);
queryClient.invalidateQueries({ queryKey: [ScheduleQueryKey] });
queryClient.invalidateQueries({ queryKey: [SchedulesQueryKey] });
} else {
onError?.(data);
}
@@ -0,0 +1,45 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { ApiResponse, ApiErrorResponse } from "@calcom/platform-types";
import type { DuplicateScheduleHandlerReturn } from "@calcom/trpc/server/routers/viewer/availability/schedule/duplicate.handler";
import http from "../../lib/http";
import { QUERY_KEY as ScheduleQueryKey } from "./useAtomSchedule";
interface useAtomDuplicateScheduleOptions {
onSuccess?: (res: ApiResponse<DuplicateScheduleHandlerReturn>) => void;
onError?: (err: ApiErrorResponse) => void;
}
export const useAtomDuplicateSchedule = (
{ onSuccess, onError }: useAtomDuplicateScheduleOptions = {
onSuccess: () => {
return;
},
onError: () => {
return;
},
}
) => {
const queryClient = useQueryClient();
return useMutation<ApiResponse<DuplicateScheduleHandlerReturn>, unknown, { scheduleId: number }>({
mutationFn: async ({ scheduleId }: { scheduleId: number }) => {
const url = `atoms/schedules/${scheduleId}/duplicate`;
const response = await http.post<ApiResponse<DuplicateScheduleHandlerReturn>>(url);
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);
},
});
};
@@ -0,0 +1,30 @@
import { useQuery } from "@tanstack/react-query";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { ApiResponse } from "@calcom/platform-types";
import type { GetAvailabilityListHandlerReturn } from "@calcom/trpc/server/routers/viewer/availability/list.handler";
import http from "../../lib/http";
import { useAtomsContext } from "../useAtomsContext";
export const QUERY_KEY = "use-atom-user-schedules";
export const useAtomGetAllSchedules = () => {
const pathname = "atoms/schedules/all";
const { isInit, accessToken } = useAtomsContext();
const { isLoading, error, data, refetch } = useQuery({
queryKey: [QUERY_KEY],
queryFn: () => {
return http.get<ApiResponse<GetAvailabilityListHandlerReturn>>(pathname).then((res) => {
if (res.data.status === SUCCESS_STATUS) {
return res.data.data;
}
throw new Error(res.data.error?.message);
});
},
enabled: isInit && !!accessToken,
});
return { isLoading, error, data, refetch };
};
@@ -0,0 +1,22 @@
import { useEffect, useRef } from "react";
import { Schedule } from "@calcom/lib/schedules/transformers/getTransformedSchedles";
export function useEnsureDefaultSchedule(
schedules: Schedule[],
onUpdateSchedule: (scheduleId: number) => void | Promise<void>
) {
const onUpdateRef = useRef(onUpdateSchedule);
useEffect(() => {
onUpdateRef.current = onUpdateSchedule;
});
useEffect(() => {
const defaultSchedule = schedules.filter((schedule) => schedule.isDefault);
if (defaultSchedule.length === 0 && schedules.length > 0) {
onUpdateRef.current(schedules[0].id);
}
}, [schedules]);
}
+2
View File
@@ -46,3 +46,5 @@ export { CalendarViewPlatformWrapper as CalendarView } from "./calendar-view/ind
export { CreateSchedulePlatformWrapper as CreateSchedule } from "./create-schedule/index";
export { CreateScheduleForm } from "./create-schedule/CreateScheduleForm";
export { ListSchedulesPlatformWrapper as ListSchedules } from "./list-schedules/index";
@@ -0,0 +1 @@
export { ListSchedulesPlatformWrapper } from "./wrappers/ListSchedulesPlatformWrapper";
@@ -0,0 +1,129 @@
import { AtomsWrapper } from "@/components/atoms-wrapper";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { ScheduleListItem } from "@calcom/features/schedules/components/ScheduleListItem";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getScheduleListItemData } from "@calcom/lib/schedules/transformers/getScheduleListItemData";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { useAtomDuplicateSchedule } from "../../hooks/schedules/useAtomDuplicateSchedule";
import { useAtomGetAllSchedules } from "../../hooks/schedules/useAtomGetAllSchedules";
import { useAtomUpdateSchedule } from "../../hooks/schedules/useAtomUpdateSchedule";
import useDeleteSchedule from "../../hooks/schedules/useDeleteSchedule";
import { useEnsureDefaultSchedule } from "../../hooks/schedules/useEnsureDefaultSchedule";
import { useMe } from "../../hooks/useMe";
import { useToast } from "../../src/components/ui/use-toast";
interface ListSchedulesPlatformWrapperProps {
getScheduleUrl?: (scheduleId: number) => string;
}
export const ListSchedulesPlatformWrapper = ({ getScheduleUrl }: ListSchedulesPlatformWrapperProps = {}) => {
const [animationParentRef] = useAutoAnimate<HTMLUListElement>();
const { toast } = useToast();
const { t } = useLocale();
const {
data: userSchedules,
isLoading: isLoadingSchedules,
refetch: refetchSchedules,
} = useAtomGetAllSchedules();
const { data: user, isLoading: isUserLoading } = useMe();
const { mutate: updateSchedule } = useAtomUpdateSchedule({
onSuccess: () => {
toast({
description: "Schedule updated successfully",
});
refetchSchedules();
},
onError: (err) => {
toast({
description: `Could not update schedule: ${err.error.message}`,
});
},
});
const { mutate: duplicateSchedule } = useAtomDuplicateSchedule({
onSuccess: () => {
toast({
description: "Schedule created successfully",
});
refetchSchedules();
},
onError: (err) => {
toast({
description: `Could not duplicate schedule: ${err.error.message}`,
});
},
});
const { mutate: deleteSchedule } = useDeleteSchedule({
onSuccess: () => {
toast({
description: "Schedule deleted successfully",
});
refetchSchedules();
},
onError: (err) => {
toast({
description: `Could not delete schedule: ${err.error.message}`,
});
},
});
useEnsureDefaultSchedule(userSchedules?.schedules ?? [], (id) => {
updateSchedule({
scheduleId: id,
body: {
isDefault: true,
},
});
});
const transformedSchedules = userSchedules?.schedules.map((schedule) => getScheduleListItemData(schedule));
if (isLoadingSchedules || isUserLoading) return <>{t("loading")}</>;
if (!isLoadingSchedules && transformedSchedules?.length === 0)
return (
<EmptyScreen
Icon="clock"
headline={t("no_schedules_present")}
description={t("create_new_schedule")}
className="w-full"
/>
);
return (
<AtomsWrapper>
<div className="border-subtle bg-default overflow-hidden rounded-md border">
<ul className="divide-subtle divide-y" data-testid="schedules" ref={animationParentRef}>
{transformedSchedules?.map((schedule) => (
<ScheduleListItem
displayOptions={{
hour12: user?.data.timeFormat ? user.data.timeFormat === 12 : undefined,
timeZone: user?.data.timeZone,
weekStart: user?.data.weekStart || "Sunday",
}}
key={schedule.id}
schedule={schedule}
isDeletable={transformedSchedules.length !== 1}
redirectUrl={getScheduleUrl ? getScheduleUrl(schedule.id) : "#"}
updateDefault={() => {
updateSchedule({
scheduleId: schedule.id,
body: {
isDefault: true,
},
});
}}
deleteFunction={async () => await deleteSchedule({ id: schedule.id })}
duplicateFunction={() => duplicateSchedule({ scheduleId: schedule.id })}
/>
))}
</ul>
</div>
</AtomsWrapper>
);
};
@@ -1,5 +1,7 @@
import { Navbar } from "@/components/Navbar";
import { Inter } from "next/font/google";
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
import { useRouter } from "next/router";
import { useRef, useCallback } from "react";
import type { AvailabilitySettingsFormRef } from "@calcom/atoms";
@@ -8,6 +10,7 @@ import { AvailabilitySettings } from "@calcom/atoms";
const inter = Inter({ subsets: ["latin"] });
export default function Availability(props: { calUsername: string; calEmail: string }) {
const router = useRouter();
const availabilityRef = useRef<AvailabilitySettingsFormRef>(null);
const handleFormStateChange = useCallback((formState: unknown) => {
@@ -50,6 +53,7 @@ export default function Availability(props: { calUsername: string; calEmail: str
</div>
<AvailabilitySettings
id={router.query.scheduleId as string}
ref={availabilityRef}
enableOverrides={true}
customClassNames={{
@@ -82,6 +86,7 @@ export default function Availability(props: { calUsername: string; calEmail: str
onFormStateChange={handleFormStateChange}
onUpdateSuccess={() => {
console.log("Updated successfully");
router.push(`/availability`);
}}
onUpdateError={() => {
console.log("update error");
@@ -0,0 +1,23 @@
import { Navbar } from "@/components/Navbar";
import { Inter } from "next/font/google";
import { ListSchedules, CreateSchedule } from "@calcom/atoms";
const inter = Inter({ subsets: ["latin"] });
export default function Availability(props: { calUsername: string; calEmail: string }) {
return (
<main className={`flex min-h-screen flex-col ${inter.className}`}>
<Navbar username={props.calUsername} />
<div className="m-5 flex flex-col gap-4">
<div data-testid="create-schedule-atom">
<CreateSchedule name="Create new schedule" />
</div>
<div data-testid="list-schedules-atom">
<ListSchedules getScheduleUrl={(scheduleId) => `/availability/${scheduleId}`} />
</div>
</div>
</main>
);
}
@@ -8,11 +8,20 @@ async function selectOption(page: Page, optionNumber: number) {
await page.keyboard.press("Enter");
}
test("availability page loads with all components", async ({ page }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip("availability page loads with all components", async ({ page }) => {
await page.goto("/availability");
await expect(page).toHaveURL("/availability");
await expect(page.locator('[data-testid="list-schedules-atom"]')).toBeVisible();
await expect(page.locator('[data-testid="schedules"]')).toBeVisible();
await expect(page.locator('[data-testid="schedules"] li').first()).toBeVisible();
await expect(
page.locator('[data-testid="schedules"] li').first().locator('[data-testid="schedule-more"]')
).toBeVisible();
await page.locator('[data-testid="schedules"] li').first().locator('[data-testid="schedule-more"]').click();
await expect(page.locator('h1:has-text("Availability Settings")')).toBeVisible();
await expect(page.locator('button:has-text("Validate Form")')).toBeVisible();
+9
View File
@@ -11,3 +11,12 @@ export {
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";
export {
listHandler as getAvailabilityListHandler,
type GetAvailabilityListHandlerReturn,
} from "@calcom/trpc/server/routers/viewer/availability/list.handler";
export {
duplicateHandler as duplicateScheduleHandler,
type DuplicateScheduleHandlerReturn,
} from "@calcom/trpc/server/routers/viewer/availability/schedule/duplicate.handler";
@@ -5,10 +5,12 @@ import { getDefaultScheduleId } from "./util";
type ListOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
user: Pick<NonNullable<TrpcSessionUser>, "id" | "defaultScheduleId">;
};
};
export type GetAvailabilityListHandlerReturn = Awaited<ReturnType<typeof listHandler>>;
export const listHandler = async ({ ctx }: ListOptions) => {
const { user } = ctx;
@@ -47,6 +49,7 @@ export const listHandler = async ({ ctx }: ListOptions) => {
},
});
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
defaultScheduleId = null;
}
@@ -8,11 +8,13 @@ import type { TScheduleDuplicateSchema } from "./duplicate.schema";
type DuplicateScheduleOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
user: Pick<NonNullable<TrpcSessionUser>, "id" | "timeZone">;
};
input: TScheduleDuplicateSchema;
};
export type DuplicateScheduleHandlerReturn = Awaited<ReturnType<typeof duplicateHandler>>;
export const duplicateHandler = async ({ ctx, input }: DuplicateScheduleOptions) => {
try {
const { scheduleId } = input;