Availability / Single View (#4467)

* Revamped Schedule before type fixes

* Removed t('copy_all'); fake focus state on CopyButton open

* Added skeletons, availability single view (wip, still a bit glitchy)

* Fixed isDefault switch glitchiness

* Fixes changing the default schedule to the created schedule

* Fix some type errors, on create set availability to DEFAULT_SCHEDULE

* Provided missing translation for copy_times_to

* Fix type errors, this may not be possible until v8 RHF

* Fix lint error

* Some responsiveness fixes

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
pull/4483/head^2
Alex van Andel 2022-09-15 06:49:59 +01:00 committed by GitHub
parent a3462657db
commit 33e8b5d227
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 372 additions and 369 deletions

View File

@ -3,23 +3,16 @@ import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
import { Schedule } from "@calcom/features/schedules";
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc, TRPCClientErrorLike } from "@calcom/trpc/react";
import { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Form } from "@calcom/ui/form/fields";
import { Button } from "@calcom/ui/v2";
import { DEFAULT_SCHEDULE } from "@lib/availability";
import type { Schedule as ScheduleType } from "@lib/types/schedule";
interface ISetupAvailabilityProps {
nextStep: () => void;
defaultScheduleId?: number | null;
defaultAvailability?: { schedule?: TimeRanges[][] };
}
interface ScheduleFormValues {
schedule: ScheduleType;
}
const SetupAvailability = (props: ISetupAvailabilityProps) => {
@ -37,7 +30,9 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
}
const availabilityForm = useForm({
defaultValues: { schedule: queryAvailability?.data?.availability || DEFAULT_SCHEDULE },
defaultValues: {
schedule: queryAvailability?.data?.availability || DEFAULT_SCHEDULE,
},
});
const mutationOptions = {
@ -51,7 +46,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
const createSchedule = trpc.useMutation("viewer.availability.schedule.create", mutationOptions);
const updateSchedule = trpc.useMutation("viewer.availability.schedule.update", mutationOptions);
return (
<Form<ScheduleFormValues>
<Form
className="w-full bg-white text-black dark:bg-opacity-5 dark:text-white"
form={availabilityForm}
handleSubmit={async (values) => {
@ -75,7 +70,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
}
}
}}>
<Schedule />
<Schedule control={availabilityForm.control} name="schedule" weekStart={1} />
<div>
<Button

View File

@ -1,36 +1,60 @@
import { GetStaticPaths, GetStaticProps } from "next";
import { useRouter } from "next/router";
import { useState } from "react";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
import { Schedule } from "@calcom/features/schedules";
import { availabilityAsString, DEFAULT_SCHEDULE } from "@calcom/lib/availability";
import Schedule from "@calcom/features/schedules/components/Schedule";
import { availabilityAsString } from "@calcom/lib/availability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { BadgeCheckIcon } from "@calcom/ui/Icon";
import Shell from "@calcom/ui/Shell";
import { trpc } from "@calcom/trpc/react";
import type { Schedule as ScheduleType } from "@calcom/types/schedule";
import { Icon } from "@calcom/ui";
import TimezoneSelect from "@calcom/ui/form/TimezoneSelect";
import { Button, Form, showToast, Switch, TextField } from "@calcom/ui/v2";
import Button from "@calcom/ui/v2/core/Button";
import Shell from "@calcom/ui/v2/core/Shell";
import Switch from "@calcom/ui/v2/core/Switch";
import VerticalDivider from "@calcom/ui/v2/core/VerticalDivider";
import { Form, Label } from "@calcom/ui/v2/core/form/fields";
import showToast from "@calcom/ui/v2/core/notifications";
import { SkeletonText } from "@calcom/ui/v2/core/skeleton";
import { QueryCell } from "@lib/QueryCell";
import { HttpError } from "@lib/core/http/error";
import EditableHeading from "@components/ui/EditableHeading";
export function AvailabilityForm(props: inferQueryOutput<"viewer.availability.schedule">) {
const { t } = useLocale();
const querySchema = z.object({
schedule: stringOrNumber,
});
type AvailabilityFormValues = {
name: string;
schedule: ScheduleType;
timeZone: string;
isDefault: boolean;
};
export default function Availability({ schedule }: { schedule: number }) {
const { t, i18n } = useLocale();
const router = useRouter();
const utils = trpc.useContext();
const form = useForm({
defaultValues: {
schedule: props.availability || DEFAULT_SCHEDULE,
isDefault: !!props.isDefault,
timeZone: props.timeZone,
},
});
const { data, isLoading } = trpc.useQuery(["viewer.availability.schedule", { scheduleId: schedule }]);
const form = useForm<AvailabilityFormValues>();
const { control, reset, setValue } = form;
useEffect(() => {
if (!isLoading && data) {
reset({
name: data?.schedule?.name,
schedule: data.availability,
timeZone: data.timeZone,
isDefault: data.isDefault,
});
}
}, [data, isLoading, reset]);
const updateMutation = trpc.useMutation("viewer.availability.schedule.update", {
onSuccess: async ({ schedule }) => {
@ -53,107 +77,100 @@ export function AvailabilityForm(props: inferQueryOutput<"viewer.availability.sc
});
return (
<Form
form={form}
handleSubmit={async (values) => {
updateMutation.mutate({
scheduleId: parseInt(router.query.schedule as string, 10),
name: props.schedule.name,
...values,
});
}}
className="-mx-4 flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
<div className="flex-1">
<div className="rounded-md border-gray-200 bg-white px-4 py-5 sm:border sm:p-6">
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">{t("change_start_end")}</h3>
<Schedule />
</div>
<div className="flex justify-end px-4 pt-4 sm:px-0">
<Button>{t("save")}</Button>
</div>
</div>
<div className="min-w-40 col-span-3 ml-2 space-y-2 lg:col-span-1">
<Controller
name="isDefault"
render={({ field: { onChange, value } }) => (
<Switch label={t("set_to_default")} onCheckedChange={onChange} checked={value} />
)}
/>
<div className="xl:max-w-80 w-full">
<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 text-sm"
onChange={(timezone) => onChange(timezone.value)}
/>
)}
/>
</div>
</div>
<div className="pt-6">
<TextField
name="aciveOn"
label={t("active_on")}
disabled
value={t("nr_event_type_other", { count: props.schedule.eventType?.length })}
className="focus:border-brand mt-1 block w-full rounded-md border-gray-300 text-sm"
<Shell
backPath="/availability"
title={t("availability_title", { availabilityTitle: data?.schedule.name })}
heading={
<EditableHeading title={data?.schedule.name || ""} onChange={(name) => setValue("name", name)} />
}
subtitle={data?.schedule.availability.map((availability) => (
<span key={availability.id}>
{availabilityAsString(availability, { locale: i18n.language })}
<br />
</span>
))}
CTA={
<div className="flex items-center justify-end">
<div className="flex items-center rounded-md px-2 sm:hover:bg-gray-100">
<Label htmlFor="hiddenSwitch" className="mt-2 hidden cursor-pointer self-center pr-2 sm:inline">
{t("set_to_default")}
</Label>
<Switch
id="hiddenSwitch"
disabled={isLoading}
checked={form.watch("isDefault")}
onCheckedChange={(e) => {
form.setValue("isDefault", e);
}}
/>
</div>
<hr className="my-8" />
<div className="rounded-md">
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
<div className="mt-3 flex">
<Button href="/availability/troubleshoot" color="secondary">
{t("launch_troubleshooter")}
</Button>
<VerticalDivider />
<div className="border-l-2 border-gray-300" />
<Button className="ml-4 lg:ml-0" type="submit" form="availability-form">
{t("save")}
</Button>
</div>
}>
<div className="flex items-baseline sm:mt-0">
{/* TODO: Find a better way to guarantee alignment, but for now this'll do. */}
<Icon.FiArrowLeft className=" mr-3 text-transparent hover:cursor-pointer" />
<div className="w-full">
<Form
form={form}
id="availability-form"
handleSubmit={async (values) => {
updateMutation.mutate({
scheduleId: schedule,
...values,
});
}}
className="-mx-4 flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
<div className="flex-1">
<div className="rounded-md border-gray-200 bg-white py-5 pr-4 sm:border sm:p-6">
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">
{t("change_start_end")}
</h3>
<Schedule control={control} name="schedule" />
</div>
</div>
</div>
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
<div className="xl:max-w-80 w-full pr-4 sm:p-0">
<div>
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
{t("timezone")}
</label>
<Controller
name="timeZone"
render={({ field: { onChange, value } }) =>
value ? (
<TimezoneSelect
value={value}
className="focus:border-brand mt-1 block rounded-md border-gray-300 text-sm"
onChange={(timezone) => onChange(timezone.value)}
/>
) : (
<SkeletonText className="h-6 w-full" />
)
}
/>
</div>
<hr className="my-8" />
<div className="rounded-md">
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
<div className="mt-3 flex">
<Button href="/availability/troubleshoot" color="secondary">
{t("launch_troubleshooter")}
</Button>
</div>
</div>
</div>
</div>
</Form>
</div>
</div>
</Form>
);
}
const querySchema = z.object({
schedule: stringOrNumber,
});
export default function Availability() {
const router = useRouter();
const { i18n } = useLocale();
const { schedule: scheduleId } = router.isReady ? querySchema.parse(router.query) : { schedule: -1 };
const query = trpc.useQuery(["viewer.availability.schedule", { scheduleId }], { enabled: router.isReady });
const [name, setName] = useState<string>();
return (
<div>
<QueryCell
query={query}
success={({ data }) => {
return (
<Shell
heading={<EditableHeading title={name || data.schedule.name} onChange={setName} />}
subtitle={data.schedule.availability.map((availability) => (
<span key={availability.id}>
{availabilityAsString(availability, { locale: i18n.language })}
<br />
</span>
))}>
<AvailabilityForm
{...{ ...data, schedule: { ...data.schedule, name: name || data.schedule.name } }}
/>
</Shell>
);
}}
/>
</div>
</Shell>
);
}

View File

@ -631,8 +631,10 @@
"billing": "Billing",
"manage_your_billing_info": "Manage your billing information and cancel your subscription.",
"availability": "Availability",
"availability_title": "{{availabilityTitle}} | Availability",
"edit_availability": "Edit availability",
"configure_availability": "Configure times when you are available for bookings.",
"copy_times_to": "Copy times to",
"change_weekly_schedule": "Change your weekly schedule",
"logo": "Logo",
"error": "Error",
@ -1150,7 +1152,6 @@
"set_availability_getting_started_subtitle_2": "You can customise all of this later in the availability page.",
"connect_calendars_from_app_store": "You can add more calendars from the app store",
"current_step_of_total": "Step {{currentStep}} of {{maxSteps}}",
"copy_all": "Copy All",
"add_variable": "Add variable",
"custom_phone_number": "Custom phone number",
"message_template": "Message template",

View File

@ -1,113 +1,194 @@
import classNames from "classnames";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
Controller,
useFieldArray,
UseFieldArrayAppend,
import { useCallback, useEffect, useMemo, useState, Fragment } from "react";
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import type {
UseFieldArrayRemove,
useFormContext,
FieldValues,
FieldPath,
FieldPathValue,
FieldArrayWithId,
ArrayPath,
ControllerRenderProps,
Control,
} from "react-hook-form";
import { GroupBase, Props } from "react-select";
import dayjs, { ConfigType, Dayjs } from "@calcom/dayjs";
import { defaultDayRange as DEFAULT_DAY_RANGE } from "@calcom/lib/availability";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { TimeRange } from "@calcom/types/schedule";
import { Icon } from "@calcom/ui";
import Dropdown, { DropdownMenuContent, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
import { Button, Select, Switch, Tooltip } from "@calcom/ui/v2";
import { Select, Switch } from "@calcom/ui/v2";
import Button from "@calcom/ui/v2/core/Button";
import { SkeletonText } from "@calcom/ui/v2/core/skeleton";
const Schedule = () => {
const { i18n } = useLocale();
const form = useFormContext();
export type FieldPathByValue<TFieldValues extends FieldValues, TValue> = {
[Key in FieldPath<TFieldValues>]: FieldPathValue<TFieldValues, Key> extends TValue ? Key : never;
}[FieldPath<TFieldValues>];
const initialValue = form.watch();
const ScheduleDay = <TFieldValues extends FieldValues>({
name,
weekday,
control,
CopyButton,
}: {
name: string;
weekday: string;
control: Control<TFieldValues>;
CopyButton: JSX.Element;
}) => {
const { watch, setValue } = useFormContext();
const watchDayRange = watch(name);
const copyAllPosition = (initialValue["schedule"] as Array<TimeRange[]>)?.findIndex(
(item: TimeRange[]) => item.length > 0
return (
<div className="mb-1 flex w-full flex-col py-1 sm:flex-row">
{/* Label & switch container */}
<div className="flex h-11 items-center justify-between">
<div>
<label className="flex flex-row items-center space-x-2">
<div>
<Switch
disabled={!watchDayRange}
defaultChecked={watchDayRange && watchDayRange.length > 0}
checked={watchDayRange && !!watchDayRange.length}
onCheckedChange={(isChecked) => {
setValue(name, isChecked ? [DEFAULT_DAY_RANGE] : []);
}}
/>
</div>
<span className="inline-block min-w-[88px] text-sm capitalize">{weekday}</span>
</label>
</div>
</div>
<>
{watchDayRange ? (
<div className="flex sm:ml-2">
<DayRanges control={control} name={name} />
{!!watchDayRange.length && <div className="mt-1">{CopyButton}</div>}
</div>
) : (
<SkeletonText className="mt-2.5 ml-1 h-6 w-48" />
)}
</>
<div className="my-2 h-[1px] w-full bg-gray-200 sm:hidden" />
</div>
);
};
const CopyButton = ({
getValuesFromDayRange,
weekStart,
}: {
getValuesFromDayRange: string;
weekStart: number;
}) => {
const { t } = useLocale();
const [open, setOpen] = useState(false);
const fieldArrayName = getValuesFromDayRange.substring(0, getValuesFromDayRange.lastIndexOf("."));
const { setValue, getValues } = useFormContext();
return (
<Dropdown open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button
className={classNames(open && "ring-brand-500 !bg-gray-100 outline-none ring-2 ring-offset-1")}
type="button"
tooltip={t("duplicate")}
color="minimal"
size="icon"
StartIcon={Icon.FiCopy}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<CopyTimes
weekStart={weekStart}
disabled={parseInt(getValuesFromDayRange.replace(fieldArrayName + ".", ""), 10)}
onClick={(selected) => {
selected.forEach((day) => setValue(`${fieldArrayName}.${day}`, getValues(getValuesFromDayRange)));
}}
onCancel={() => setOpen(false)}
/>
</DropdownMenuContent>
</Dropdown>
);
};
const Schedule = <
TFieldValues extends FieldValues,
TPath extends FieldPathByValue<TFieldValues, TimeRange[][]>
>({
name,
control,
weekStart = 0,
}: {
name: TPath;
control: Control<TFieldValues>;
weekStart?: number;
}) => {
const { i18n } = useLocale();
return (
<>
{/* First iterate for each day */}
{weekdayNames(i18n.language, 0, "long").map((weekday, num) => {
const name = `schedule.${num}`;
const copyAllShouldRender = copyAllPosition === num;
{weekdayNames(i18n.language, weekStart, "long").map((weekday, num) => {
const weekdayIndex = (num + weekStart) % 7;
const dayRangeName = `${name}.${weekdayIndex}`;
return (
<div className="mb-1 flex w-full flex-col py-1 sm:flex-row" key={weekday}>
{/* Label & switch container */}
<div className="flex h-11 items-center justify-between">
<div>
<label className="flex flex-row items-center space-x-2">
<div>
<Switch
defaultChecked={initialValue["schedule"][num].length > 0}
checked={!!initialValue["schedule"][num].length}
onCheckedChange={(isChecked) => {
form.setValue(name, isChecked ? [DEFAULT_DAY_RANGE] : []);
}}
/>
</div>
<span className="inline-block min-w-[88px] text-sm capitalize">{weekday}</span>
</label>
</div>
<div className="inline sm:hidden">
<ActionButtons
name={name}
setValue={form.setValue}
watcher={form.watch(name, initialValue[name])}
copyAllShouldRender={copyAllShouldRender}
/>
</div>
</div>
<div className="w-full sm:ml-2">
<DayRanges name={name} copyAllShouldRender={copyAllShouldRender} />
</div>
<div className="my-2 h-[1px] w-full bg-gray-200 sm:hidden" />
</div>
<ScheduleDay
name={dayRangeName}
key={weekday}
weekday={weekday}
control={control}
CopyButton={<CopyButton weekStart={weekStart} getValuesFromDayRange={dayRangeName} />}
/>
);
})}
</>
);
};
const DayRanges = ({
const DayRanges = <TFieldValues extends FieldValues>({
name,
copyAllShouldRender,
control,
}: {
name: string;
defaultValue?: TimeRange[];
copyAllShouldRender?: boolean;
control: Control<TFieldValues>;
}) => {
const form = useFormContext();
const { t } = useLocale();
const fields = form.watch(`${name}` as `schedule.0`);
const { remove } = useFieldArray({
name,
const { remove, fields, append } = useFieldArray({
control,
name: name as unknown as ArrayPath<TFieldValues>,
});
return (
<>
{fields.map((field: { id: string }, index: number) => (
<div key={field.id + name} className="mt-2 mb-2 flex rtl:space-x-reverse sm:mt-0">
<TimeRangeField name={`${name}.${index}`} />
{index === 0 && (
<div className="hidden sm:inline">
<ActionButtons
name={name}
setValue={form.setValue}
watcher={form.watch(name)}
copyAllShouldRender={copyAllShouldRender}
<div>
{fields.map((field, index: number) => (
<Fragment key={field.id}>
<div className="mb-2 flex first:mt-1">
<Controller name={`${name}.${index}`} render={({ field }) => <TimeRangeField {...field} />} />
{index === 0 && (
<Button
tooltip={t("add_time_availability")}
className=" text-neutral-400"
type="button"
color="minimal"
size="icon"
StartIcon={Icon.FiPlus}
onClick={() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nextRange: any = getNextRange(fields[fields.length - 1]);
if (nextRange) append(nextRange);
}}
/>
</div>
)}
{index !== 0 && <RemoveTimeButton index={index} remove={remove} />}
</div>
)}
{index !== 0 && <RemoveTimeButton index={index} remove={remove} />}
</div>
</Fragment>
))}
</>
</div>
);
};
@ -132,48 +213,26 @@ const RemoveTimeButton = ({
);
};
interface TimeRangeFieldProps {
name: string;
className?: string;
}
const TimeRangeField = ({ name, className }: TimeRangeFieldProps) => {
const { watch } = useFormContext();
const values = watch(name);
const minEnd = values["start"];
const maxStart = values["end"];
const TimeRangeField = ({ className, value, onChange }: { className?: string } & ControllerRenderProps) => {
// this is a controlled component anyway given it uses LazySelect, so keep it RHF agnostic.
return (
<div className={classNames("mx-1 flex", className)}>
<Controller
name={`${name}.start`}
render={({ field: { onChange } }) => {
return (
<LazySelect
className="h-9 w-[100px]"
value={values["start"]}
max={maxStart}
onChange={(option) => {
onChange(new Date(option?.value as number));
}}
/>
);
<div className={classNames("mx-1", className)}>
<LazySelect
className="inline-block h-9 w-[100px]"
value={value.start}
max={value.end}
onChange={(option) => {
onChange({ ...value, start: new Date(option?.value as number) });
}}
/>
<span className="mx-2 w-2 self-center"> - </span>
<Controller
name={`${name}.end`}
render={({ field: { onChange } }) => (
<LazySelect
className="w-[100px] rounded-md"
value={values["end"]}
min={minEnd}
onChange={(option) => {
onChange(new Date(option?.value as number));
}}
/>
)}
<LazySelect
className="inline-block w-[100px] rounded-md"
value={value.end}
min={value.start}
onChange={(option) => {
onChange({ ...value, end: new Date(option?.value as number) });
}}
/>
</div>
);
@ -266,136 +325,69 @@ const useOptions = () => {
return { options: filteredOptions, filter };
};
const ActionButtons = ({
name,
watcher,
setValue,
copyAllShouldRender,
}: {
name: string;
watcher: TimeRange[];
setValue: (key: string, value: TimeRange[]) => void;
copyAllShouldRender?: boolean;
}) => {
const { t } = useLocale();
const form = useFormContext();
const values = form.watch();
const { append } = useFieldArray({
name,
});
return (
<div className="flex items-center">
<Tooltip content={t("add_time_availability") as string}>
<Button
className="text-neutral-400"
type="button"
color="minimal"
size="icon"
StartIcon={Icon.FiPlus}
onClick={() => {
handleAppend({
fields: watcher,
/* Generics should help with this, but forgive us father as I have sinned */
append: append as unknown as UseFieldArrayAppend<TimeRange>,
});
}}
/>
</Tooltip>
<Dropdown>
<Tooltip content={t("duplicate") as string}>
<DropdownMenuTrigger asChild>
<Button type="button" color="minimal" size="icon" StartIcon={Icon.FiCopy} />
</DropdownMenuTrigger>
</Tooltip>
<DropdownMenuContent>
<CopyTimes
disabled={[parseInt(name.substring(name.lastIndexOf(".") + 1), 10)]}
onApply={(selected) =>
selected.forEach((day) => {
setValue(name.substring(0, name.lastIndexOf(".") + 1) + day, watcher);
})
}
/>
</DropdownMenuContent>
</Dropdown>
{/* This only displays on Desktop */}
{copyAllShouldRender && (
<Tooltip content={t("add_time_availability") as string}>
<Button
color="minimal"
className="whitespace-nowrap text-sm text-neutral-400"
type="button"
onClick={() => {
values["schedule"].forEach((item: TimeRange[], index: number) => {
if (item.length > 0) {
setValue(`schedule.${index}`, watcher);
}
});
}}
title={`${t("copy_all")}`}>
{t("copy_all")}
</Button>
</Tooltip>
)}
</div>
);
};
const handleAppend = ({
fields = [],
append,
}: {
fields: TimeRange[];
append: UseFieldArrayAppend<TimeRange>;
}) => {
if (fields.length === 0) {
return append(DEFAULT_DAY_RANGE);
}
const nextRangeStart = dayjs((fields[fields.length - 1] as unknown as TimeRange).end);
const getNextRange = (field?: FieldArrayWithId) => {
const nextRangeStart = dayjs((field as unknown as TimeRange).end);
const nextRangeEnd = dayjs(nextRangeStart).add(1, "hour");
if (nextRangeEnd.isBefore(nextRangeStart.endOf("day"))) {
return append({
return {
start: nextRangeStart.toDate(),
end: nextRangeEnd.toDate(),
});
};
}
};
const CopyTimes = ({ disabled, onApply }: { disabled: number[]; onApply: (selected: number[]) => void }) => {
const CopyTimes = ({
disabled,
onClick,
onCancel,
weekStart,
}: {
disabled: number;
onClick: (selected: number[]) => void;
onCancel: () => void;
weekStart: number;
}) => {
const [selected, setSelected] = useState<number[]>([]);
const { i18n, t } = useLocale();
return (
<div className="m-4 space-y-2 py-4">
<p className="h6 text-xs font-medium uppercase text-neutral-400">Copy times to</p>
<ol className="space-y-2">
{weekdayNames(i18n.language).map((weekday, num) => (
<li key={weekday}>
<label className="flex w-full items-center justify-between">
<span className="px-1">{weekday}</span>
<input
value={num}
defaultChecked={disabled.includes(num)}
disabled={disabled.includes(num)}
onChange={(e) => {
if (e.target.checked && !selected.includes(num)) {
setSelected(selected.concat([num]));
} else if (!e.target.checked && selected.includes(num)) {
setSelected(selected.slice(selected.indexOf(num), 1));
}
}}
type="checkbox"
className="inline-block rounded-[4px] border-gray-300 text-neutral-900 focus:ring-neutral-500 disabled:text-neutral-400"
/>
</label>
</li>
))}
</ol>
<div className="pt-2">
<Button className="w-full justify-center" color="primary" onClick={() => onApply(selected)}>
<div className="space-y-2 py-2">
<div className="p-2">
<p className="h6 pb-3 pl-1 text-xs font-medium uppercase text-neutral-400">{t("copy_times_to")}</p>
<ol className="space-y-2">
{weekdayNames(i18n.language, weekStart).map((weekday, num) => {
const weekdayIndex = (num + weekStart) % 7;
return (
<li key={weekday}>
<label className="flex w-full items-center justify-between">
<span className="px-1">{weekday}</span>
<input
value={weekdayIndex}
defaultChecked={disabled === weekdayIndex}
disabled={disabled === weekdayIndex}
onChange={(e) => {
if (e.target.checked && !selected.includes(weekdayIndex)) {
setSelected(selected.concat([weekdayIndex]));
} else if (!e.target.checked && selected.includes(weekdayIndex)) {
setSelected(selected.slice(selected.indexOf(weekdayIndex), 1));
}
}}
type="checkbox"
className="inline-block rounded-[4px] border-gray-300 text-neutral-900 focus:ring-neutral-500 disabled:text-neutral-400"
/>
</label>
</li>
);
})}
</ol>
</div>
<hr />
<div className="space-x-2 px-2">
<Button color="minimalSecondary" onClick={() => onCancel()}>
{t("cancel")}
</Button>
<Button color="primary" onClick={() => onClick(selected)}>
{t("apply")}
</Button>
</div>

View File

@ -2,7 +2,7 @@ import { Availability as AvailabilityModel, Prisma, Schedule as ScheduleModel, U
import { z } from "zod";
import { getUserAvailability } from "@calcom/core/getUserAvailability";
import { getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { PrismaClient } from "@calcom/prisma/client";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
import { Schedule } from "@calcom/types/schedule";
@ -114,24 +114,22 @@ export const availabilityRouter = createProtectedRouter()
},
};
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 availability = getAvailabilityFromSchedule(input.schedule || DEFAULT_SCHEDULE);
data.availability = {
createMany: {
data: availability.map((schedule) => ({
days: schedule.days,
startTime: schedule.startTime,
endTime: schedule.endTime,
})),
},
};
const schedule = await prisma.schedule.create({
data,
});
const hasDefaultScheduleId = await hasDefaultSchedule(user, prisma);
if (hasDefaultScheduleId) {
if (!hasDefaultScheduleId) {
await setupDefaultSchedule(user.id, schedule.id, prisma);
}