// TODO: replace headlessui with radix-ui import { Disclosure, RadioGroup } from "@headlessui/react"; import { PhoneIcon, XIcon } from "@heroicons/react/outline"; import { ChevronRightIcon, ClockIcon, DocumentIcon, ExternalLinkIcon, LinkIcon, LocationMarkerIcon, PencilIcon, PlusIcon, TrashIcon, UserAddIcon, UsersIcon, } from "@heroicons/react/solid"; import { EventTypeCustomInput, Prisma, SchedulingType } from "@prisma/client"; 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, useRef, useState } from "react"; import { FormattedNumber, IntlProvider } from "react-intl"; import { useMutation } from "react-query"; import Select, { OptionTypeBase } from "react-select"; import { StripeData } from "@ee/lib/stripe/server"; import { asNumberOrThrow, asNumberOrUndefined, asStringOrThrow, asStringOrUndefined, } from "@lib/asStringOrNull"; import { getSession } from "@lib/auth"; import classNames from "@lib/classNames"; 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 { Dialog, DialogContent, DialogTrigger } from "@components/Dialog"; import Shell from "@components/Shell"; import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; import CustomInputTypeForm from "@components/eventtype/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); const PERIOD_TYPES = [ { type: "rolling", suffix: "into the future", }, { type: "range", prefix: "Within a date range", }, { type: "unlimited", prefix: "Indefinitely into the future", }, ]; const EventTypePage = (props: inferSSRProps) => { const { eventType, locationOptions, availability, team, teamMembers, hasPaymentIntegration, currency } = props; const { t } = useLocale(); const router = useRouter(); const updateMutation = useMutation(updateEventType, { onSuccess: async ({ eventType }) => { await router.push("/event-types"); showToast(`${eventType.title} event type updated successfully`, "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("Event type deleted successfully", "success"); }, onError: (err: HttpError) => { const message = `${err.statusCode}: ${err.message}`; showToast(message, "error"); }, }); const [users, setUsers] = useState([]); const [editIcon, setEditIcon] = useState(true); const [enteredAvailability, setEnteredAvailability] = useState(); const [showLocationModal, setShowLocationModal] = useState(false); const [selectedTimeZone, setSelectedTimeZone] = useState(""); const [selectedLocation, setSelectedLocation] = useState(undefined); const [locations, setLocations] = useState(eventType.locations || []); 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, setPeriodType] = useState(() => { return ( PERIOD_TYPES.find((s) => s.type === eventType.periodType) || PERIOD_TYPES.find((s) => s.type === "unlimited") ); }); const [requirePayment, setRequirePayment] = useState(eventType.price > 0); const [hidden, setHidden] = useState(eventType.hidden); const titleRef = useRef(null); const eventNameRef = useRef(null); const [advancedSettingsVisible, setAdvancedSettingsVisible] = useState(false); useEffect(() => { setSelectedTimeZone(eventType.timeZone || ""); }, []); async function updateEventTypeHandler(event: React.FormEvent) { event.preventDefault(); const formData = Object.fromEntries(new FormData(event.currentTarget).entries()); const enteredTitle: string = titleRef.current!.value; const advancedPayload: AdvancedOptions = {}; if (advancedSettingsVisible) { advancedPayload.eventName = eventNameRef.current.value; advancedPayload.periodType = periodType?.type; advancedPayload.periodDays = asNumberOrUndefined(formData.periodDays); advancedPayload.periodCountCalendarDays = Boolean( asNumberOrUndefined(formData.periodCountCalendarDays) ); advancedPayload.periodStartDate = periodDates.startDate || undefined; advancedPayload.periodEndDate = periodDates.endDate || undefined; advancedPayload.minimumBookingNotice = asNumberOrUndefined(formData.minimumBookingNotice); // prettier-ignore advancedPayload.price = !requirePayment ? undefined : formData.price ? Math.round(parseFloat(asStringOrThrow(formData.price)) * 100) : /* otherwise */ 0; advancedPayload.currency = currency; advancedPayload.availability = enteredAvailability || undefined; advancedPayload.customInputs = customInputs; advancedPayload.timeZone = selectedTimeZone; advancedPayload.disableGuests = formData.disableGuests === "on"; advancedPayload.requiresConfirmation = formData.requiresConfirmation === "on"; } const payload: EventTypeInput = { id: eventType.id, title: enteredTitle, slug: asStringOrThrow(formData.slug), description: asStringOrThrow(formData.description), length: asNumberOrThrow(formData.length), hidden, locations, ...advancedPayload, ...(team ? { schedulingType: formData.schedulingType as SchedulingType, users, } : {}), }; updateMutation.mutate(payload); } 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 closeLocationModal = () => { setSelectedLocation(undefined); setShowLocationModal(false); }; const updateLocations = (e: React.FormEvent) => { e.preventDefault(); const newLocation = e.currentTarget.location.value; let details = {}; if (newLocation === LocationType.InPerson) { details = { address: e.currentTarget.address.value }; } const existingIdx = locations.findIndex((loc) => newLocation === loc.type); if (existingIdx !== -1) { const copy = locations; copy[existingIdx] = { ...locations[existingIdx], ...details }; setLocations(copy); } else { setLocations(locations.concat({ type: newLocation, ...details })); } setShowLocationModal(false); }; const removeLocation = (selectedLocation: typeof eventType.locations[number]) => { setLocations(locations.filter((location) => location.type !== selectedLocation.type)); }; 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) => { customInputs.splice(index, 1); setCustomInputs([...customInputs]); }; const schedulingTypeOptions: { value: SchedulingType; label: string; description: string }[] = [ { value: SchedulingType.COLLECTIVE, label: "Collective", description: "Schedule meetings when all selected team members are available.", }, { value: SchedulingType.ROUND_ROBIN, label: "Round Robin", description: "Cycle meetings between multiple team members.", }, ]; const [periodDates, setPeriodDates] = 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 || ""}`, }); return (
setEditIcon(false)}> {editIcon && ( )}
} subtitle={eventType.description || ""}>
{process.env.NEXT_PUBLIC_APP_URL?.replace(/^(https?:|)\/\//, "")}/ {team ? "team/" + team.slug : eventType.users[0].username}/
{t("duration")} } name="length" id="length" required placeholder="15" defaultValue={eventType.length || 15} />

{locations.length === 0 && (
{team &&
} {team && (
setUsers(options.map((option) => option.value))} defaultValue={eventType.users.map(mapUserToValue)} options={teamMembers.map(mapUserToValue)} id="users" placeholder={t("add_attendees")} />
)} {({ open }) => ( <>
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 ? "Required" : "Optional"}
  • ))}

{t("date_range")}
{PERIOD_TYPES.map((period) => ( classNames( checked ? "border-secondary-200 z-10" : "border-gray-200", "relative min-h-12 flex items-center cursor-pointer focus:outline-none" ) }> {({ active, checked }) => ( <>
{period.prefix ? {period.prefix}  : null} {period.type === "rolling" && (
)} {checked && period.type === "range" && (
)} {period.suffix ?  {period.suffix} : null}
)}
))}

{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 } />
{new Intl.NumberFormat("en", { style: "currency", currency: currency, maximumSignificantDigits: 1, maximumFractionDigits: 0, }) .format(0) .replace("0", "")}
)}
)}
)}
{t("delete")} {t("delete_event_type_description")}
{showLocationModal && (