diff --git a/apps/web/components/v2/eventtype/AvailabilityTab.tsx b/apps/web/components/v2/eventtype/AvailabilityTab.tsx new file mode 100644 index 0000000000..2e4bedb3a6 --- /dev/null +++ b/apps/web/components/v2/eventtype/AvailabilityTab.tsx @@ -0,0 +1,144 @@ +import { FormValues } from "pages/v2/event-types/[type]"; +import { Controller, useFormContext } from "react-hook-form"; + +import dayjs from "@calcom/dayjs"; +import classNames from "@calcom/lib/classNames"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { weekdayNames } from "@calcom/lib/weekday"; +import { trpc } from "@calcom/trpc/react"; +import { Icon } from "@calcom/ui"; +import Button from "@calcom/ui/v2/core/Button"; +import Select from "@calcom/ui/v2/core/form/Select"; +import { SkeletonText } from "@calcom/ui/v2/core/skeleton"; + +import { AvailabilitySelectSkeletonLoader } from "@components/availability/SkeletonLoader"; + +type AvailabilityOption = { + label: string; + value: number; +}; + +const AvailabilitySelect = ({ + className = "", + ...props +}: { + className?: string; + name: string; + value: number; + onBlur: () => void; + onChange: (value: AvailabilityOption | null) => void; +}) => { + const { data, isLoading } = trpc.useQuery(["viewer.availability.list"]); + if (isLoading) { + return ; + } + + const schedules = data?.schedules || []; + + const options = schedules.map((schedule) => ({ + value: schedule.id, + label: schedule.name, + })); + + const value = options.find((option) => + props.value + ? option.value === props.value + : option.value === schedules.find((schedule) => schedule.isDefault)?.id + ); + + return ( + + ); +}; + +const format = (date: Date) => + Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "numeric" }).format( + new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss")) + ); + +export const AvailabilityTab = () => { + const { t, i18n } = useLocale(); + const { watch } = useFormContext(); + + const scheduleId = watch("schedule"); + const { isLoading, data: schedule } = trpc.useQuery(["viewer.availability.schedule", { scheduleId }]); + + const filterDays = (dayNum: number) => + schedule?.schedule.availability.filter((item) => item.days.includes((dayNum + 1) % 7)) || []; + + return ( + <> + + + + {t("availability")} + + + ( + { + field.onChange(selected?.value || null); + }} + /> + )} + /> + + + + + {weekdayNames(i18n.language, 1, "long").map((day, index) => { + const isAvailable = !!filterDays(index).length; + return ( + + + {day} + + {isLoading ? ( + + ) : isAvailable ? ( + + {filterDays(index).map((dayRange, i) => ( + + {format(dayRange.startTime)} + - + {format(dayRange.endTime)} + + ))} + + ) : ( + {t("unavailable")} + )} + + ); + })} + + + + + + {schedule?.timeZone || } + + + {t("edit_availability")} + + + + > + ); +}; diff --git a/apps/web/pages/v2/event-types/[type]/index.tsx b/apps/web/pages/v2/event-types/[type]/index.tsx index 5815c169ea..8246c08afc 100644 --- a/apps/web/pages/v2/event-types/[type]/index.tsx +++ b/apps/web/pages/v2/event-types/[type]/index.tsx @@ -23,6 +23,7 @@ import { getSession } from "@lib/auth"; import { HttpError } from "@lib/core/http/error"; import { inferSSRProps } from "@lib/types/inferSSRProps"; +import { AvailabilityTab } from "@components/v2/eventtype/AvailabilityTab"; // These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings import { EventAdvancedTab } from "@components/v2/eventtype/EventAdvancedTab"; import { EventAppsTab } from "@components/v2/eventtype/EventAppsTab"; @@ -35,6 +36,8 @@ import EventWorkflowsTab from "@components/v2/eventtype/EventWorkfowsTab"; import { getTranslation } from "@server/lib/i18n"; +const TABS_WITHOUT_ACTION_BUTTONS = ["workflows", "availability"]; + export type FormValues = { title: string; eventTitle: string; @@ -136,7 +139,7 @@ const EventTypePage = (props: inferSSRProps) => { locations: eventType.locations || [], recurringEvent: eventType.recurringEvent || null, description: eventType.description ?? undefined, - schedule: eventType.schedule?.id, + schedule: eventType.schedule || undefined, hidden: eventType.hidden, periodDates: { startDate: periodDates.startDate, @@ -155,8 +158,7 @@ const EventTypePage = (props: inferSSRProps) => { teamMembers={teamMembers} /> ), - /* TODO: Actually make this tab */ - availability: null, + availability: , team: ( ) => { }} className="space-y-6"> {tabMap[tabName]} - {tabName !== "workflows" && ( + {!TABS_WITHOUT_ACTION_BUTTONS.includes(tabName) && ( {t("cancel")} @@ -272,6 +274,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => email: true, plan: true, locale: true, + defaultScheduleId: true, }); const rawEventType = await prisma.eventType.findFirst({ @@ -314,7 +317,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => hidden: true, locations: true, eventName: true, - availability: true, customInputs: true, timeZone: true, periodType: true, @@ -408,6 +410,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const { locations, metadata, ...restEventType } = rawEventType; const eventType = { ...restEventType, + schedule: rawEventType.schedule?.id || rawEventType.users[0].defaultScheduleId, recurringEvent: parseRecurringEvent(restEventType.recurringEvent), locations: locations as unknown as LocationObject[], metadata: (metadata || {}) as JSONObject, @@ -435,23 +438,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => (credentials.find((integration) => integration.type === "stripe_payment")?.key as unknown as StripeData) ?.default_currency || "usd"; - type Availability = typeof eventType["availability"]; - const getAvailability = (availability: Availability) => - availability?.length - ? availability.map((schedule) => ({ - ...schedule, - startTime: new Date(new Date().toDateString() + " " + schedule.startTime.toTimeString()).valueOf(), - endTime: new Date(new Date().toDateString() + " " + schedule.endTime.toTimeString()).valueOf(), - })) - : null; - - const availability = getAvailability(eventType.availability) || []; - availability.sort((a, b) => a.startTime - b.startTime); - const eventTypeObject = Object.assign({}, eventType, { periodStartDate: eventType.periodStartDate?.toString() ?? null, periodEndDate: eventType.periodEndDate?.toString() ?? null, - availability, }); const teamMembers = eventTypeObject.team @@ -472,7 +461,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => session, eventType: eventTypeObject, locationOptions, - availability, team: eventTypeObject.team || null, teamMembers, hasPaymentIntegration, diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index bf55a206e4..c37ab3ed50 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -628,6 +628,7 @@ "billing": "Billing", "manage_your_billing_info": "Manage your billing information and cancel your subscription.", "availability": "Availability", + "edit_availability": "Edit availability", "configure_availability": "Configure times when you are available for bookings.", "change_weekly_schedule": "Change your weekly schedule", "logo": "Logo",