Feature/multiple schedules post turbo (#2150)
* Concluded merge * Applied stash to newly merged * Always disconnect + remove redundant success message * Added named dialog to replace new=1 * Merged with main p2 * Set eventTypeId to @unique * WIP * Undo vscode changes * Availability dropdown works * Remove console.log + set schedule to null as it is unneeded * Added schedule to availability endpoint * Reduce one refresh; hotfix state inconsistency with forced refresh for now * Add missing translations * Fixed some type errors I missed * Ditch outdated remnant from before packages/prisma * Remove Availability section for teams * Bringing back the Availability section temporarily to teams to allow configuration * Migrated getting-started to new availability system + updated translations + updated seed * Fixed type error coming from main * Titlecase 'default' by providing translation * Fixed broken 'radio' buttons. * schedule deleted translation added * Added empty state for when no schedules are configured * Added correct created message + hotfix reload hard on delete to refresh state * Removed index renames * Type fixes * Update NewScheduleButton.tsx Co-authored-by: zomars <zomars@me.com> Co-authored-by: Bailey Pumfleet <pumfleet@hey.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									bcbf8390e0
								
							
						
					
					
						commit
						6a211dd5b3
					
				
					 28 changed files with 1073 additions and 1207 deletions
				
			
		| 
						 | 
					@ -307,7 +307,7 @@ export default function Shell(props: {
 | 
				
			||||||
                  </Button>
 | 
					                  </Button>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              )}
 | 
					              )}
 | 
				
			||||||
              <div className="block min-h-[80px] justify-between px-4 sm:flex sm:px-6 md:px-8">
 | 
					              <div className="block justify-between px-4 sm:flex sm:px-6 md:px-8">
 | 
				
			||||||
                {props.HeadingLeftIcon && <div className="ltr:mr-4">{props.HeadingLeftIcon}</div>}
 | 
					                {props.HeadingLeftIcon && <div className="ltr:mr-4">{props.HeadingLeftIcon}</div>}
 | 
				
			||||||
                <div className="mb-8 w-full">
 | 
					                <div className="mb-8 w-full">
 | 
				
			||||||
                  <h1 className="font-cal mb-1 text-xl text-gray-900">{props.heading}</h1>
 | 
					                  <h1 className="font-cal mb-1 text-xl text-gray-900">{props.heading}</h1>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										77
									
								
								apps/web/components/availability/NewScheduleButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								apps/web/components/availability/NewScheduleButton.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,77 @@
 | 
				
			||||||
 | 
					import { PlusIcon } from "@heroicons/react/solid";
 | 
				
			||||||
 | 
					import { useRouter } from "next/router";
 | 
				
			||||||
 | 
					import { useForm } from "react-hook-form";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useLocale } from "@calcom/lib/hooks/useLocale";
 | 
				
			||||||
 | 
					import showToast from "@calcom/lib/notification";
 | 
				
			||||||
 | 
					import { Button } from "@calcom/ui";
 | 
				
			||||||
 | 
					import { Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui/Dialog";
 | 
				
			||||||
 | 
					import { Form, TextField } from "@calcom/ui/form/fields";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { HttpError } from "@lib/core/http/error";
 | 
				
			||||||
 | 
					import { trpc } from "@lib/trpc";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function NewScheduleButton({ name = "new-schedule" }: { name?: string }) {
 | 
				
			||||||
 | 
					  const router = useRouter();
 | 
				
			||||||
 | 
					  const { t } = useLocale();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const form = useForm<{
 | 
				
			||||||
 | 
					    name: string;
 | 
				
			||||||
 | 
					  }>();
 | 
				
			||||||
 | 
					  const { register } = form;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const createMutation = trpc.useMutation("viewer.availability.schedule.create", {
 | 
				
			||||||
 | 
					    onSuccess: async ({ schedule }) => {
 | 
				
			||||||
 | 
					      await router.push("/availability/" + schedule.id);
 | 
				
			||||||
 | 
					      showToast(t("schedule_created_successfully", { scheduleName: schedule.name }), "success");
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    onError: (err) => {
 | 
				
			||||||
 | 
					      if (err instanceof HttpError) {
 | 
				
			||||||
 | 
					        const message = `${err.statusCode}: ${err.message}`;
 | 
				
			||||||
 | 
					        showToast(message, "error");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (err.data?.code === "UNAUTHORIZED") {
 | 
				
			||||||
 | 
					        const message = `${err.data.code}: You are not able to create this event`;
 | 
				
			||||||
 | 
					        showToast(message, "error");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Dialog name={name} clearQueryParamsOnClose={["copy-schedule-id"]}>
 | 
				
			||||||
 | 
					      <DialogTrigger asChild>
 | 
				
			||||||
 | 
					        <Button data-testid={name} StartIcon={PlusIcon}>
 | 
				
			||||||
 | 
					          {t("new_schedule_btn")}
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </DialogTrigger>
 | 
				
			||||||
 | 
					      <DialogContent>
 | 
				
			||||||
 | 
					        <div className="mb-4">
 | 
				
			||||||
 | 
					          <h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
 | 
				
			||||||
 | 
					            {t("add_new_schedule")}
 | 
				
			||||||
 | 
					          </h3>
 | 
				
			||||||
 | 
					          <div>
 | 
				
			||||||
 | 
					            <p className="text-sm text-gray-500">{t("new_event_type_to_book_description")}</p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <Form
 | 
				
			||||||
 | 
					          form={form}
 | 
				
			||||||
 | 
					          handleSubmit={(values) => {
 | 
				
			||||||
 | 
					            createMutation.mutate(values);
 | 
				
			||||||
 | 
					          }}>
 | 
				
			||||||
 | 
					          <div className="mt-3 space-y-4">
 | 
				
			||||||
 | 
					            <TextField label={t("name")} {...register("name")} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div className="mt-8 flex flex-row-reverse gap-x-2">
 | 
				
			||||||
 | 
					            <Button type="submit" loading={createMutation.isLoading}>
 | 
				
			||||||
 | 
					              {t("continue")}
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					            <DialogClose asChild>
 | 
				
			||||||
 | 
					              <Button color="secondary">{t("cancel")}</Button>
 | 
				
			||||||
 | 
					            </DialogClose>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </Form>
 | 
				
			||||||
 | 
					      </DialogContent>
 | 
				
			||||||
 | 
					    </Dialog>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -147,7 +147,7 @@ const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <fieldset className="flex min-h-[86px] flex-col justify-between space-y-2 py-5 sm:flex-row sm:space-y-0">
 | 
					    <fieldset className="flex flex-col justify-between space-y-2 py-5 sm:flex-row sm:space-y-0">
 | 
				
			||||||
      <div className="w-1/3">
 | 
					      <div className="w-1/3">
 | 
				
			||||||
        <label className="flex items-center space-x-2 rtl:space-x-reverse">
 | 
					        <label className="flex items-center space-x-2 rtl:space-x-reverse">
 | 
				
			||||||
          <input
 | 
					          <input
 | 
				
			||||||
							
								
								
									
										34
									
								
								apps/web/components/ui/EditableHeading.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								apps/web/components/ui/EditableHeading.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					import { PencilIcon } from "@heroicons/react/solid";
 | 
				
			||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const EditableHeading = ({ title, onChange }: { title: string; onChange: (value: string) => void }) => {
 | 
				
			||||||
 | 
					  const [editIcon, setEditIcon] = useState(true);
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="group relative cursor-pointer" onClick={() => setEditIcon(false)}>
 | 
				
			||||||
 | 
					      {editIcon ? (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          <h1
 | 
				
			||||||
 | 
					            style={{ fontSize: 22, letterSpacing: "-0.0009em" }}
 | 
				
			||||||
 | 
					            className="inline pl-0 text-gray-900 focus:text-black group-hover:text-gray-500">
 | 
				
			||||||
 | 
					            {title}
 | 
				
			||||||
 | 
					          </h1>
 | 
				
			||||||
 | 
					          <PencilIcon className="ml-1 -mt-1 inline h-4 w-4 text-gray-700 group-hover:text-gray-500" />
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      ) : (
 | 
				
			||||||
 | 
					        <div style={{ marginBottom: -11 }}>
 | 
				
			||||||
 | 
					          <input
 | 
				
			||||||
 | 
					            type="text"
 | 
				
			||||||
 | 
					            autoFocus
 | 
				
			||||||
 | 
					            style={{ top: -6, fontSize: 22 }}
 | 
				
			||||||
 | 
					            required
 | 
				
			||||||
 | 
					            className="relative h-10 w-full cursor-pointer border-none bg-transparent pl-0 text-gray-900 hover:text-gray-700 focus:text-black focus:outline-none focus:ring-0"
 | 
				
			||||||
 | 
					            defaultValue={title}
 | 
				
			||||||
 | 
					            onChange={(e) => onChange(e.target.value)}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default EditableHeading;
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1 @@
 | 
				
			||||||
// By default starts on Sunday (Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
 | 
					export * from "@calcom/lib/weekday";
 | 
				
			||||||
export function weekdayNames(locale: string | string[], weekStart = 0, type: "short" | "long" = "long") {
 | 
					 | 
				
			||||||
  return Array.from(Array(7).keys()).map((d) => nameOfDay(locale, d + weekStart, type));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function nameOfDay(locale: string | string[], day: number, type: "short" | "long" = "long") {
 | 
					 | 
				
			||||||
  return new Intl.DateTimeFormat(locale, { weekday: type }).format(new Date(1970, 0, day + 4));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,6 +43,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
 | 
				
			||||||
    periodDays: true,
 | 
					    periodDays: true,
 | 
				
			||||||
    periodCountCalendarDays: true,
 | 
					    periodCountCalendarDays: true,
 | 
				
			||||||
    schedulingType: true,
 | 
					    schedulingType: true,
 | 
				
			||||||
 | 
					    schedule: {
 | 
				
			||||||
 | 
					      select: {
 | 
				
			||||||
 | 
					        availability: true,
 | 
				
			||||||
 | 
					        timeZone: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    minimumBookingNotice: true,
 | 
					    minimumBookingNotice: true,
 | 
				
			||||||
    beforeEventBuffer: true,
 | 
					    beforeEventBuffer: true,
 | 
				
			||||||
    afterEventBuffer: true,
 | 
					    afterEventBuffer: true,
 | 
				
			||||||
| 
						 | 
					@ -80,6 +86,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
 | 
				
			||||||
      hideBranding: true,
 | 
					      hideBranding: true,
 | 
				
			||||||
      brandColor: true,
 | 
					      brandColor: true,
 | 
				
			||||||
      darkBrandColor: true,
 | 
					      darkBrandColor: true,
 | 
				
			||||||
 | 
					      defaultScheduleId: true,
 | 
				
			||||||
 | 
					      schedules: {
 | 
				
			||||||
 | 
					        select: {
 | 
				
			||||||
 | 
					          availability: true,
 | 
				
			||||||
 | 
					          timeZone: true,
 | 
				
			||||||
 | 
					          id: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      theme: true,
 | 
					      theme: true,
 | 
				
			||||||
      plan: true,
 | 
					      plan: true,
 | 
				
			||||||
      eventTypes: {
 | 
					      eventTypes: {
 | 
				
			||||||
| 
						 | 
					@ -175,13 +189,24 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
 | 
				
			||||||
    periodEndDate: eventType.periodEndDate?.toString() ?? null,
 | 
					    periodEndDate: eventType.periodEndDate?.toString() ?? null,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const schedule = eventType.schedule
 | 
				
			||||||
 | 
					    ? { ...eventType.schedule }
 | 
				
			||||||
 | 
					    : {
 | 
				
			||||||
 | 
					        ...user.schedules.filter(
 | 
				
			||||||
 | 
					          (schedule) => !user.defaultScheduleId || schedule.id === user.defaultScheduleId
 | 
				
			||||||
 | 
					        )[0],
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeZone = schedule.timeZone || eventType.timeZone || user.timeZone;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const workingHours = getWorkingHours(
 | 
					  const workingHours = getWorkingHours(
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      timeZone: eventType.timeZone || user.timeZone,
 | 
					      timeZone,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    eventType.availability.length ? eventType.availability : user.availability
 | 
					    schedule.availability || (eventType.availability.length ? eventType.availability : user.availability)
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  eventTypeObject.schedule = null;
 | 
				
			||||||
  eventTypeObject.availability = [];
 | 
					  eventTypeObject.availability = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,6 +36,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
 | 
				
			||||||
      startTime: true,
 | 
					      startTime: true,
 | 
				
			||||||
      endTime: true,
 | 
					      endTime: true,
 | 
				
			||||||
      selectedCalendars: true,
 | 
					      selectedCalendars: true,
 | 
				
			||||||
 | 
					      schedules: {
 | 
				
			||||||
 | 
					        select: {
 | 
				
			||||||
 | 
					          availability: true,
 | 
				
			||||||
 | 
					          timeZone: true,
 | 
				
			||||||
 | 
					          id: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      defaultScheduleId: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,6 +52,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
 | 
				
			||||||
      where: { id },
 | 
					      where: { id },
 | 
				
			||||||
      select: {
 | 
					      select: {
 | 
				
			||||||
        timeZone: true,
 | 
					        timeZone: true,
 | 
				
			||||||
 | 
					        schedule: {
 | 
				
			||||||
 | 
					          select: {
 | 
				
			||||||
 | 
					            availability: true,
 | 
				
			||||||
 | 
					            timeZone: true,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        availability: {
 | 
					        availability: {
 | 
				
			||||||
          select: {
 | 
					          select: {
 | 
				
			||||||
            startTime: true,
 | 
					            startTime: true,
 | 
				
			||||||
| 
						 | 
					@ -76,10 +90,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
 | 
				
			||||||
    end: dayjs(a.end).add(currentUser.bufferTime, "minute").toString(),
 | 
					    end: dayjs(a.end).add(currentUser.bufferTime, "minute").toString(),
 | 
				
			||||||
  }));
 | 
					  }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const timeZone = eventType?.timeZone || currentUser.timeZone;
 | 
					  const schedule = eventType?.schedule
 | 
				
			||||||
 | 
					    ? { ...eventType?.schedule }
 | 
				
			||||||
 | 
					    : {
 | 
				
			||||||
 | 
					        ...currentUser.schedules.filter(
 | 
				
			||||||
 | 
					          (schedule) => !currentUser.defaultScheduleId || schedule.id === currentUser.defaultScheduleId
 | 
				
			||||||
 | 
					        )[0],
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeZone = schedule.timeZone || eventType?.timeZone || currentUser.timeZone;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const workingHours = getWorkingHours(
 | 
					  const workingHours = getWorkingHours(
 | 
				
			||||||
    { timeZone },
 | 
					    {
 | 
				
			||||||
    eventType?.availability.length ? eventType.availability : currentUser.availability
 | 
					      timeZone,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    schedule.availability ||
 | 
				
			||||||
 | 
					      (eventType?.availability.length ? eventType.availability : currentUser.availability)
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  res.status(200).json({
 | 
					  res.status(200).json({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										154
									
								
								apps/web/pages/availability/[schedule].tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								apps/web/pages/availability/[schedule].tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,154 @@
 | 
				
			||||||
 | 
					import { BadgeCheckIcon } from "@heroicons/react/solid";
 | 
				
			||||||
 | 
					import { useRouter } from "next/router";
 | 
				
			||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import { Controller, useForm } from "react-hook-form";
 | 
				
			||||||
 | 
					import TimezoneSelect from "react-timezone-select";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { DEFAULT_SCHEDULE, availabilityAsString } from "@calcom/lib/availability";
 | 
				
			||||||
 | 
					import showToast from "@calcom/lib/notification";
 | 
				
			||||||
 | 
					import Button from "@calcom/ui/Button";
 | 
				
			||||||
 | 
					import Switch from "@calcom/ui/Switch";
 | 
				
			||||||
 | 
					import { Form } from "@calcom/ui/form/fields";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { QueryCell } from "@lib/QueryCell";
 | 
				
			||||||
 | 
					import { HttpError } from "@lib/core/http/error";
 | 
				
			||||||
 | 
					import { useLocale } from "@lib/hooks/useLocale";
 | 
				
			||||||
 | 
					import { inferQueryOutput, trpc } from "@lib/trpc";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Shell from "@components/Shell";
 | 
				
			||||||
 | 
					import Schedule from "@components/availability/Schedule";
 | 
				
			||||||
 | 
					import EditableHeading from "@components/ui/EditableHeading";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function AvailabilityForm(props: inferQueryOutput<"viewer.availability.schedule">) {
 | 
				
			||||||
 | 
					  const { t } = useLocale();
 | 
				
			||||||
 | 
					  const router = useRouter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const form = useForm({
 | 
				
			||||||
 | 
					    defaultValues: {
 | 
				
			||||||
 | 
					      schedule: props.availability || DEFAULT_SCHEDULE,
 | 
				
			||||||
 | 
					      isDefault: !!props.isDefault,
 | 
				
			||||||
 | 
					      timeZone: props.timeZone,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const updateMutation = trpc.useMutation("viewer.availability.schedule.update", {
 | 
				
			||||||
 | 
					    onSuccess: async () => {
 | 
				
			||||||
 | 
					      await router.push("/availability");
 | 
				
			||||||
 | 
					      window.location.reload();
 | 
				
			||||||
 | 
					      showToast(t("availability_updated_successfully"), "success");
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    onError: (err) => {
 | 
				
			||||||
 | 
					      if (err instanceof HttpError) {
 | 
				
			||||||
 | 
					        const message = `${err.statusCode}: ${err.message}`;
 | 
				
			||||||
 | 
					        showToast(message, "error");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Form
 | 
				
			||||||
 | 
					      form={form}
 | 
				
			||||||
 | 
					      handleSubmit={async (values) => {
 | 
				
			||||||
 | 
					        updateMutation.mutate({
 | 
				
			||||||
 | 
					          scheduleId: parseInt(router.query.schedule as string, 10),
 | 
				
			||||||
 | 
					          name: props.schedule.name,
 | 
				
			||||||
 | 
					          ...values,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      className="grid grid-cols-3 gap-2">
 | 
				
			||||||
 | 
					      <div className="col-span-3 space-y-2 lg:col-span-2">
 | 
				
			||||||
 | 
					        <div className="divide-y rounded-sm border border-gray-200 bg-white px-4 py-5 sm:p-6">
 | 
				
			||||||
 | 
					          <h3 className="mb-5 text-base font-medium leading-6 text-gray-900">{t("change_start_end")}</h3>
 | 
				
			||||||
 | 
					          <Schedule name="schedule" />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div className="space-x-2 text-right">
 | 
				
			||||||
 | 
					          <Button color="secondary" href="/availability" tabIndex={-1}>
 | 
				
			||||||
 | 
					            {t("cancel")}
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					          <Button>{t("save")}</Button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div className="min-w-40 col-span-3 ml-2 space-y-2 lg:col-span-1">
 | 
				
			||||||
 | 
					        {props.isDefault ? (
 | 
				
			||||||
 | 
					          <div className="inline-block rounded border border-gray-300 bg-gray-200 px-2 py-0.5 pl-1.5 text-sm font-medium text-neutral-800">
 | 
				
			||||||
 | 
					            <span className="flex items-center">
 | 
				
			||||||
 | 
					              <BadgeCheckIcon className="mr-1 h-4 w-4" /> {t("default")}
 | 
				
			||||||
 | 
					            </span>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        ) : (
 | 
				
			||||||
 | 
					          <Controller
 | 
				
			||||||
 | 
					            name="isDefault"
 | 
				
			||||||
 | 
					            render={({ field: { onChange, value } }) => (
 | 
				
			||||||
 | 
					              <Switch label={t("set_to_default")} onCheckedChange={onChange} checked={value} />
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
 | 
				
			||||||
 | 
					            {t("timezone")}
 | 
				
			||||||
 | 
					          </label>
 | 
				
			||||||
 | 
					          <div className="mt-1">
 | 
				
			||||||
 | 
					            <Controller
 | 
				
			||||||
 | 
					              name="timeZone"
 | 
				
			||||||
 | 
					              render={({ field: { onChange, value } }) => (
 | 
				
			||||||
 | 
					                <TimezoneSelect
 | 
				
			||||||
 | 
					                  value={value}
 | 
				
			||||||
 | 
					                  className="focus:border-brand mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:ring-black sm:text-sm"
 | 
				
			||||||
 | 
					                  onChange={(timezone) => onChange(timezone.value)}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div className="mt-2 rounded-sm border border-gray-200 px-4 py-5 sm:p-6 ">
 | 
				
			||||||
 | 
					          <h3 className="text-base font-medium leading-6 text-gray-900">
 | 
				
			||||||
 | 
					            {t("something_doesnt_look_right")}
 | 
				
			||||||
 | 
					          </h3>
 | 
				
			||||||
 | 
					          <div className="mt-2 max-w-xl text-sm text-gray-500">
 | 
				
			||||||
 | 
					            <p>{t("troubleshoot_availability")}</p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div className="mt-5">
 | 
				
			||||||
 | 
					            <Button href="/availability/troubleshoot" color="secondary">
 | 
				
			||||||
 | 
					              {t("launch_troubleshooter")}
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </Form>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Availability() {
 | 
				
			||||||
 | 
					  const router = useRouter();
 | 
				
			||||||
 | 
					  const { i18n } = useLocale();
 | 
				
			||||||
 | 
					  const query = trpc.useQuery([
 | 
				
			||||||
 | 
					    "viewer.availability.schedule",
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      scheduleId: parseInt(router.query.schedule as string),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ]);
 | 
				
			||||||
 | 
					  const [name, setName] = useState<string>();
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					      <QueryCell
 | 
				
			||||||
 | 
					        query={query}
 | 
				
			||||||
 | 
					        success={({ data }) => {
 | 
				
			||||||
 | 
					          return (
 | 
				
			||||||
 | 
					            <Shell
 | 
				
			||||||
 | 
					              heading={<EditableHeading title={data.schedule.name} onChange={setName} />}
 | 
				
			||||||
 | 
					              subtitle={data.schedule.availability.map((availability) => (
 | 
				
			||||||
 | 
					                <>
 | 
				
			||||||
 | 
					                  {availabilityAsString(availability, i18n.language)}
 | 
				
			||||||
 | 
					                  <br />
 | 
				
			||||||
 | 
					                </>
 | 
				
			||||||
 | 
					              ))}>
 | 
				
			||||||
 | 
					              <AvailabilityForm
 | 
				
			||||||
 | 
					                {...{ ...data, schedule: { ...data.schedule, name: name || data.schedule.name } }}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </Shell>
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,139 +1,115 @@
 | 
				
			||||||
import { zodResolver } from "@hookform/resolvers/zod";
 | 
					import { ClockIcon } from "@heroicons/react/outline";
 | 
				
			||||||
import dayjs from "dayjs";
 | 
					import { DotsHorizontalIcon, TrashIcon } from "@heroicons/react/solid";
 | 
				
			||||||
import timezone from "dayjs/plugin/timezone";
 | 
					import { Availability } from "@prisma/client";
 | 
				
			||||||
import utc from "dayjs/plugin/utc";
 | 
					import Link from "next/link";
 | 
				
			||||||
import { useForm } from "react-hook-form";
 | 
					 | 
				
			||||||
import { z } from "zod";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { availabilityAsString } from "@calcom/lib/availability";
 | 
				
			||||||
 | 
					import { useLocale } from "@calcom/lib/hooks/useLocale";
 | 
				
			||||||
import showToast from "@calcom/lib/notification";
 | 
					import showToast from "@calcom/lib/notification";
 | 
				
			||||||
import { Alert } from "@calcom/ui/Alert";
 | 
					import { Button } from "@calcom/ui";
 | 
				
			||||||
import Button from "@calcom/ui/Button";
 | 
					import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
 | 
				
			||||||
import { Form } from "@calcom/ui/form/fields";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { QueryCell } from "@lib/QueryCell";
 | 
					import { QueryCell } from "@lib/QueryCell";
 | 
				
			||||||
import { DEFAULT_SCHEDULE } from "@lib/availability";
 | 
					import { HttpError } from "@lib/core/http/error";
 | 
				
			||||||
import { useLocale } from "@lib/hooks/useLocale";
 | 
					 | 
				
			||||||
import { inferQueryOutput, trpc } from "@lib/trpc";
 | 
					import { inferQueryOutput, trpc } from "@lib/trpc";
 | 
				
			||||||
import { Schedule as ScheduleType } from "@lib/types/schedule";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Shell from "@components/Shell";
 | 
					import Shell from "@components/Shell";
 | 
				
			||||||
import Schedule from "@components/ui/form/Schedule";
 | 
					import { NewScheduleButton } from "@components/availability/NewScheduleButton";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dayjs.extend(utc);
 | 
					const CreateFirstScheduleView = () => {
 | 
				
			||||||
dayjs.extend(timezone);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type FormValues = {
 | 
					 | 
				
			||||||
  schedule: ScheduleType;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function AvailabilityForm(props: inferQueryOutput<"viewer.availability">) {
 | 
					 | 
				
			||||||
  const { t } = useLocale();
 | 
					  const { t } = useLocale();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const createSchedule = async ({ schedule }: FormValues) => {
 | 
					  return (
 | 
				
			||||||
    const res = await fetch(`/api/schedule`, {
 | 
					    <div className="md:py-20">
 | 
				
			||||||
      method: "POST",
 | 
					      <div className="mx-auto block text-center md:max-w-screen-sm">
 | 
				
			||||||
      body: JSON.stringify({ schedule, timeZone: props.timeZone }),
 | 
					        <ClockIcon className="inline w-12 text-neutral-400" />
 | 
				
			||||||
      headers: {
 | 
					        <h3 className="mt-2 text-xl font-bold text-neutral-900">{t("new_schedule_heading")}</h3>
 | 
				
			||||||
        "Content-Type": "application/json",
 | 
					        <p className="text-md mt-1 mb-2 text-neutral-600">{t("new_schedule_description")}</p>
 | 
				
			||||||
      },
 | 
					        <NewScheduleButton name="first-new-schedule" />
 | 
				
			||||||
    });
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
    if (!res.ok) {
 | 
					  );
 | 
				
			||||||
      throw new Error((await res.json()).message);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    const responseData = await res.json();
 | 
					 | 
				
			||||||
    showToast(t("availability_updated_successfully"), "success");
 | 
					 | 
				
			||||||
    return responseData.data;
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const schema = z.object({
 | 
					export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availability.list">) {
 | 
				
			||||||
    schedule: z
 | 
					  const { t, i18n } = useLocale();
 | 
				
			||||||
      .object({
 | 
					  const deleteMutation = trpc.useMutation("viewer.availability.schedule.delete", {
 | 
				
			||||||
        start: z.date(),
 | 
					    onSuccess: async () => {
 | 
				
			||||||
        end: z.date(),
 | 
					      showToast(t("schedule_deleted_successfully"), "success");
 | 
				
			||||||
      })
 | 
					      window.location.reload();
 | 
				
			||||||
      .superRefine((val, ctx) => {
 | 
					 | 
				
			||||||
        if (dayjs(val.end).isBefore(dayjs(val.start))) {
 | 
					 | 
				
			||||||
          ctx.addIssue({
 | 
					 | 
				
			||||||
            code: z.ZodIssueCode.custom,
 | 
					 | 
				
			||||||
            message: "Invalid entry: End time can not be before start time",
 | 
					 | 
				
			||||||
            path: ["end"],
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .optional()
 | 
					 | 
				
			||||||
      .array()
 | 
					 | 
				
			||||||
      .array(),
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const days = [
 | 
					 | 
				
			||||||
    t("sunday_time_error"),
 | 
					 | 
				
			||||||
    t("monday_time_error"),
 | 
					 | 
				
			||||||
    t("tuesday_time_error"),
 | 
					 | 
				
			||||||
    t("wednesday_time_error"),
 | 
					 | 
				
			||||||
    t("thursday_time_error"),
 | 
					 | 
				
			||||||
    t("friday_time_error"),
 | 
					 | 
				
			||||||
    t("saturday_time_error"),
 | 
					 | 
				
			||||||
  ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const form = useForm({
 | 
					 | 
				
			||||||
    defaultValues: {
 | 
					 | 
				
			||||||
      schedule: props.schedule || DEFAULT_SCHEDULE,
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    resolver: zodResolver(schema),
 | 
					    onError: (err) => {
 | 
				
			||||||
  });
 | 
					      if (err instanceof HttpError) {
 | 
				
			||||||
 | 
					        const message = `${err.statusCode}: ${err.message}`;
 | 
				
			||||||
  return (
 | 
					        showToast(message, "error");
 | 
				
			||||||
    <div className="grid grid-cols-3 gap-2">
 | 
					 | 
				
			||||||
      <Form
 | 
					 | 
				
			||||||
        form={form}
 | 
					 | 
				
			||||||
        handleSubmit={async (values) => {
 | 
					 | 
				
			||||||
          await createSchedule(values);
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
        className="col-span-3 space-y-2 lg:col-span-2">
 | 
					 | 
				
			||||||
        <div className="divide-y rounded-sm border border-gray-200 bg-white px-4 py-5 sm:p-6">
 | 
					 | 
				
			||||||
          <h3 className="mb-5 text-base font-medium leading-6 text-gray-900">{t("change_start_end")}</h3>
 | 
					 | 
				
			||||||
          <Schedule name="schedule" />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        {form.formState.errors.schedule && (
 | 
					 | 
				
			||||||
          <Alert
 | 
					 | 
				
			||||||
            className="mt-1"
 | 
					 | 
				
			||||||
            severity="error"
 | 
					 | 
				
			||||||
            message={
 | 
					 | 
				
			||||||
              days[form.formState.errors.schedule.length - 1] + " : " + t("error_end_time_before_start_time")
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
          />
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className="-mx-4 mb-16 overflow-hidden rounded-sm border border-gray-200 bg-white sm:mx-0">
 | 
				
			||||||
 | 
					      {schedules.length === 0 && <CreateFirstScheduleView />}
 | 
				
			||||||
 | 
					      <ul className="divide-y divide-neutral-200" data-testid="schedules">
 | 
				
			||||||
 | 
					        {schedules.map((schedule) => (
 | 
				
			||||||
 | 
					          <li key={schedule.id}>
 | 
				
			||||||
 | 
					            <div className="flex items-center justify-between py-5 hover:bg-neutral-50">
 | 
				
			||||||
 | 
					              <div className="group flex w-full items-center justify-between hover:bg-neutral-50 sm:px-6">
 | 
				
			||||||
 | 
					                <Link href={"/availability/" + schedule.id}>
 | 
				
			||||||
 | 
					                  <a className="flex-grow truncate text-sm" title={schedule.name}>
 | 
				
			||||||
 | 
					                    <div>
 | 
				
			||||||
 | 
					                      <span className="truncate font-medium text-neutral-900">{schedule.name}</span>
 | 
				
			||||||
 | 
					                      {schedule.isDefault && (
 | 
				
			||||||
 | 
					                        <span className="ml-2 inline items-center rounded-sm bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-800">
 | 
				
			||||||
 | 
					                          {t("default")}
 | 
				
			||||||
 | 
					                        </span>
 | 
				
			||||||
                      )}
 | 
					                      )}
 | 
				
			||||||
        <div className="text-right">
 | 
					 | 
				
			||||||
          <Button>{t("save")}</Button>
 | 
					 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
      </Form>
 | 
					                    <p className="mt-1 text-xs text-neutral-500">
 | 
				
			||||||
      <div className="min-w-40 col-span-3 ltr:ml-2 rtl:mr-2 lg:col-span-1">
 | 
					                      {schedule.availability.map((availability: Availability) => (
 | 
				
			||||||
        <div className="rounded-sm border border-gray-200 px-4 py-5 sm:p-6 ">
 | 
					                        <>
 | 
				
			||||||
          <h3 className="text-base font-medium leading-6 text-gray-900">
 | 
					                          {availabilityAsString(availability, i18n.language)}
 | 
				
			||||||
            {t("something_doesnt_look_right")}
 | 
					                          <br />
 | 
				
			||||||
          </h3>
 | 
					                        </>
 | 
				
			||||||
          <div className="mt-2 max-w-xl text-sm text-gray-500">
 | 
					                      ))}
 | 
				
			||||||
            <p>{t("troubleshoot_availability")}</p>
 | 
					                    </p>
 | 
				
			||||||
 | 
					                  </a>
 | 
				
			||||||
 | 
					                </Link>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
          <div className="mt-5">
 | 
					              <Dropdown>
 | 
				
			||||||
            <Button href="/availability/troubleshoot" color="secondary">
 | 
					                <DropdownMenuTrigger className="group mr-5 h-10 w-10 border border-transparent p-0 text-neutral-400 hover:border-gray-200">
 | 
				
			||||||
              {t("launch_troubleshooter")}
 | 
					                  <DotsHorizontalIcon className="h-5 w-5 group-hover:text-gray-800" />
 | 
				
			||||||
 | 
					                </DropdownMenuTrigger>
 | 
				
			||||||
 | 
					                <DropdownMenuContent>
 | 
				
			||||||
 | 
					                  <DropdownMenuItem>
 | 
				
			||||||
 | 
					                    <Button
 | 
				
			||||||
 | 
					                      onClick={() =>
 | 
				
			||||||
 | 
					                        deleteMutation.mutate({
 | 
				
			||||||
 | 
					                          scheduleId: schedule.id,
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                      type="button"
 | 
				
			||||||
 | 
					                      color="minimal"
 | 
				
			||||||
 | 
					                      className="w-full font-normal"
 | 
				
			||||||
 | 
					                      StartIcon={TrashIcon}>
 | 
				
			||||||
 | 
					                      {t("delete_schedule")}
 | 
				
			||||||
                    </Button>
 | 
					                    </Button>
 | 
				
			||||||
 | 
					                  </DropdownMenuItem>
 | 
				
			||||||
 | 
					                </DropdownMenuContent>
 | 
				
			||||||
 | 
					              </Dropdown>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        </div>
 | 
					          </li>
 | 
				
			||||||
      </div>
 | 
					        ))}
 | 
				
			||||||
 | 
					      </ul>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Availability() {
 | 
					export default function AvailabilityPage() {
 | 
				
			||||||
  const { t } = useLocale();
 | 
					  const { t } = useLocale();
 | 
				
			||||||
  const query = trpc.useQuery(["viewer.availability"]);
 | 
					  const query = trpc.useQuery(["viewer.availability.list"]);
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
      <Shell heading={t("availability")} subtitle={t("configure_availability")}>
 | 
					      <Shell heading={t("availability")} subtitle={t("configure_availability")} CTA={<NewScheduleButton />}>
 | 
				
			||||||
        <QueryCell query={query} success={({ data }) => <AvailabilityForm {...data} />} />
 | 
					        <QueryCell query={query} success={({ data }) => <AvailabilityList {...data} />} />
 | 
				
			||||||
      </Shell>
 | 
					      </Shell>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,28 +14,32 @@ import {
 | 
				
			||||||
} from "@heroicons/react/solid";
 | 
					} from "@heroicons/react/solid";
 | 
				
			||||||
import { zodResolver } from "@hookform/resolvers/zod";
 | 
					import { zodResolver } from "@hookform/resolvers/zod";
 | 
				
			||||||
import { MembershipRole } from "@prisma/client";
 | 
					import { MembershipRole } from "@prisma/client";
 | 
				
			||||||
import { Availability, EventTypeCustomInput, PeriodType, Prisma, SchedulingType } from "@prisma/client";
 | 
					import { EventTypeCustomInput, PeriodType, Prisma, SchedulingType } from "@prisma/client";
 | 
				
			||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
 | 
					import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
 | 
				
			||||||
import * as RadioGroup from "@radix-ui/react-radio-group";
 | 
					import * as RadioGroup from "@radix-ui/react-radio-group";
 | 
				
			||||||
 | 
					import classNames from "classnames";
 | 
				
			||||||
import dayjs from "dayjs";
 | 
					import dayjs from "dayjs";
 | 
				
			||||||
import timezone from "dayjs/plugin/timezone";
 | 
					import timezone from "dayjs/plugin/timezone";
 | 
				
			||||||
import utc from "dayjs/plugin/utc";
 | 
					import utc from "dayjs/plugin/utc";
 | 
				
			||||||
import { GetServerSidePropsContext } from "next";
 | 
					import { GetServerSidePropsContext } from "next";
 | 
				
			||||||
 | 
					import Link from "next/link";
 | 
				
			||||||
import { useRouter } from "next/router";
 | 
					import { useRouter } from "next/router";
 | 
				
			||||||
import React, { useEffect, useState } from "react";
 | 
					import React, { useEffect, useState } from "react";
 | 
				
			||||||
import { Controller, useForm } from "react-hook-form";
 | 
					import { Controller, useForm } from "react-hook-form";
 | 
				
			||||||
import { FormattedNumber, IntlProvider } from "react-intl";
 | 
					import { FormattedNumber, IntlProvider } from "react-intl";
 | 
				
			||||||
import Select from "react-select";
 | 
					import Select, { Props as SelectProps } from "react-select";
 | 
				
			||||||
import { JSONObject } from "superjson/dist/types";
 | 
					import { JSONObject } from "superjson/dist/types";
 | 
				
			||||||
import { z } from "zod";
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import showToast from "@calcom/lib/notification";
 | 
					import showToast from "@calcom/lib/notification";
 | 
				
			||||||
import { StripeData } from "@calcom/stripe/server";
 | 
					import { StripeData } from "@calcom/stripe/server";
 | 
				
			||||||
 | 
					import { Alert } from "@calcom/ui/Alert";
 | 
				
			||||||
import Button from "@calcom/ui/Button";
 | 
					import Button from "@calcom/ui/Button";
 | 
				
			||||||
import { Dialog, DialogContent, DialogTrigger } from "@calcom/ui/Dialog";
 | 
					import { Dialog, DialogContent, DialogTrigger } from "@calcom/ui/Dialog";
 | 
				
			||||||
import Switch from "@calcom/ui/Switch";
 | 
					import Switch from "@calcom/ui/Switch";
 | 
				
			||||||
import { Form } from "@calcom/ui/form/fields";
 | 
					import { Form } from "@calcom/ui/form/fields";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { QueryCell } from "@lib/QueryCell";
 | 
				
			||||||
import { asStringOrThrow, asStringOrUndefined } from "@lib/asStringOrNull";
 | 
					import { asStringOrThrow, asStringOrUndefined } from "@lib/asStringOrNull";
 | 
				
			||||||
import { getSession } from "@lib/auth";
 | 
					import { getSession } from "@lib/auth";
 | 
				
			||||||
import { HttpError } from "@lib/core/http/error";
 | 
					import { HttpError } from "@lib/core/http/error";
 | 
				
			||||||
| 
						 | 
					@ -54,7 +58,6 @@ import Shell from "@components/Shell";
 | 
				
			||||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
 | 
					import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
 | 
				
			||||||
import CustomInputTypeForm from "@components/pages/eventtypes/CustomInputTypeForm";
 | 
					import CustomInputTypeForm from "@components/pages/eventtypes/CustomInputTypeForm";
 | 
				
			||||||
import InfoBadge from "@components/ui/InfoBadge";
 | 
					import InfoBadge from "@components/ui/InfoBadge";
 | 
				
			||||||
import { Scheduler } from "@components/ui/Scheduler";
 | 
					 | 
				
			||||||
import CheckboxField from "@components/ui/form/CheckboxField";
 | 
					import CheckboxField from "@components/ui/form/CheckboxField";
 | 
				
			||||||
import CheckedSelect from "@components/ui/form/CheckedSelect";
 | 
					import CheckedSelect from "@components/ui/form/CheckedSelect";
 | 
				
			||||||
import { DateRangePicker } from "@components/ui/form/DateRangePicker";
 | 
					import { DateRangePicker } from "@components/ui/form/DateRangePicker";
 | 
				
			||||||
| 
						 | 
					@ -77,7 +80,6 @@ interface NFT extends Token {
 | 
				
			||||||
  // Some OpenSea NFTs have several contracts
 | 
					  // Some OpenSea NFTs have several contracts
 | 
				
			||||||
  contracts: Array<Token>;
 | 
					  contracts: Array<Token>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
type AvailabilityInput = Pick<Availability, "days" | "startTime" | "endTime">;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
type OptionTypeBase = {
 | 
					type OptionTypeBase = {
 | 
				
			||||||
  label: string;
 | 
					  label: string;
 | 
				
			||||||
| 
						 | 
					@ -98,6 +100,41 @@ const addDefaultLocationOptions = (
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const AvailabilitySelect = ({ className, ...props }: SelectProps) => {
 | 
				
			||||||
 | 
					  const query = trpc.useQuery(["viewer.availability.list"]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <QueryCell
 | 
				
			||||||
 | 
					      query={query}
 | 
				
			||||||
 | 
					      success={({ data }) => {
 | 
				
			||||||
 | 
					        const options = data.schedules.map((schedule) => ({
 | 
				
			||||||
 | 
					          value: schedule.id,
 | 
				
			||||||
 | 
					          label: schedule.name,
 | 
				
			||||||
 | 
					        }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const value = options.find((option) =>
 | 
				
			||||||
 | 
					          props.value
 | 
				
			||||||
 | 
					            ? option.value === props.value
 | 
				
			||||||
 | 
					            : option.value === data.schedules.find((schedule) => schedule.isDefault)?.id
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <Select
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					            options={options}
 | 
				
			||||||
 | 
					            isSearchable={false}
 | 
				
			||||||
 | 
					            classNamePrefix="react-select"
 | 
				
			||||||
 | 
					            className={classNames(
 | 
				
			||||||
 | 
					              "react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm",
 | 
				
			||||||
 | 
					              className
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            value={value}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
 | 
					const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
 | 
				
			||||||
  const { t } = useLocale();
 | 
					  const { t } = useLocale();
 | 
				
			||||||
  const PERIOD_TYPES = [
 | 
					  const PERIOD_TYPES = [
 | 
				
			||||||
| 
						 | 
					@ -169,7 +206,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [editIcon, setEditIcon] = useState(true);
 | 
					  const [editIcon, setEditIcon] = useState(true);
 | 
				
			||||||
  const [showLocationModal, setShowLocationModal] = useState(false);
 | 
					  const [showLocationModal, setShowLocationModal] = useState(false);
 | 
				
			||||||
  const [selectedTimeZone, setSelectedTimeZone] = useState("");
 | 
					 | 
				
			||||||
  const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
 | 
					  const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
 | 
				
			||||||
  const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
 | 
					  const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
 | 
				
			||||||
  const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
 | 
					  const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
 | 
				
			||||||
| 
						 | 
					@ -185,11 +221,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
 | 
				
			||||||
  const [requirePayment, setRequirePayment] = useState(eventType.price > 0);
 | 
					  const [requirePayment, setRequirePayment] = useState(eventType.price > 0);
 | 
				
			||||||
  const [advancedSettingsVisible, setAdvancedSettingsVisible] = useState(false);
 | 
					  const [advancedSettingsVisible, setAdvancedSettingsVisible] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [availabilityState, setAvailabilityState] = useState<{
 | 
					 | 
				
			||||||
    openingHours: AvailabilityInput[];
 | 
					 | 
				
			||||||
    dateOverrides: AvailabilityInput[];
 | 
					 | 
				
			||||||
  }>({ openingHours: [], dateOverrides: [] });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    const fetchTokens = async () => {
 | 
					    const fetchTokens = async () => {
 | 
				
			||||||
      // Get a list of most popular ERC20s and ERC777s, combine them into a single list, set as tokensList
 | 
					      // Get a list of most popular ERC20s and ERC777s, combine them into a single list, set as tokensList
 | 
				
			||||||
| 
						 | 
					@ -225,10 +256,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
 | 
				
			||||||
    fetchTokens();
 | 
					    fetchTokens();
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    setSelectedTimeZone(eventType.timeZone || "");
 | 
					 | 
				
			||||||
  }, []);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  async function deleteEventTypeHandler(event: React.MouseEvent<HTMLElement, MouseEvent>) {
 | 
					  async function deleteEventTypeHandler(event: React.MouseEvent<HTMLElement, MouseEvent>) {
 | 
				
			||||||
    event.preventDefault();
 | 
					    event.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -383,11 +410,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
 | 
				
			||||||
    locations: { type: LocationType; address?: string; link?: string }[];
 | 
					    locations: { type: LocationType; address?: string; link?: string }[];
 | 
				
			||||||
    customInputs: EventTypeCustomInput[];
 | 
					    customInputs: EventTypeCustomInput[];
 | 
				
			||||||
    users: string[];
 | 
					    users: string[];
 | 
				
			||||||
    availability: {
 | 
					    schedule: number;
 | 
				
			||||||
      openingHours: AvailabilityInput[];
 | 
					 | 
				
			||||||
      dateOverrides: AvailabilityInput[];
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    timeZone: string;
 | 
					 | 
				
			||||||
    periodType: PeriodType;
 | 
					    periodType: PeriodType;
 | 
				
			||||||
    periodDays: number;
 | 
					    periodDays: number;
 | 
				
			||||||
    periodCountCalendarDays: "1" | "0";
 | 
					    periodCountCalendarDays: "1" | "0";
 | 
				
			||||||
| 
						 | 
					@ -403,6 +426,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
 | 
				
			||||||
  }>({
 | 
					  }>({
 | 
				
			||||||
    defaultValues: {
 | 
					    defaultValues: {
 | 
				
			||||||
      locations: eventType.locations || [],
 | 
					      locations: eventType.locations || [],
 | 
				
			||||||
 | 
					      schedule: eventType.schedule?.id,
 | 
				
			||||||
      periodDates: {
 | 
					      periodDates: {
 | 
				
			||||||
        startDate: periodDates.startDate,
 | 
					        startDate: periodDates.startDate,
 | 
				
			||||||
        endDate: periodDates.endDate,
 | 
					        endDate: periodDates.endDate,
 | 
				
			||||||
| 
						 | 
					@ -748,7 +772,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
 | 
				
			||||||
                    updateMutation.mutate({
 | 
					                    updateMutation.mutate({
 | 
				
			||||||
                      ...input,
 | 
					                      ...input,
 | 
				
			||||||
                      locations,
 | 
					                      locations,
 | 
				
			||||||
                      availability: availabilityState,
 | 
					 | 
				
			||||||
                      periodStartDate: periodDates.startDate,
 | 
					                      periodStartDate: periodDates.startDate,
 | 
				
			||||||
                      periodEndDate: periodDates.endDate,
 | 
					                      periodEndDate: periodDates.endDate,
 | 
				
			||||||
                      periodCountCalendarDays: periodCountCalendarDays === "1",
 | 
					                      periodCountCalendarDays: periodCountCalendarDays === "1",
 | 
				
			||||||
| 
						 | 
					@ -1346,8 +1369,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
 | 
				
			||||||
                            </div>
 | 
					                            </div>
 | 
				
			||||||
                          </div>
 | 
					                          </div>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
 | 
					 | 
				
			||||||
                        <hr className="border-neutral-200" />
 | 
					 | 
				
			||||||
                        <div className="block sm:flex">
 | 
					                        <div className="block sm:flex">
 | 
				
			||||||
                          <div className="min-w-48 mb-4 sm:mb-0">
 | 
					                          <div className="min-w-48 mb-4 sm:mb-0">
 | 
				
			||||||
                            <label
 | 
					                            <label
 | 
				
			||||||
| 
						 | 
					@ -1358,33 +1379,27 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
 | 
				
			||||||
                          </div>
 | 
					                          </div>
 | 
				
			||||||
                          <div className="w-full">
 | 
					                          <div className="w-full">
 | 
				
			||||||
                            <Controller
 | 
					                            <Controller
 | 
				
			||||||
                              name="availability"
 | 
					                              name="schedule"
 | 
				
			||||||
                              control={formMethods.control}
 | 
					                              control={formMethods.control}
 | 
				
			||||||
                              render={() => (
 | 
					                              render={({ field }) => (
 | 
				
			||||||
                                <Scheduler
 | 
					                                <AvailabilitySelect
 | 
				
			||||||
                                  setAvailability={(val) => {
 | 
					                                  {...field}
 | 
				
			||||||
                                    const schedule = {
 | 
					                                  onChange={(selected: { label: string; value: number }) =>
 | 
				
			||||||
                                      openingHours: val.openingHours,
 | 
					                                    field.onChange(selected.value)
 | 
				
			||||||
                                      dateOverrides: val.dateOverrides,
 | 
					                                  }
 | 
				
			||||||
                                    };
 | 
					 | 
				
			||||||
                                    // Updating internal state that would be sent on mutation
 | 
					 | 
				
			||||||
                                    setAvailabilityState(schedule);
 | 
					 | 
				
			||||||
                                    // Updating form values displayed, but this one doesn't reach form submit scope
 | 
					 | 
				
			||||||
                                    formMethods.setValue("availability", schedule);
 | 
					 | 
				
			||||||
                                  }}
 | 
					 | 
				
			||||||
                                  setTimeZone={(timeZone) => {
 | 
					 | 
				
			||||||
                                    formMethods.setValue("timeZone", timeZone);
 | 
					 | 
				
			||||||
                                    setSelectedTimeZone(timeZone);
 | 
					 | 
				
			||||||
                                  }}
 | 
					 | 
				
			||||||
                                  timeZone={selectedTimeZone}
 | 
					 | 
				
			||||||
                                  availability={availability.map((schedule) => ({
 | 
					 | 
				
			||||||
                                    ...schedule,
 | 
					 | 
				
			||||||
                                    startTime: new Date(schedule.startTime),
 | 
					 | 
				
			||||||
                                    endTime: new Date(schedule.endTime),
 | 
					 | 
				
			||||||
                                  }))}
 | 
					 | 
				
			||||||
                                />
 | 
					                                />
 | 
				
			||||||
                              )}
 | 
					                              )}
 | 
				
			||||||
                            />
 | 
					                            />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            <Link href="/availability">
 | 
				
			||||||
 | 
					                              <a>
 | 
				
			||||||
 | 
					                                <Alert
 | 
				
			||||||
 | 
					                                  className="mt-1 text-xs"
 | 
				
			||||||
 | 
					                                  severity="info"
 | 
				
			||||||
 | 
					                                  message="You can manage your schedules on the Availability page."
 | 
				
			||||||
 | 
					                                />
 | 
				
			||||||
 | 
					                              </a>
 | 
				
			||||||
 | 
					                            </Link>
 | 
				
			||||||
                          </div>
 | 
					                          </div>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1802,6 +1817,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
 | 
				
			||||||
        select: userSelect,
 | 
					        select: userSelect,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      schedulingType: true,
 | 
					      schedulingType: true,
 | 
				
			||||||
 | 
					      schedule: {
 | 
				
			||||||
 | 
					        select: {
 | 
				
			||||||
 | 
					          id: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      userId: true,
 | 
					      userId: true,
 | 
				
			||||||
      price: true,
 | 
					      price: true,
 | 
				
			||||||
      currency: true,
 | 
					      currency: true,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
import { ArrowRightIcon } from "@heroicons/react/outline";
 | 
					import { ArrowRightIcon } from "@heroicons/react/outline";
 | 
				
			||||||
import { zodResolver } from "@hookform/resolvers/zod";
 | 
					import { zodResolver } from "@hookform/resolvers/zod";
 | 
				
			||||||
import { Prisma, IdentityProvider } from "@prisma/client";
 | 
					import { IdentityProvider, Prisma } from "@prisma/client";
 | 
				
			||||||
import classnames from "classnames";
 | 
					import classnames from "classnames";
 | 
				
			||||||
import dayjs from "dayjs";
 | 
					import dayjs from "dayjs";
 | 
				
			||||||
import localizedFormat from "dayjs/plugin/localizedFormat";
 | 
					import localizedFormat from "dayjs/plugin/localizedFormat";
 | 
				
			||||||
| 
						 | 
					@ -35,9 +35,9 @@ import { Schedule as ScheduleType } from "@lib/types/schedule";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { ClientSuspense } from "@components/ClientSuspense";
 | 
					import { ClientSuspense } from "@components/ClientSuspense";
 | 
				
			||||||
import Loader from "@components/Loader";
 | 
					import Loader from "@components/Loader";
 | 
				
			||||||
 | 
					import Schedule from "@components/availability/Schedule";
 | 
				
			||||||
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
 | 
					import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
 | 
				
			||||||
import Text from "@components/ui/Text";
 | 
					import Text from "@components/ui/Text";
 | 
				
			||||||
import Schedule from "@components/ui/form/Schedule";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import getEventTypes from "../lib/queries/event-types/get-event-types";
 | 
					import getEventTypes from "../lib/queries/event-types/get-event-types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -134,22 +134,12 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
 | 
				
			||||||
    return responseData.data;
 | 
					    return responseData.data;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const createSchedule = async ({ schedule }: ScheduleFormValues) => {
 | 
					  const createSchedule = trpc.useMutation("viewer.availability.schedule.create", {
 | 
				
			||||||
    const res = await fetch(`/api/schedule`, {
 | 
					    onError: (err) => {
 | 
				
			||||||
      method: "POST",
 | 
					      throw new Error(err.message);
 | 
				
			||||||
      body: JSON.stringify({ schedule }),
 | 
					 | 
				
			||||||
      headers: {
 | 
					 | 
				
			||||||
        "Content-Type": "application/json",
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!res.ok) {
 | 
					 | 
				
			||||||
      throw new Error((await res.json()).message);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    const responseData = await res.json();
 | 
					 | 
				
			||||||
    return responseData.data;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /** Name */
 | 
					  /** Name */
 | 
				
			||||||
  const nameRef = useRef<HTMLInputElement>(null);
 | 
					  const nameRef = useRef<HTMLInputElement>(null);
 | 
				
			||||||
  const usernameRef = useRef<HTMLInputElement>(null);
 | 
					  const usernameRef = useRef<HTMLInputElement>(null);
 | 
				
			||||||
| 
						 | 
					@ -444,7 +434,10 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
 | 
				
			||||||
          handleSubmit={async (values) => {
 | 
					          handleSubmit={async (values) => {
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
              setSubmitting(true);
 | 
					              setSubmitting(true);
 | 
				
			||||||
              await createSchedule({ ...values });
 | 
					              await createSchedule.mutate({
 | 
				
			||||||
 | 
					                name: t("default_schedule_name"),
 | 
				
			||||||
 | 
					                ...values,
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
              debouncedHandleConfirmStep();
 | 
					              debouncedHandleConfirmStep();
 | 
				
			||||||
              setSubmitting(false);
 | 
					              setSubmitting(false);
 | 
				
			||||||
            } catch (error) {
 | 
					            } catch (error) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -137,8 +137,6 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
 | 
				
			||||||
  const localeOptions = useMemo(() => {
 | 
					  const localeOptions = useMemo(() => {
 | 
				
			||||||
    return (router.locales || []).map((locale) => ({
 | 
					    return (router.locales || []).map((locale) => ({
 | 
				
			||||||
      value: locale,
 | 
					      value: locale,
 | 
				
			||||||
      // FIXME
 | 
					 | 
				
			||||||
      // @ts-ignore
 | 
					 | 
				
			||||||
      label: new Intl.DisplayNames(props.localeProp, { type: "language" }).of(locale) || "",
 | 
					      label: new Intl.DisplayNames(props.localeProp, { type: "language" }).of(locale) || "",
 | 
				
			||||||
    }));
 | 
					    }));
 | 
				
			||||||
  }, [props.localeProp, router.locales]);
 | 
					  }, [props.localeProp, router.locales]);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -69,6 +69,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
 | 
				
			||||||
          timeZone: true,
 | 
					          timeZone: true,
 | 
				
			||||||
          slotInterval: true,
 | 
					          slotInterval: true,
 | 
				
			||||||
          metadata: true,
 | 
					          metadata: true,
 | 
				
			||||||
 | 
					          schedule: {
 | 
				
			||||||
 | 
					            select: {
 | 
				
			||||||
 | 
					              timeZone: true,
 | 
				
			||||||
 | 
					              availability: true,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -82,13 +88,17 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [eventType] = team.eventTypes;
 | 
					  const [eventType] = team.eventTypes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeZone = eventType.schedule?.timeZone || eventType.timeZone || undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const workingHours = getWorkingHours(
 | 
					  const workingHours = getWorkingHours(
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      timeZone: eventType.timeZone || undefined,
 | 
					      timeZone,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    eventType.availability
 | 
					    eventType.schedule?.availability || eventType.availability
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  eventType.schedule = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const eventTypeObject = Object.assign({}, eventType, {
 | 
					  const eventTypeObject = Object.assign({}, eventType, {
 | 
				
			||||||
    metadata: (eventType.metadata || {}) as JSONObject,
 | 
					    metadata: (eventType.metadata || {}) as JSONObject,
 | 
				
			||||||
    periodStartDate: eventType.periodStartDate?.toString() ?? null,
 | 
					    periodStartDate: eventType.periodStartDate?.toString() ?? null,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -670,6 +670,16 @@
 | 
				
			||||||
  "prisma_studio_tip_description": "Learn how to set up your first user",
 | 
					  "prisma_studio_tip_description": "Learn how to set up your first user",
 | 
				
			||||||
  "contact_sales": "Contact Sales",
 | 
					  "contact_sales": "Contact Sales",
 | 
				
			||||||
  "error_404": "Error 404",
 | 
					  "error_404": "Error 404",
 | 
				
			||||||
 | 
					  "default": "Default",
 | 
				
			||||||
 | 
					  "set_to_default": "Set to Default",
 | 
				
			||||||
 | 
					  "new_schedule_btn": "New schedule",
 | 
				
			||||||
 | 
					  "add_new_schedule": "Add a new schedule",
 | 
				
			||||||
 | 
					  "delete_schedule": "Delete schedule",
 | 
				
			||||||
 | 
					  "schedule_created_successfully": "{{scheduleName}} schedule created successfully",
 | 
				
			||||||
 | 
					  "schedule_deleted_successfully": "Schedule deleted successfully",
 | 
				
			||||||
 | 
					  "default_schedule_name": "Working Hours",
 | 
				
			||||||
 | 
					  "new_schedule_heading": "Create an availability schedule",
 | 
				
			||||||
 | 
					  "new_schedule_description": "Creating availability schedules allows you to manage availability across event types. They can be applied to one or more event types.",
 | 
				
			||||||
  "requires_ownership_of_a_token": "Requires ownership of a token belonging to the following address:",
 | 
					  "requires_ownership_of_a_token": "Requires ownership of a token belonging to the following address:",
 | 
				
			||||||
  "example_name": "John Doe",
 | 
					  "example_name": "John Doe",
 | 
				
			||||||
  "time_format": "Time format",
 | 
					  "time_format": "Time format",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -520,5 +520,8 @@
 | 
				
			||||||
  "calendar": "Agenda",
 | 
					  "calendar": "Agenda",
 | 
				
			||||||
  "not_installed": "Niet geïnstalleerd",
 | 
					  "not_installed": "Niet geïnstalleerd",
 | 
				
			||||||
  "error_password_mismatch": "Wachtwoorden komen niet overeen.",
 | 
					  "error_password_mismatch": "Wachtwoorden komen niet overeen.",
 | 
				
			||||||
  "error_required_field": "Dit veld is verplicht."
 | 
					  "error_required_field": "Dit veld is verplicht.",
 | 
				
			||||||
 | 
					  "default": "standaard keuze",
 | 
				
			||||||
 | 
					  "set_to_default": "Zet als standaard keuze",
 | 
				
			||||||
 | 
					  "click_here_to_add_a_new_schedule": "Klik hier om een nieuwe planning aan te maken"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,6 +37,7 @@ async function getUserFromSession({
 | 
				
			||||||
      weekStart: true,
 | 
					      weekStart: true,
 | 
				
			||||||
      startTime: true,
 | 
					      startTime: true,
 | 
				
			||||||
      endTime: true,
 | 
					      endTime: true,
 | 
				
			||||||
 | 
					      defaultScheduleId: true,
 | 
				
			||||||
      bufferTime: true,
 | 
					      bufferTime: true,
 | 
				
			||||||
      theme: true,
 | 
					      theme: true,
 | 
				
			||||||
      createdDate: true,
 | 
					      createdDate: true,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,8 +19,8 @@ import {
 | 
				
			||||||
  samlTenantProduct,
 | 
					  samlTenantProduct,
 | 
				
			||||||
} from "@lib/saml";
 | 
					} from "@lib/saml";
 | 
				
			||||||
import slugify from "@lib/slugify";
 | 
					import slugify from "@lib/slugify";
 | 
				
			||||||
import { Schedule } from "@lib/types/schedule";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { availabilityRouter } from "@server/routers/viewer/availability";
 | 
				
			||||||
import { eventTypesRouter } from "@server/routers/viewer/eventTypes";
 | 
					import { eventTypesRouter } from "@server/routers/viewer/eventTypes";
 | 
				
			||||||
import { TRPCError } from "@trpc/server";
 | 
					import { TRPCError } from "@trpc/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -564,48 +564,6 @@ const loggedInViewerRouter = createProtectedRouter()
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
  .query("availability", {
 | 
					 | 
				
			||||||
    async resolve({ ctx }) {
 | 
					 | 
				
			||||||
      const { prisma, user } = ctx;
 | 
					 | 
				
			||||||
      const availabilityQuery = await prisma.availability.findMany({
 | 
					 | 
				
			||||||
        where: {
 | 
					 | 
				
			||||||
          userId: user.id,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      const schedule = availabilityQuery.reduce(
 | 
					 | 
				
			||||||
        (schedule: Schedule, availability) => {
 | 
					 | 
				
			||||||
          availability.days.forEach((day) => {
 | 
					 | 
				
			||||||
            schedule[day].push({
 | 
					 | 
				
			||||||
              start: new Date(
 | 
					 | 
				
			||||||
                Date.UTC(
 | 
					 | 
				
			||||||
                  new Date().getUTCFullYear(),
 | 
					 | 
				
			||||||
                  new Date().getUTCMonth(),
 | 
					 | 
				
			||||||
                  new Date().getUTCDate(),
 | 
					 | 
				
			||||||
                  availability.startTime.getUTCHours(),
 | 
					 | 
				
			||||||
                  availability.startTime.getUTCMinutes()
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              end: new Date(
 | 
					 | 
				
			||||||
                Date.UTC(
 | 
					 | 
				
			||||||
                  new Date().getUTCFullYear(),
 | 
					 | 
				
			||||||
                  new Date().getUTCMonth(),
 | 
					 | 
				
			||||||
                  new Date().getUTCDate(),
 | 
					 | 
				
			||||||
                  availability.endTime.getUTCHours(),
 | 
					 | 
				
			||||||
                  availability.endTime.getUTCMinutes()
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
          return schedule;
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        Array.from([...Array(7)]).map(() => [])
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        schedule,
 | 
					 | 
				
			||||||
        timeZone: user.timeZone,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
  .mutation("updateProfile", {
 | 
					  .mutation("updateProfile", {
 | 
				
			||||||
    input: z.object({
 | 
					    input: z.object({
 | 
				
			||||||
      username: z.string().optional(),
 | 
					      username: z.string().optional(),
 | 
				
			||||||
| 
						 | 
					@ -840,5 +798,6 @@ export const viewerRouter = createRouter()
 | 
				
			||||||
  .merge(publicViewerRouter)
 | 
					  .merge(publicViewerRouter)
 | 
				
			||||||
  .merge(loggedInViewerRouter)
 | 
					  .merge(loggedInViewerRouter)
 | 
				
			||||||
  .merge("eventTypes.", eventTypesRouter)
 | 
					  .merge("eventTypes.", eventTypesRouter)
 | 
				
			||||||
 | 
					  .merge("availability.", availabilityRouter)
 | 
				
			||||||
  .merge("teams.", viewerTeamsRouter)
 | 
					  .merge("teams.", viewerTeamsRouter)
 | 
				
			||||||
  .merge("webhook.", webhookRouter);
 | 
					  .merge("webhook.", webhookRouter);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										218
									
								
								apps/web/server/routers/viewer/availability.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								apps/web/server/routers/viewer/availability.tsx
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,218 @@
 | 
				
			||||||
 | 
					import { Prisma } from "@prisma/client";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { getAvailabilityFromSchedule } from "@lib/availability";
 | 
				
			||||||
 | 
					import { Schedule } from "@lib/types/schedule";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { createProtectedRouter } from "@server/createRouter";
 | 
				
			||||||
 | 
					import { TRPCError } from "@trpc/server";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const availabilityRouter = createProtectedRouter()
 | 
				
			||||||
 | 
					  .query("list", {
 | 
				
			||||||
 | 
					    async resolve({ ctx }) {
 | 
				
			||||||
 | 
					      const { prisma, user } = ctx;
 | 
				
			||||||
 | 
					      const schedules = await prisma.schedule.findMany({
 | 
				
			||||||
 | 
					        where: {
 | 
				
			||||||
 | 
					          userId: user.id,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        select: {
 | 
				
			||||||
 | 
					          id: true,
 | 
				
			||||||
 | 
					          name: true,
 | 
				
			||||||
 | 
					          availability: true,
 | 
				
			||||||
 | 
					          timeZone: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        orderBy: {
 | 
				
			||||||
 | 
					          id: "asc",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        schedules: schedules.map((schedule) => ({
 | 
				
			||||||
 | 
					          ...schedule,
 | 
				
			||||||
 | 
					          isDefault: user.defaultScheduleId === schedule.id || schedules.length === 1,
 | 
				
			||||||
 | 
					        })),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .query("schedule", {
 | 
				
			||||||
 | 
					    input: z.object({
 | 
				
			||||||
 | 
					      scheduleId: z.number(),
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    async resolve({ ctx, input }) {
 | 
				
			||||||
 | 
					      const { prisma, user } = ctx;
 | 
				
			||||||
 | 
					      const schedule = await prisma.schedule.findUnique({
 | 
				
			||||||
 | 
					        where: {
 | 
				
			||||||
 | 
					          id: input.scheduleId,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        select: {
 | 
				
			||||||
 | 
					          id: true,
 | 
				
			||||||
 | 
					          userId: true,
 | 
				
			||||||
 | 
					          name: true,
 | 
				
			||||||
 | 
					          availability: true,
 | 
				
			||||||
 | 
					          timeZone: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      if (!schedule || schedule.userId !== user.id) {
 | 
				
			||||||
 | 
					        throw new TRPCError({
 | 
				
			||||||
 | 
					          code: "UNAUTHORIZED",
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const availability = schedule.availability.reduce(
 | 
				
			||||||
 | 
					        (schedule: Schedule, availability) => {
 | 
				
			||||||
 | 
					          availability.days.forEach((day) => {
 | 
				
			||||||
 | 
					            schedule[day].push({
 | 
				
			||||||
 | 
					              start: new Date(
 | 
				
			||||||
 | 
					                Date.UTC(
 | 
				
			||||||
 | 
					                  new Date().getUTCFullYear(),
 | 
				
			||||||
 | 
					                  new Date().getUTCMonth(),
 | 
				
			||||||
 | 
					                  new Date().getUTCDate(),
 | 
				
			||||||
 | 
					                  availability.startTime.getUTCHours(),
 | 
				
			||||||
 | 
					                  availability.startTime.getUTCMinutes()
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              end: new Date(
 | 
				
			||||||
 | 
					                Date.UTC(
 | 
				
			||||||
 | 
					                  new Date().getUTCFullYear(),
 | 
				
			||||||
 | 
					                  new Date().getUTCMonth(),
 | 
				
			||||||
 | 
					                  new Date().getUTCDate(),
 | 
				
			||||||
 | 
					                  availability.endTime.getUTCHours(),
 | 
				
			||||||
 | 
					                  availability.endTime.getUTCMinutes()
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          return schedule;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        Array.from([...Array(7)]).map(() => [])
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        schedule,
 | 
				
			||||||
 | 
					        availability,
 | 
				
			||||||
 | 
					        timeZone: schedule.timeZone || user.timeZone,
 | 
				
			||||||
 | 
					        isDefault: !user.defaultScheduleId || user.defaultScheduleId === schedule.id,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .mutation("schedule.create", {
 | 
				
			||||||
 | 
					    input: z.object({
 | 
				
			||||||
 | 
					      name: z.string(),
 | 
				
			||||||
 | 
					      copyScheduleId: z.number().optional(),
 | 
				
			||||||
 | 
					      schedule: z
 | 
				
			||||||
 | 
					        .array(
 | 
				
			||||||
 | 
					          z.array(
 | 
				
			||||||
 | 
					            z.object({
 | 
				
			||||||
 | 
					              start: z.date(),
 | 
				
			||||||
 | 
					              end: z.date(),
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .optional(),
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    async resolve({ input, ctx }) {
 | 
				
			||||||
 | 
					      const { user, prisma } = ctx;
 | 
				
			||||||
 | 
					      const data: Prisma.ScheduleCreateInput = {
 | 
				
			||||||
 | 
					        name: input.name,
 | 
				
			||||||
 | 
					        user: {
 | 
				
			||||||
 | 
					          connect: {
 | 
				
			||||||
 | 
					            id: user.id,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (input.schedule) {
 | 
				
			||||||
 | 
					        const availability = getAvailabilityFromSchedule(input.schedule);
 | 
				
			||||||
 | 
					        data.availability = {
 | 
				
			||||||
 | 
					          createMany: {
 | 
				
			||||||
 | 
					            data: availability.map((schedule) => ({
 | 
				
			||||||
 | 
					              days: schedule.days,
 | 
				
			||||||
 | 
					              startTime: schedule.startTime,
 | 
				
			||||||
 | 
					              endTime: schedule.endTime,
 | 
				
			||||||
 | 
					            })),
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const schedule = await prisma.schedule.create({
 | 
				
			||||||
 | 
					        data,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      return { schedule };
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .mutation("schedule.delete", {
 | 
				
			||||||
 | 
					    input: z.object({
 | 
				
			||||||
 | 
					      scheduleId: z.number(),
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    async resolve({ input, ctx }) {
 | 
				
			||||||
 | 
					      const { user, prisma } = ctx;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (user.defaultScheduleId === input.scheduleId) {
 | 
				
			||||||
 | 
					        // unset default
 | 
				
			||||||
 | 
					        await prisma.user.update({
 | 
				
			||||||
 | 
					          where: {
 | 
				
			||||||
 | 
					            id: user.id,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          data: {
 | 
				
			||||||
 | 
					            defaultScheduleId: undefined,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      await prisma.schedule.delete({
 | 
				
			||||||
 | 
					        where: {
 | 
				
			||||||
 | 
					          id: input.scheduleId,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  .mutation("schedule.update", {
 | 
				
			||||||
 | 
					    input: z.object({
 | 
				
			||||||
 | 
					      scheduleId: z.number(),
 | 
				
			||||||
 | 
					      timeZone: z.string().optional(),
 | 
				
			||||||
 | 
					      name: z.string().optional(),
 | 
				
			||||||
 | 
					      isDefault: z.boolean().optional(),
 | 
				
			||||||
 | 
					      schedule: z.array(
 | 
				
			||||||
 | 
					        z.array(
 | 
				
			||||||
 | 
					          z.object({
 | 
				
			||||||
 | 
					            start: z.date(),
 | 
				
			||||||
 | 
					            end: z.date(),
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    async resolve({ input, ctx }) {
 | 
				
			||||||
 | 
					      const { user, prisma } = ctx;
 | 
				
			||||||
 | 
					      const availability = getAvailabilityFromSchedule(input.schedule);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (input.isDefault) {
 | 
				
			||||||
 | 
					        await prisma.user.update({
 | 
				
			||||||
 | 
					          where: {
 | 
				
			||||||
 | 
					            id: user.id,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          data: {
 | 
				
			||||||
 | 
					            defaultScheduleId: input.scheduleId,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await prisma.schedule.update({
 | 
				
			||||||
 | 
					        where: {
 | 
				
			||||||
 | 
					          id: input.scheduleId,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					          timeZone: input.timeZone,
 | 
				
			||||||
 | 
					          name: input.name,
 | 
				
			||||||
 | 
					          availability: {
 | 
				
			||||||
 | 
					            deleteMany: {
 | 
				
			||||||
 | 
					              scheduleId: {
 | 
				
			||||||
 | 
					                equals: input.scheduleId,
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            createMany: {
 | 
				
			||||||
 | 
					              data: availability.map((schedule) => ({
 | 
				
			||||||
 | 
					                days: schedule.days,
 | 
				
			||||||
 | 
					                startTime: schedule.startTime,
 | 
				
			||||||
 | 
					                endTime: schedule.endTime,
 | 
				
			||||||
 | 
					              })),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
| 
						 | 
					@ -63,27 +63,16 @@ function handleCustomInputs(customInputs: EventTypeCustomInput[], eventTypeId: n
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const AvailabilityInput = _AvailabilityModel.pick({
 | 
					 | 
				
			||||||
  days: true,
 | 
					 | 
				
			||||||
  startTime: true,
 | 
					 | 
				
			||||||
  endTime: true,
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const EventTypeUpdateInput = _EventTypeModel
 | 
					const EventTypeUpdateInput = _EventTypeModel
 | 
				
			||||||
  /** Optional fields */
 | 
					  /** Optional fields */
 | 
				
			||||||
  .extend({
 | 
					  .extend({
 | 
				
			||||||
    availability: z
 | 
					 | 
				
			||||||
      .object({
 | 
					 | 
				
			||||||
        openingHours: z.array(AvailabilityInput).optional(),
 | 
					 | 
				
			||||||
        dateOverrides: z.array(AvailabilityInput).optional(),
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .optional(),
 | 
					 | 
				
			||||||
    customInputs: z.array(_EventTypeCustomInputModel),
 | 
					    customInputs: z.array(_EventTypeCustomInputModel),
 | 
				
			||||||
    destinationCalendar: _DestinationCalendarModel.pick({
 | 
					    destinationCalendar: _DestinationCalendarModel.pick({
 | 
				
			||||||
      integration: true,
 | 
					      integration: true,
 | 
				
			||||||
      externalId: true,
 | 
					      externalId: true,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    users: z.array(stringOrNumber).optional(),
 | 
					    users: z.array(stringOrNumber).optional(),
 | 
				
			||||||
 | 
					    schedule: z.number().optional(),
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
  .partial()
 | 
					  .partial()
 | 
				
			||||||
  .merge(
 | 
					  .merge(
 | 
				
			||||||
| 
						 | 
					@ -190,7 +179,7 @@ export const eventTypesRouter = createProtectedRouter()
 | 
				
			||||||
  .mutation("update", {
 | 
					  .mutation("update", {
 | 
				
			||||||
    input: EventTypeUpdateInput.strict(),
 | 
					    input: EventTypeUpdateInput.strict(),
 | 
				
			||||||
    async resolve({ ctx, input }) {
 | 
					    async resolve({ ctx, input }) {
 | 
				
			||||||
      const { availability, periodType, locations, destinationCalendar, customInputs, users, id, ...rest } =
 | 
					      const { schedule, periodType, locations, destinationCalendar, customInputs, users, id, ...rest } =
 | 
				
			||||||
        input;
 | 
					        input;
 | 
				
			||||||
      const data: Prisma.EventTypeUpdateInput = rest;
 | 
					      const data: Prisma.EventTypeUpdateInput = rest;
 | 
				
			||||||
      data.locations = locations ?? undefined;
 | 
					      data.locations = locations ?? undefined;
 | 
				
			||||||
| 
						 | 
					@ -211,6 +200,14 @@ export const eventTypesRouter = createProtectedRouter()
 | 
				
			||||||
        data.customInputs = handleCustomInputs(customInputs, id);
 | 
					        data.customInputs = handleCustomInputs(customInputs, id);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (schedule) {
 | 
				
			||||||
 | 
					        data.schedule = {
 | 
				
			||||||
 | 
					          connect: {
 | 
				
			||||||
 | 
					            id: schedule,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (users) {
 | 
					      if (users) {
 | 
				
			||||||
        data.users = {
 | 
					        data.users = {
 | 
				
			||||||
          set: [],
 | 
					          set: [],
 | 
				
			||||||
| 
						 | 
					@ -218,20 +215,6 @@ export const eventTypesRouter = createProtectedRouter()
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (availability?.openingHours) {
 | 
					 | 
				
			||||||
        await ctx.prisma.availability.deleteMany({
 | 
					 | 
				
			||||||
          where: {
 | 
					 | 
				
			||||||
            eventTypeId: input.id,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        data.availability = {
 | 
					 | 
				
			||||||
          createMany: {
 | 
					 | 
				
			||||||
            data: availability.openingHours,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const eventType = await ctx.prisma.eventType.update({
 | 
					      const eventType = await ctx.prisma.eventType.update({
 | 
				
			||||||
        where: { id },
 | 
					        where: { id },
 | 
				
			||||||
        data,
 | 
					        data,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,14 @@
 | 
				
			||||||
  --brand-text-color-dark-mode: #292929;
 | 
					  --brand-text-color-dark-mode: #292929;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button[role="switch"][data-state="checked"] {
 | 
				
			||||||
 | 
					  @apply bg-gray-900;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					button[role="switch"][data-state="checked"] span {
 | 
				
			||||||
 | 
					  transform: translateX(16px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* PhoneInput dark-mode overrides (it would add a lot of boilerplate to do this in JavaScript) */
 | 
					/* PhoneInput dark-mode overrides (it would add a lot of boilerplate to do this in JavaScript) */
 | 
				
			||||||
.PhoneInputInput {
 | 
					.PhoneInputInput {
 | 
				
			||||||
  @apply border-0 text-sm focus:ring-0;
 | 
					  @apply border-0 text-sm focus:ring-0;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@ import customParseFormat from "dayjs/plugin/customParseFormat";
 | 
				
			||||||
import timezone from "dayjs/plugin/timezone";
 | 
					import timezone from "dayjs/plugin/timezone";
 | 
				
			||||||
import utc from "dayjs/plugin/utc";
 | 
					import utc from "dayjs/plugin/utc";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { nameOfDay } from "@calcom/lib/weekday";
 | 
				
			||||||
import type { Availability } from "@calcom/prisma/client";
 | 
					import type { Availability } from "@calcom/prisma/client";
 | 
				
			||||||
import type { Schedule, TimeRange, WorkingHours } from "@calcom/types/schedule";
 | 
					import type { Schedule, TimeRange, WorkingHours } from "@calcom/types/schedule";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,7 +39,9 @@ export function getAvailabilityFromSchedule(schedule: Schedule): Availability[]
 | 
				
			||||||
      let idx;
 | 
					      let idx;
 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
        (idx = availability.findIndex(
 | 
					        (idx = availability.findIndex(
 | 
				
			||||||
          (schedule) => schedule.startTime === time.start && schedule.endTime === time.end
 | 
					          (schedule) =>
 | 
				
			||||||
 | 
					            schedule.startTime.toString() === time.start.toString() &&
 | 
				
			||||||
 | 
					            schedule.endTime.toString() === time.end.toString()
 | 
				
			||||||
        )) !== -1
 | 
					        )) !== -1
 | 
				
			||||||
      ) {
 | 
					      ) {
 | 
				
			||||||
        availability[idx].days.push(day);
 | 
					        availability[idx].days.push(day);
 | 
				
			||||||
| 
						 | 
					@ -124,3 +127,41 @@ export function getWorkingHours(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return workingHours;
 | 
					  return workingHours;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function availabilityAsString(availability: Availability, locale: string) {
 | 
				
			||||||
 | 
					  const weekSpan = (availability: Availability) => {
 | 
				
			||||||
 | 
					    const days = availability.days.slice(1).reduce(
 | 
				
			||||||
 | 
					      (days, day) => {
 | 
				
			||||||
 | 
					        if (days[days.length - 1].length === 1 && days[days.length - 1][0] === day - 1) {
 | 
				
			||||||
 | 
					          // append if the range is not complete (but the next day needs adding)
 | 
				
			||||||
 | 
					          days[days.length - 1].push(day);
 | 
				
			||||||
 | 
					        } else if (days[days.length - 1][days[days.length - 1].length - 1] === day - 1) {
 | 
				
			||||||
 | 
					          // range complete, overwrite if the last day directly preceeds the current day
 | 
				
			||||||
 | 
					          days[days.length - 1] = [days[days.length - 1][0], day];
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          // new range
 | 
				
			||||||
 | 
					          days.push([day]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return days;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      [[availability.days[0]]] as number[][]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    return days
 | 
				
			||||||
 | 
					      .map((dayRange) => dayRange.map((day) => nameOfDay(locale, day, "short")).join(" - "))
 | 
				
			||||||
 | 
					      .join(", ");
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const timeSpan = (availability: Availability) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "numeric" }).format(
 | 
				
			||||||
 | 
					        new Date(availability.startTime.toISOString().slice(0, -1))
 | 
				
			||||||
 | 
					      ) +
 | 
				
			||||||
 | 
					      " - " +
 | 
				
			||||||
 | 
					      new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "numeric" }).format(
 | 
				
			||||||
 | 
					        new Date(availability.endTime.toISOString().slice(0, -1))
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return weekSpan(availability) + ", " + timeSpan(availability);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										8
									
								
								packages/lib/weekday.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								packages/lib/weekday.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					// By default starts on Sunday (Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
 | 
				
			||||||
 | 
					export function weekdayNames(locale: string | string[], weekStart = 0, type: "short" | "long" = "long") {
 | 
				
			||||||
 | 
					  return Array.from(Array(7).keys()).map((d) => nameOfDay(locale, d + weekStart, type));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function nameOfDay(locale: string | string[], day: number, type: "short" | "long" = "long") {
 | 
				
			||||||
 | 
					  return new Intl.DateTimeFormat(locale, { weekday: type }).format(new Date(1970, 0, day + 4));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					  Warnings:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  - You are about to drop the column `label` on the `Availability` table. All the data in the column will be lost.
 | 
				
			||||||
 | 
					  - You are about to drop the column `freeBusyTimes` on the `Schedule` table. All the data in the column will be lost.
 | 
				
			||||||
 | 
					  - You are about to drop the column `title` on the `Schedule` table. All the data in the column will be lost.
 | 
				
			||||||
 | 
					  - A unique constraint covering the columns `[eventTypeId]` on the table `Schedule` will be added. If there are existing duplicate values, this will fail.
 | 
				
			||||||
 | 
					  - Added the required column `name` to the `Schedule` table without a default value. This is not possible if the table is not empty.
 | 
				
			||||||
 | 
					  - Made the column `userId` on table `Schedule` required. This step will fail if there are existing NULL values in that column.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					-- AlterTable
 | 
				
			||||||
 | 
					ALTER TABLE "Availability" DROP COLUMN "label",
 | 
				
			||||||
 | 
					ADD COLUMN     "scheduleId" INTEGER;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- AlterTable
 | 
				
			||||||
 | 
					ALTER TABLE "Schedule" DROP COLUMN "freeBusyTimes",
 | 
				
			||||||
 | 
					DROP COLUMN "title",
 | 
				
			||||||
 | 
					ADD COLUMN     "name" TEXT NOT NULL,
 | 
				
			||||||
 | 
					ADD COLUMN     "timeZone" TEXT,
 | 
				
			||||||
 | 
					ALTER COLUMN "userId" SET NOT NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- AlterTable
 | 
				
			||||||
 | 
					ALTER TABLE "users" ADD COLUMN     "defaultScheduleId" INTEGER;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- CreateIndex
 | 
				
			||||||
 | 
					CREATE UNIQUE INDEX "Schedule_eventTypeId_key" ON "Schedule"("eventTypeId");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- AddForeignKey
 | 
				
			||||||
 | 
					ALTER TABLE "Availability" ADD CONSTRAINT "Availability_scheduleId_fkey" FOREIGN KEY ("scheduleId") REFERENCES "Schedule"("id") ON DELETE SET NULL ON UPDATE CASCADE;
 | 
				
			||||||
| 
						 | 
					@ -62,7 +62,7 @@ model EventType {
 | 
				
			||||||
  beforeEventBuffer       Int                    @default(0)
 | 
					  beforeEventBuffer       Int                    @default(0)
 | 
				
			||||||
  afterEventBuffer        Int                    @default(0)
 | 
					  afterEventBuffer        Int                    @default(0)
 | 
				
			||||||
  schedulingType          SchedulingType?
 | 
					  schedulingType          SchedulingType?
 | 
				
			||||||
  Schedule                Schedule[]
 | 
					  schedule                Schedule?
 | 
				
			||||||
  price                   Int                    @default(0)
 | 
					  price                   Int                    @default(0)
 | 
				
			||||||
  currency                String                 @default("usd")
 | 
					  currency                String                 @default("usd")
 | 
				
			||||||
  slotInterval            Int?
 | 
					  slotInterval            Int?
 | 
				
			||||||
| 
						 | 
					@ -128,7 +128,8 @@ model User {
 | 
				
			||||||
  credentials         Credential[]
 | 
					  credentials         Credential[]
 | 
				
			||||||
  teams               Membership[]
 | 
					  teams               Membership[]
 | 
				
			||||||
  bookings            Booking[]
 | 
					  bookings            Booking[]
 | 
				
			||||||
  availability        Availability[]
 | 
					  schedules           Schedule[]
 | 
				
			||||||
 | 
					  defaultScheduleId   Int?
 | 
				
			||||||
  selectedCalendars   SelectedCalendar[]
 | 
					  selectedCalendars   SelectedCalendar[]
 | 
				
			||||||
  completedOnboarding Boolean              @default(false)
 | 
					  completedOnboarding Boolean              @default(false)
 | 
				
			||||||
  locale              String?
 | 
					  locale              String?
 | 
				
			||||||
| 
						 | 
					@ -137,9 +138,9 @@ model User {
 | 
				
			||||||
  twoFactorEnabled    Boolean              @default(false)
 | 
					  twoFactorEnabled    Boolean              @default(false)
 | 
				
			||||||
  identityProvider    IdentityProvider     @default(CAL)
 | 
					  identityProvider    IdentityProvider     @default(CAL)
 | 
				
			||||||
  identityProviderId  String?
 | 
					  identityProviderId  String?
 | 
				
			||||||
 | 
					  availability        Availability[]
 | 
				
			||||||
  invitedTo           Int?
 | 
					  invitedTo           Int?
 | 
				
			||||||
  plan                UserPlan             @default(TRIAL)
 | 
					  plan                UserPlan             @default(TRIAL)
 | 
				
			||||||
  Schedule            Schedule[]
 | 
					 | 
				
			||||||
  webhooks            Webhook[]
 | 
					  webhooks            Webhook[]
 | 
				
			||||||
  brandColor          String               @default("#292929")
 | 
					  brandColor          String               @default("#292929")
 | 
				
			||||||
  darkBrandColor      String               @default("#fafafa")
 | 
					  darkBrandColor      String               @default("#fafafa")
 | 
				
			||||||
| 
						 | 
					@ -256,17 +257,17 @@ model Booking {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
model Schedule {
 | 
					model Schedule {
 | 
				
			||||||
  id           Int            @id @default(autoincrement())
 | 
					  id           Int            @id @default(autoincrement())
 | 
				
			||||||
  user          User?      @relation(fields: [userId], references: [id], onDelete: Cascade)
 | 
					  user         User           @relation(fields: [userId], references: [id], onDelete: Cascade)
 | 
				
			||||||
  userId        Int?
 | 
					  userId       Int
 | 
				
			||||||
  eventType    EventType?     @relation(fields: [eventTypeId], references: [id])
 | 
					  eventType    EventType?     @relation(fields: [eventTypeId], references: [id])
 | 
				
			||||||
  eventTypeId   Int?
 | 
					  eventTypeId  Int?           @unique
 | 
				
			||||||
  title         String?
 | 
					  name         String
 | 
				
			||||||
  freeBusyTimes Json?
 | 
					  timeZone     String?
 | 
				
			||||||
 | 
					  availability Availability[]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
model Availability {
 | 
					model Availability {
 | 
				
			||||||
  id          Int        @id @default(autoincrement())
 | 
					  id          Int        @id @default(autoincrement())
 | 
				
			||||||
  label       String?
 | 
					 | 
				
			||||||
  user        User?      @relation(fields: [userId], references: [id], onDelete: Cascade)
 | 
					  user        User?      @relation(fields: [userId], references: [id], onDelete: Cascade)
 | 
				
			||||||
  userId      Int?
 | 
					  userId      Int?
 | 
				
			||||||
  eventType   EventType? @relation(fields: [eventTypeId], references: [id])
 | 
					  eventType   EventType? @relation(fields: [eventTypeId], references: [id])
 | 
				
			||||||
| 
						 | 
					@ -275,6 +276,8 @@ model Availability {
 | 
				
			||||||
  startTime   DateTime   @db.Time
 | 
					  startTime   DateTime   @db.Time
 | 
				
			||||||
  endTime     DateTime   @db.Time
 | 
					  endTime     DateTime   @db.Time
 | 
				
			||||||
  date        DateTime?  @db.Date
 | 
					  date        DateTime?  @db.Date
 | 
				
			||||||
 | 
					  Schedule    Schedule?  @relation(fields: [scheduleId], references: [id])
 | 
				
			||||||
 | 
					  scheduleId  Int?
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
model SelectedCalendar {
 | 
					model SelectedCalendar {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,11 +29,18 @@ async function createUserAndEventType(opts: {
 | 
				
			||||||
    emailVerified: new Date(),
 | 
					    emailVerified: new Date(),
 | 
				
			||||||
    completedOnboarding: opts.user.completedOnboarding ?? true,
 | 
					    completedOnboarding: opts.user.completedOnboarding ?? true,
 | 
				
			||||||
    locale: "en",
 | 
					    locale: "en",
 | 
				
			||||||
 | 
					    schedules: opts.user.completedOnboarding
 | 
				
			||||||
 | 
					      ? {
 | 
				
			||||||
 | 
					          create: {
 | 
				
			||||||
 | 
					            name: "Working Hours",
 | 
				
			||||||
            availability: {
 | 
					            availability: {
 | 
				
			||||||
              createMany: {
 | 
					              createMany: {
 | 
				
			||||||
                data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE),
 | 
					                data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE),
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      : undefined,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  const user = await prisma.user.upsert({
 | 
					  const user = await prisma.user.upsert({
 | 
				
			||||||
    where: { email: opts.user.email },
 | 
					    where: { email: opts.user.email },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { CheckCircleIcon, InformationCircleIcon, XCircleIcon } from "@heroicons/react/solid";
 | 
					import { CheckCircleIcon, ExclamationIcon, InformationCircleIcon, XCircleIcon } from "@heroicons/react/solid";
 | 
				
			||||||
import classNames from "classnames";
 | 
					import classNames from "classnames";
 | 
				
			||||||
import { ReactNode } from "react";
 | 
					import { ReactNode } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ export interface AlertProps {
 | 
				
			||||||
  message?: ReactNode;
 | 
					  message?: ReactNode;
 | 
				
			||||||
  actions?: ReactNode;
 | 
					  actions?: ReactNode;
 | 
				
			||||||
  className?: string;
 | 
					  className?: string;
 | 
				
			||||||
  severity: "success" | "warning" | "error";
 | 
					  severity: "success" | "warning" | "error" | "info";
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
export function Alert(props: AlertProps) {
 | 
					export function Alert(props: AlertProps) {
 | 
				
			||||||
  const { severity } = props;
 | 
					  const { severity } = props;
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,7 @@ export function Alert(props: AlertProps) {
 | 
				
			||||||
        props.className,
 | 
					        props.className,
 | 
				
			||||||
        severity === "error" && "border-red-900 bg-red-50 text-red-800",
 | 
					        severity === "error" && "border-red-900 bg-red-50 text-red-800",
 | 
				
			||||||
        severity === "warning" && "border-yellow-700 bg-yellow-50 text-yellow-700",
 | 
					        severity === "warning" && "border-yellow-700 bg-yellow-50 text-yellow-700",
 | 
				
			||||||
 | 
					        severity === "info" && "border-sky-700 bg-sky-50 text-sky-700",
 | 
				
			||||||
        severity === "success" && "bg-gray-900 text-white"
 | 
					        severity === "success" && "bg-gray-900 text-white"
 | 
				
			||||||
      )}>
 | 
					      )}>
 | 
				
			||||||
      <div className="flex">
 | 
					      <div className="flex">
 | 
				
			||||||
| 
						 | 
					@ -27,7 +28,10 @@ export function Alert(props: AlertProps) {
 | 
				
			||||||
            <XCircleIcon className={classNames("h-5 w-5 text-red-400")} aria-hidden="true" />
 | 
					            <XCircleIcon className={classNames("h-5 w-5 text-red-400")} aria-hidden="true" />
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
          {severity === "warning" && (
 | 
					          {severity === "warning" && (
 | 
				
			||||||
            <InformationCircleIcon className={classNames("h-5 w-5 text-yellow-400")} aria-hidden="true" />
 | 
					            <ExclamationIcon className={classNames("h-5 w-5 text-yellow-400")} aria-hidden="true" />
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					          {severity === "info" && (
 | 
				
			||||||
 | 
					            <InformationCircleIcon className={classNames("h-5 w-5 text-sky-400")} aria-hidden="true" />
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
          {severity === "success" && (
 | 
					          {severity === "success" && (
 | 
				
			||||||
            <CheckCircleIcon className={classNames("h-5 w-5 text-gray-400")} aria-hidden="true" />
 | 
					            <CheckCircleIcon className={classNames("h-5 w-5 text-gray-400")} aria-hidden="true" />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,37 +1,22 @@
 | 
				
			||||||
import { useId } from "@radix-ui/react-id";
 | 
					import { useId } from "@radix-ui/react-id";
 | 
				
			||||||
import * as Label from "@radix-ui/react-label";
 | 
					import * as Label from "@radix-ui/react-label";
 | 
				
			||||||
import * as PrimitiveSwitch from "@radix-ui/react-switch";
 | 
					import * as PrimitiveSwitch from "@radix-ui/react-switch";
 | 
				
			||||||
import React, { useState } from "react";
 | 
					import React from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import classNames from "@calcom/lib/classNames";
 | 
					const Switch = (
 | 
				
			||||||
 | 
					  props: React.ComponentProps<typeof PrimitiveSwitch.Root> & {
 | 
				
			||||||
type SwitchProps = React.ComponentProps<typeof PrimitiveSwitch.Root> & {
 | 
					 | 
				
			||||||
    label: string;
 | 
					    label: string;
 | 
				
			||||||
};
 | 
					 | 
				
			||||||
export default function Switch(props: SwitchProps) {
 | 
					 | 
				
			||||||
  const { label, onCheckedChange, ...primitiveProps } = props;
 | 
					 | 
				
			||||||
  const [checked, setChecked] = useState(props.defaultChecked || false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const onPrimitiveCheckedChange = (change: boolean) => {
 | 
					 | 
				
			||||||
    if (onCheckedChange) {
 | 
					 | 
				
			||||||
      onCheckedChange(change);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
    setChecked(change);
 | 
					) => {
 | 
				
			||||||
  };
 | 
					  const { label, ...primitiveProps } = props;
 | 
				
			||||||
  const id = useId();
 | 
					  const id = useId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="flex h-[20px] items-center">
 | 
					    <div className="flex h-[20px] items-center">
 | 
				
			||||||
      <PrimitiveSwitch.Root
 | 
					      <PrimitiveSwitch.Root className="h-[20px] w-[36px] rounded-sm bg-gray-400 p-0.5" {...primitiveProps}>
 | 
				
			||||||
        className={classNames(checked ? "bg-gray-900" : "bg-gray-400", "h-[20px] w-[36px] rounded-sm p-0.5")}
 | 
					 | 
				
			||||||
        checked={checked}
 | 
					 | 
				
			||||||
        onCheckedChange={onPrimitiveCheckedChange}
 | 
					 | 
				
			||||||
        {...primitiveProps}>
 | 
					 | 
				
			||||||
        <PrimitiveSwitch.Thumb
 | 
					        <PrimitiveSwitch.Thumb
 | 
				
			||||||
          id={id}
 | 
					          id={id}
 | 
				
			||||||
          className={classNames(
 | 
					          className={"block h-[16px] w-[16px] translate-x-0 bg-white transition-transform"}
 | 
				
			||||||
            "block h-[16px] w-[16px] bg-white transition-transform",
 | 
					 | 
				
			||||||
            checked ? "translate-x-[16px]" : "translate-x-0"
 | 
					 | 
				
			||||||
          )}
 | 
					 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </PrimitiveSwitch.Root>
 | 
					      </PrimitiveSwitch.Root>
 | 
				
			||||||
      {label && (
 | 
					      {label && (
 | 
				
			||||||
| 
						 | 
					@ -43,4 +28,6 @@ export default function Switch(props: SwitchProps) {
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Switch;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in a new issue