import { useMemo, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import dayjs from "@calcom/dayjs"; import { Dialog } from "@calcom/features/components/controlled-dialog"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useDebounce } from "@calcom/lib/hooks/useDebounce"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import classNames from "@calcom/ui/classNames"; import { Alert } from "@calcom/ui/components/alert"; import { Button } from "@calcom/ui/components/button"; import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog"; import { DateRangePicker, TextArea, Input, Checkbox } from "@calcom/ui/components/form"; import { Label } from "@calcom/ui/components/form"; import { Select } from "@calcom/ui/components/form"; import { Switch } from "@calcom/ui/components/form"; import { showToast } from "@calcom/ui/components/toast"; import { useHasTeamPlan } from "@calcom/web/modules/billing/hooks/useHasPaidPlan"; import { UpgradeTeamsBadgeWebWrapper as UpgradeTeamsBadge } from "~/billing/components/UpgradeTeamsBadgeWebWrapper"; import { OutOfOfficeTab } from "~/settings/outOfOffice/OutOfOfficeToggleGroup"; export type { BookingRedirectForm } from "~/settings/outOfOffice/types"; import type { BookingRedirectForm } from "~/settings/outOfOffice/types"; type Option = { value: number; label: string }; export const CreateOrEditOutOfOfficeEntryModal = ({ openModal, closeModal, currentlyEditingOutOfOfficeEntry, }: { openModal: boolean; closeModal: () => void; currentlyEditingOutOfOfficeEntry: BookingRedirectForm | null; }) => { const { t } = useLocale(); const utils = trpc.useUtils(); const me = useMeQuery(); const searchParams = useCompatSearchParams(); const oooType = searchParams?.get("type") ?? OutOfOfficeTab.MINE; const [searchMember, setSearchMember] = useState(""); const debouncedSearchMember = useDebounce(searchMember, 500); const oooForMembers = trpc.viewer.teams.legacyListMembers.useInfiniteQuery( { limit: 10, searchText: debouncedSearchMember, adminOrOwnedTeamsOnly: true }, { enabled: oooType === OutOfOfficeTab.TEAM, getNextPageParam: (lastPage) => lastPage.nextCursor, } ); const oooMemberListOptions: { value: number; label: string; avatarUrl: string | null; }[] = currentlyEditingOutOfOfficeEntry ? [ { value: currentlyEditingOutOfOfficeEntry.forUserId || -1, label: currentlyEditingOutOfOfficeEntry.forUserName || "", avatarUrl: currentlyEditingOutOfOfficeEntry.forUserAvatar || "", }, ] : oooForMembers?.data?.pages .flatMap((page) => page.members) ?.filter((member) => me?.data?.id !== member.id) .map((member) => ({ value: member.id, label: member.name || member.username || "", avatarUrl: member.avatarUrl, })) || []; const [searchRedirectMember, setSearchRedirectMember] = useState(""); const debouncedSearchRedirect = useDebounce(searchRedirectMember, 500); const redirectMembers = trpc.viewer.teams.legacyListMembers.useInfiniteQuery( { limit: 10, searchText: debouncedSearchRedirect, adminOrOwnedTeamsOnly: oooType === OutOfOfficeTab.TEAM, }, { enabled: true, getNextPageParam: (lastPage) => lastPage.nextCursor, } ); const redirectToMemberListOptions: { value: number; label: string; avatarUrl: string | null; }[] = redirectMembers?.data?.pages .flatMap((page) => page.members) ?.filter((member) => oooType === OutOfOfficeTab.MINE ? me?.data?.id !== member.id : oooType === OutOfOfficeTab.TEAM ) .map((member) => ({ value: member.id, label: member.name || member.username || "", avatarUrl: member.avatarUrl, })) || []; const { data: outOfOfficeReasonList, isPending: isReasonListPending } = trpc.viewer.ooo.outOfOfficeReasonList.useQuery(); const reasonList = (outOfOfficeReasonList || []).map((reason) => ({ label: `${reason.emoji} ${reason.userId === null ? t(reason.reason) : reason.reason}`, value: reason.id, })); const [profileRedirect, setProfileRedirect] = useState(!!currentlyEditingOutOfOfficeEntry?.toTeamUserId); const { hasTeamPlan } = useHasTeamPlan(); const { handleSubmit, setValue, control, register, watch, formState: { isSubmitting }, getValues, } = useForm({ defaultValues: currentlyEditingOutOfOfficeEntry ? currentlyEditingOutOfOfficeEntry : { dateRange: { startDate: dayjs().startOf("d").toDate(), endDate: dayjs().startOf("d").add(2, "d").toDate(), }, startDateOffset: dayjs().utcOffset(), endDateOffset: dayjs().utcOffset(), toTeamUserId: null, reasonId: 1, forUserId: null, showNotePublicly: false, }, }); const watchedTeamUserId = watch("toTeamUserId"); const watchForUserId = watch("forUserId"); const watchedDateRange = watch("dateRange"); const watchedNotes = watch("notes"); const hasValidNotes = Boolean(watchedNotes?.trim()); // Fetch user's holiday settings to show warning if OOO dates overlap with holidays const { data: holidaySettings } = trpc.viewer.holidays.getUserSettings.useQuery({}); // Check if selected dates overlap with any enabled holidays const overlappingHolidays = useMemo(() => { if (!holidaySettings?.countryCode || !watchedDateRange?.startDate || !watchedDateRange?.endDate) { return []; } // Filter holidays that are enabled and fall within the date range const startStr = dayjs(watchedDateRange.startDate).format("YYYY-MM-DD"); const endStr = dayjs(watchedDateRange.endDate).format("YYYY-MM-DD"); return (holidaySettings.holidays || []) .filter((h) => h.enabled && h.date >= startStr && h.date <= endStr) .map((h) => ({ date: h.date, holiday: { id: h.id, name: h.name } })); }, [holidaySettings, watchedDateRange]); const createOrEditOutOfOfficeEntry = trpc.viewer.ooo.outOfOfficeCreateOrUpdate.useMutation({ onSuccess: () => { showToast( currentlyEditingOutOfOfficeEntry ? t("success_edited_entry_out_of_office") : t("success_entry_created"), "success" ); utils.viewer.ooo.outOfOfficeEntriesList.invalidate(); closeModal(); }, onError: (error) => { showToast(t(error.message), "error"); }, }); return ( { if (!open) { closeModal(); } }}> { event.preventDefault(); }}>
{ if (!data.dateRange.endDate) { showToast(t("end_date_not_selected"), "error"); } else { createOrEditOutOfOfficeEntry.mutate({ ...data, startDateOffset: -1 * data.dateRange.startDate.getTimezoneOffset(), endDateOffset: -1 * data.dateRange.endDate.getTimezoneOffset(), }); } })}>
{/* In case of Team, Select Member for whom OOO is created */} {oooType === OutOfOfficeTab.TEAM && (
(