import { PhoneIcon, XIcon } from "@heroicons/react/outline"; import { ChevronRightIcon, DocumentIcon, ExternalLinkIcon, ClockIcon, LinkIcon, LocationMarkerIcon, PencilIcon, PlusIcon, TrashIcon, UserAddIcon, UsersIcon, } from "@heroicons/react/solid"; import { EventTypeCustomInput, Prisma, SchedulingType } from "@prisma/client"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible"; import * as RadioGroup from "@radix-ui/react-radio-group"; import dayjs from "dayjs"; import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; import { GetServerSidePropsContext } from "next"; import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; import { useForm, Controller } from "react-hook-form"; import { FormattedNumber, IntlProvider } from "react-intl"; import { useMutation } from "react-query"; import Select from "react-select"; import { StripeData } from "@ee/lib/stripe/server"; import { asNumberOrUndefined, asStringOrThrow, asStringOrUndefined } from "@lib/asStringOrNull"; import { getSession } from "@lib/auth"; import { HttpError } from "@lib/core/http/error"; import { useLocale } from "@lib/hooks/useLocale"; import getIntegrations, { hasIntegration } from "@lib/integrations/getIntegrations"; import { LocationType } from "@lib/location"; import deleteEventType from "@lib/mutations/event-types/delete-event-type"; import updateEventType from "@lib/mutations/event-types/update-event-type"; import showToast from "@lib/notification"; import prisma from "@lib/prisma"; import { defaultAvatarSrc } from "@lib/profile"; import { AdvancedOptions, EventTypeInput } from "@lib/types/event-type"; import { inferSSRProps } from "@lib/types/inferSSRProps"; import { WorkingHours } from "@lib/types/schedule"; import { Dialog, DialogContent, DialogTrigger } from "@components/Dialog"; import Shell from "@components/Shell"; import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; import CustomInputTypeForm from "@components/pages/eventtypes/CustomInputTypeForm"; import Button from "@components/ui/Button"; import { Scheduler } from "@components/ui/Scheduler"; import Switch from "@components/ui/Switch"; import CheckboxField from "@components/ui/form/CheckboxField"; import CheckedSelect from "@components/ui/form/CheckedSelect"; import { DateRangePicker } from "@components/ui/form/DateRangePicker"; import MinutesField from "@components/ui/form/MinutesField"; import * as RadioArea from "@components/ui/form/radio-area"; dayjs.extend(utc); dayjs.extend(timezone); type OptionTypeBase = { label: string; value: LocationType; disabled?: boolean; }; const addDefaultLocationOptions = ( defaultLocations: OptionTypeBase[], locationOptions: OptionTypeBase[] ): void => { const existingLocationOptions = locationOptions.flatMap((locationOptionItem) => [locationOptionItem.value]); defaultLocations.map((item) => { if (!existingLocationOptions.includes(item.value)) { locationOptions.push(item); } }); }; const EventTypePage = (props: inferSSRProps) => { const { t } = useLocale(); const PERIOD_TYPES = [ { type: "ROLLING" as const, suffix: t("into_the_future"), }, { type: "RANGE" as const, prefix: t("within_date_range"), }, { type: "UNLIMITED" as const, prefix: t("indefinitely_into_future"), }, ]; const { eventType, locationOptions, availability, team, teamMembers, hasPaymentIntegration, currency } = props; /** Appending default locations */ const defaultLocations = [ { value: LocationType.InPerson, label: t("in_person_meeting") }, { value: LocationType.Phone, label: t("phone_call") }, ]; addDefaultLocationOptions(defaultLocations, locationOptions); const router = useRouter(); const updateMutation = useMutation(updateEventType, { onSuccess: async ({ eventType }) => { await router.push("/event-types"); showToast(t("event_type_updated_successfully", { eventTypeTitle: eventType.title }), "success"); }, onError: (err: HttpError) => { const message = `${err.statusCode}: ${err.message}`; showToast(message, "error"); }, }); const deleteMutation = useMutation(deleteEventType, { onSuccess: async () => { await router.push("/event-types"); showToast(t("event_type_deleted_successfully"), "success"); }, onError: (err: HttpError) => { const message = `${err.statusCode}: ${err.message}`; showToast(message, "error"); }, }); const [editIcon, setEditIcon] = useState(true); const [showLocationModal, setShowLocationModal] = useState(false); const [selectedTimeZone, setSelectedTimeZone] = useState(""); const [selectedLocation, setSelectedLocation] = useState(undefined); const [selectedCustomInput, setSelectedCustomInput] = useState(undefined); const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false); const [customInputs, setCustomInputs] = useState( eventType.customInputs.sort((a, b) => a.id - b.id) || [] ); const periodType = PERIOD_TYPES.find((s) => s.type === eventType.periodType) || PERIOD_TYPES.find((s) => s.type === "UNLIMITED"); const [requirePayment, setRequirePayment] = useState(eventType.price > 0); const [advancedSettingsVisible, setAdvancedSettingsVisible] = useState(false); useEffect(() => { setSelectedTimeZone(eventType.timeZone || ""); }, []); async function deleteEventTypeHandler(event: React.MouseEvent) { event.preventDefault(); const payload = { id: eventType.id }; deleteMutation.mutate(payload); } const openLocationModal = (type: LocationType) => { setSelectedLocation(locationOptions.find((option) => option.value === type)); setShowLocationModal(true); }; const removeLocation = (selectedLocation: typeof eventType.locations[number]) => { formMethods.setValue( "locations", formMethods.getValues("locations").filter((location) => location.type !== selectedLocation.type), { shouldValidate: true } ); }; const LocationOptions = () => { if (!selectedLocation) { return null; } switch (selectedLocation.value) { case LocationType.InPerson: return (
location.type === LocationType.InPerson)?.address } />
); case LocationType.Phone: return

{t("cal_invitee_phone_number_scheduling")}

; case LocationType.GoogleMeet: return

{t("cal_provide_google_meet_location")}

; case LocationType.Zoom: return

{t("cal_provide_zoom_meeting_url")}

; case LocationType.Daily: return

{t("cal_provide_video_meeting_url")}

; } return null; }; const removeCustom = (index: number) => { formMethods.getValues("customInputs").splice(index, 1); customInputs.splice(index, 1); setCustomInputs([...customInputs]); }; const schedulingTypeOptions: { value: SchedulingType; label: string; description: string }[] = [ { value: SchedulingType.COLLECTIVE, label: t("collective"), description: t("collective_description"), }, { value: SchedulingType.ROUND_ROBIN, label: t("round_robin"), description: t("round_robin_description"), }, ]; const [periodDates] = useState<{ startDate: Date; endDate: Date }>({ startDate: new Date(eventType.periodStartDate || Date.now()), endDate: new Date(eventType.periodEndDate || Date.now()), }); const permalink = `${process.env.NEXT_PUBLIC_APP_URL}/${ team ? `team/${team.slug}` : eventType.users[0].username }/${eventType.slug}`; const mapUserToValue = ({ id, name, avatar, }: { id: number | null; name: string | null; avatar: string | null; }) => ({ value: `${id || ""}`, label: `${name || ""}`, avatar: `${avatar || ""}`, }); const formMethods = useForm<{ title: string; eventTitle: string; slug: string; length: number; description: string; disableGuests: boolean; requiresConfirmation: boolean; schedulingType: SchedulingType | null; price: number; isHidden: boolean; locations: { type: LocationType; address?: string }[]; customInputs: EventTypeCustomInput[]; users: string[]; scheduler: { enteredAvailability: { openingHours: WorkingHours[]; dateOverrides: WorkingHours[] }; selectedTimezone: string; }; periodType: string | number; periodDays: number; periodDaysType: string; periodDates: { startDate: Date; endDate: Date }; minimumBookingNotice: number; }>({ defaultValues: { locations: eventType.locations || [], periodDates: { startDate: periodDates.startDate, endDate: periodDates.endDate, }, }, }); const locationFormMethods = useForm<{ locationType: LocationType; locationAddress: string; }>(); const Locations = () => { return (
{formMethods.getValues("locations").length === 0 && (
)}
} subtitle={eventType.description || ""}>
{ const enteredTitle: string = values.title; const advancedPayload: AdvancedOptions = {}; if (advancedSettingsVisible) { advancedPayload.eventName = values.eventTitle; advancedPayload.periodType = asStringOrUndefined(values.periodType); advancedPayload.periodDays = asNumberOrUndefined(values.periodDays); advancedPayload.periodCountCalendarDays = Boolean(parseInt(values.periodDaysType)); advancedPayload.periodStartDate = values.periodDates.startDate || undefined; advancedPayload.periodEndDate = values.periodDates.endDate || undefined; advancedPayload.minimumBookingNotice = values.minimumBookingNotice; // prettier-ignore advancedPayload.price = !requirePayment ? undefined : values.price ? Math.round(parseFloat(asStringOrThrow(values.price)) * 100) : /* otherwise */ 0; advancedPayload.currency = currency; // advancedPayload.availability = values.scheduler.enteredAvailability || undefined; advancedPayload.customInputs = values.customInputs; advancedPayload.timeZone = values.scheduler.selectedTimezone; advancedPayload.disableGuests = values.disableGuests; advancedPayload.requiresConfirmation = values.requiresConfirmation; } const payload: EventTypeInput = { id: eventType.id, title: enteredTitle, slug: asStringOrThrow(values.slug), description: asStringOrThrow(values.description), length: values.length, hidden: values.isHidden, locations: values.locations, ...advancedPayload, ...(team ? { schedulingType: values.schedulingType as SchedulingType, users: values.users, } : {}), }; updateMutation.mutate(payload); })} className="space-y-6">
{process.env.NEXT_PUBLIC_APP_URL?.replace(/^(https?:|)\/\//, "")}/ {team ? "team/" + team.slug : eventType.users[0].username}/
( {t("duration")} } id="length" required min="10" placeholder="15" defaultValue={eventType.length || 15} onChange={(e) => { formMethods.setValue("length", Number(e.target.value)); }} /> )} />

} />

{team &&
} {team && (
( { // FIXME // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore formMethods.setValue("schedulingType", val); }} /> )} />
user.id.toString())} render={() => ( { formMethods.setValue( "users", options.map((user) => user.value) ); }} defaultValue={eventType.users.map(mapUserToValue)} options={teamMembers.map(mapUserToValue)} placeholder={t("add_attendees")} /> )} />
)} setAdvancedSettingsVisible(!advancedSettingsVisible)}> <> {t("show_advanced_settings")}
    {customInputs.map((customInput: EventTypeCustomInput, idx: number) => (
  • {t("label")}: {customInput.label}
    {customInput.placeholder && (
    {t("placeholder")}: {customInput.placeholder}
    )}
    {t("type")}: {customInput.type}
    {customInput.required ? t("required") : t("optional")}
  • ))}
( { formMethods.setValue("requiresConfirmation", e?.target.checked); }} /> )} /> ( { formMethods.setValue("disableGuests", e?.target.checked); }} /> )} />
( { formMethods.setValue("minimumBookingNotice", Number(e.target.value)); }} /> )} />
( formMethods.setValue("periodType", val)}> {PERIOD_TYPES.map((period) => (
{period.prefix ? {period.prefix}  : null} {period.type === "ROLLING" && (
{ formMethods.setValue("periodDays", Number(e.target.value)); }} />
)} {period.type === "RANGE" && (
( { formMethods.setValue("periodDates", { startDate, endDate }); }} /> )} />
)} {period.suffix ? (  {period.suffix} ) : null}
))}
)} />

( { formMethods.setValue("scheduler.enteredAvailability", { openingHours: val.openingHours, dateOverrides: val.dateOverrides, }); }} setTimeZone={(timeZone) => { formMethods.setValue("scheduler.selectedTimezone", timeZone); setSelectedTimeZone(timeZone); }} timeZone={selectedTimeZone} availability={availability.map((schedule) => ({ ...schedule, startTime: new Date(schedule.startTime), endTime: new Date(schedule.endTime), }))} /> )} />
{hasPaymentIntegration && ( <>
setRequirePayment(event.target.checked)} id="requirePayment" name="requirePayment" type="checkbox" className="w-4 h-4 border-gray-300 rounded focus:ring-primary-500 text-primary-600" defaultChecked={requirePayment} />

{t("require_payment")} (0.5% +{" "} {" "} {t("commission_per_transaction")})

{requirePayment && (
0 ? eventType.price / 100.0 : undefined } {...formMethods.register("price")} />
{new Intl.NumberFormat("en", { style: "currency", currency: currency, maximumSignificantDigits: 1, maximumFractionDigits: 0, }) .format(0) .replace("0", "")}
)}
)}
{/* )} */}
( { formMethods.setValue("isHidden", isChecked); }} label={t("hide_event_type")} /> )} />
{t("delete")} {t("delete_event_type_description")}

{t("this_input_will_shown_booking_this_event")}

{ const newLocation = values.locationType; let details = {}; if (newLocation === LocationType.InPerson) { details = { address: values.locationAddress }; } const existingIdx = formMethods .getValues("locations") .findIndex((loc) => values.locationType === loc.type); if (existingIdx !== -1) { const copy = formMethods.getValues("locations"); copy[existingIdx] = { ...formMethods.getValues("locations")[existingIdx], ...details }; formMethods.setValue("locations", copy); } else { formMethods.setValue( "locations", formMethods.getValues("locations").concat({ type: values.locationType, ...details }) ); } setShowLocationModal(false); })}> (