From 3986d61f400f929c9a7f3c563e1a0efe6ef2a2b3 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Wed, 10 Dec 2025 14:40:04 +0100 Subject: [PATCH] feat(bookings): improve bookings redesign (#25251) * use booking.uid instead of booking.id for url param * show timezone on calendar * fix type * restore horizontal tab and remove header and subtitle * clean up sidebar items * fix event propagation from attendees * fetch all statuses except for cancelled on calendar view * clean up styles of the badges on BookingListItem * fix useMediaQuery * add close button to the header * add assignment reason to the details sheet * use separator row * use ToggleGroup for the top bookings tab * move ViewToggleButton * resize the action button * remove wrong prop * fix type error * fix type error * hide view toggle button on mobile (and fix the breakpoint) * remove unused e2e tests * fix e2e tests * hide toggle button when feature flag is off * update skeleton * improve attendees on booking list item and slide over * improve attendee dropdown * fix type error * move query to containers * select attendee email * infinite fetching for calendar view * update styles * fix compatibility * fix: add backward compatibility for status field in getAllUserBookings * increase calendar height * fix type error * support Member filter only for admin / owners * add debug log (TEMP) * add event border color * show Reject / Accept buttons on BookingDetailsSheet * move description section to the top * update When section * update style of Who section * add CancelBookingDialog WIP * fix CancelBookingDialog * increase clickable area * add schedule info section WIP * fix flaky reject button * fixing reschedule info WIP * add fromReschedule index to Booking * improve rescheduled information * improve reassignment * fix type error * fix unit test * respect user's weekStart value on the booking calendar view * update debug log * improve payment section * clean up * fix log message * reposition filters on list view * fix bookings controller api2 e2e test * clean up file by extracting logic into custom hooks * rename files * merge BookingCalendar into its container * extract logic into separate hook files * remove redundant logic * rearrange items on calendar view * add WeekPicker * extract filter button * responsive header on list view * horizontal scroll for ToggleGroup WIP * fix type error * fix cancelling recurring event * address feedback * fix e2e tests * fix unit test * fix e2e tests * make hover style more visible for ToggleGroup * fix margin on CancelBookingDialog * update styles on the slide over (mostly font weight) * update style of CancelBookingDialog * update styles * update margin top for the header * refactor getBookingDetails handler * fix gap in who section * auto-filter the current user on the calendar view * calculate calendar height considering top banners * improve booking details sheet interaction without overlay * update calendar event styles * update reject dialog style * put uid first in the query params * fix class name * memoize functions in useMediaQuery * query attendee with id instead of email * update margins * replace TRPCError with ErrorWithCode * move calculation outside loop * remove dead code --- .../(main-nav)/bookings/[status]/page.tsx | 10 +- .../wipemycalother/wipeMyCalActionButton.tsx | 5 +- .../booking/AcceptBookingButton.tsx | 51 ++ .../components/booking/BookingListItem.tsx | 661 +++++++++--------- apps/web/components/booking/CancelBooking.tsx | 44 +- .../booking/RejectBookingButton.tsx | 62 ++ .../web/components/booking/SkeletonLoader.tsx | 18 +- .../CancelBooking.cancellationFee.test.tsx | 1 + .../actions/BookingActionsDropdown.tsx | 169 ++--- .../booking/actions/bookingActions.test.ts | 16 +- .../booking/actions/bookingActions.ts | 9 +- apps/web/components/booking/actions/store.ts | 15 +- .../booking/hooks/useBookingConfirmation.tsx | 74 ++ apps/web/components/booking/types.ts | 4 +- .../components/dialog/CancelBookingDialog.tsx | 122 ++++ .../dialog/RejectionReasonDialog.tsx | 62 ++ apps/web/lib/app-providers-app-dir.tsx | 7 +- apps/web/lib/app-providers.tsx | 31 +- .../booking/assignmentReasonBadgeTitleMap.ts | 2 +- apps/web/lib/hooks/useMediaQuery.ts | 20 - apps/web/lib/hooks/useNuqsParams.ts | 36 + .../components/BookingCalendarContainer.tsx | 223 ++++++ ...lendarView.tsx => BookingCalendarView.tsx} | 89 +-- .../components/BookingDetailsSheet.tsx | 559 ++++++++++++--- .../{BookingsList.tsx => BookingList.tsx} | 21 +- .../components/BookingListContainer.tsx | 266 +++++++ .../bookings/components/BookingsCalendar.tsx | 89 --- .../components/BookingsCalendarContainer.tsx | 162 ----- .../components/BookingsListContainer.tsx | 346 --------- .../bookings/components/JoinMeetingButton.tsx | 6 +- .../bookings/components}/ViewToggleButton.tsx | 23 +- .../bookings/components/WeekPicker.tsx | 39 ++ .../bookings/hooks/useBookingCalendarData.ts | 74 ++ .../bookings/hooks/useBookingCursor.ts | 20 +- .../bookings/hooks/useBookingFilters.ts | 26 + .../bookings/hooks/useBookingListColumns.tsx | 152 ++++ .../bookings/hooks/useBookingListData.ts | 115 +++ .../bookings/hooks/useBookingStatusTab.ts | 64 ++ .../hooks/useCalendarAllowedFilters.ts | 35 + .../bookings/hooks/useCurrentWeekStart.ts | 42 ++ .../bookings/hooks/usePaymentStatus.ts | 89 +++ .../bookings/hooks/useSelectedBookingUid.ts | 10 + .../web/modules/bookings/lib/validStatuses.ts | 2 + apps/web/modules/bookings/lib/weekUtils.ts | 19 + .../store/bookingDetailsSheetStore.tsx | 62 +- apps/web/modules/bookings/types.ts | 23 +- .../bookings/views/bookings-single-view.tsx | 70 +- .../modules/bookings/views/bookings-view.tsx | 168 +---- apps/web/package.json | 2 +- apps/web/playwright/booking-filters.e2e.ts | 118 ---- apps/web/playwright/booking-pages.e2e.ts | 63 +- .../playwright/dynamic-booking-pages.e2e.ts | 16 +- apps/web/playwright/filter-helpers.ts | 2 +- apps/web/playwright/i18n-routing.e2e.ts | 4 +- apps/web/playwright/lib/testUtils.ts | 33 +- apps/web/playwright/locale.e2e.ts | 16 +- apps/web/playwright/reschedule.e2e.ts | 37 +- apps/web/public/static/locales/en/common.json | 2 + .../bookings/lib/getAllUserBookings.ts | 13 +- .../repositories/BookingRepository.ts | 57 +- .../bookings/services/BookingAccessService.ts | 62 +- .../services/BookingDetailsService.ts | 48 ++ .../components/DateValues/index.tsx | 4 +- .../weeklyview/components/event/Event.tsx | 42 +- .../calendars/weeklyview/types/events.ts | 2 +- .../data-table/components/DataTable.tsx | 4 +- .../components/filters/AddFilterButton.tsx | 10 +- .../components/filters/FilterBar.tsx | 12 +- .../segment/FilterSegmentSelect.tsx | 8 +- packages/features/data-table/hooks/index.ts | 1 + .../hooks/useDisplayedFilterCount.ts | 16 + .../features/shell/navigation/Navigation.tsx | 40 -- .../shell/navigation/NavigationItem.tsx | 53 +- packages/lib/hooks/useMediaQuery.ts | 34 +- .../migration.sql | 2 + packages/prisma/schema.prisma | 1 + packages/prisma/selects/booking.ts | 36 + .../routers/viewer/bookings/_router.tsx | 14 +- .../routers/viewer/bookings/get.handler.ts | 120 +++- .../routers/viewer/bookings/get.schema.ts | 4 + .../bookings/getBookingDetails.handler.ts | 21 + .../bookings/getBookingDetails.schema.ts | 7 + packages/ui/components/dropdown/Dropdown.tsx | 2 +- .../components/form/datepicker/DatePicker.tsx | 7 +- .../form/toggleGroup/ToggleGroup.tsx | 4 +- packages/ui/components/sheet/Sheet.tsx | 64 +- packages/ui/components/table/TableActions.tsx | 6 +- packages/ui/components/table/TableNew.tsx | 4 +- scripts/seed-insights.ts | 602 +++++++++++++--- yarn.lock | 14 +- 90 files changed, 3741 insertions(+), 2079 deletions(-) create mode 100644 apps/web/components/booking/AcceptBookingButton.tsx create mode 100644 apps/web/components/booking/RejectBookingButton.tsx create mode 100644 apps/web/components/booking/hooks/useBookingConfirmation.tsx create mode 100644 apps/web/components/dialog/CancelBookingDialog.tsx create mode 100644 apps/web/components/dialog/RejectionReasonDialog.tsx delete mode 100644 apps/web/lib/hooks/useMediaQuery.ts create mode 100644 apps/web/lib/hooks/useNuqsParams.ts create mode 100644 apps/web/modules/bookings/components/BookingCalendarContainer.tsx rename apps/web/modules/bookings/components/{BookingsCalendarView.tsx => BookingCalendarView.tsx} (51%) rename apps/web/modules/bookings/components/{BookingsList.tsx => BookingList.tsx} (78%) create mode 100644 apps/web/modules/bookings/components/BookingListContainer.tsx delete mode 100644 apps/web/modules/bookings/components/BookingsCalendar.tsx delete mode 100644 apps/web/modules/bookings/components/BookingsCalendarContainer.tsx delete mode 100644 apps/web/modules/bookings/components/BookingsListContainer.tsx rename apps/web/{app/(use-page-wrapper)/(main-nav)/bookings/[status] => modules/bookings/components}/ViewToggleButton.tsx (58%) create mode 100644 apps/web/modules/bookings/components/WeekPicker.tsx create mode 100644 apps/web/modules/bookings/hooks/useBookingCalendarData.ts create mode 100644 apps/web/modules/bookings/hooks/useBookingFilters.ts create mode 100644 apps/web/modules/bookings/hooks/useBookingListColumns.tsx create mode 100644 apps/web/modules/bookings/hooks/useBookingListData.ts create mode 100644 apps/web/modules/bookings/hooks/useBookingStatusTab.ts create mode 100644 apps/web/modules/bookings/hooks/useCalendarAllowedFilters.ts create mode 100644 apps/web/modules/bookings/hooks/useCurrentWeekStart.ts create mode 100644 apps/web/modules/bookings/hooks/usePaymentStatus.ts create mode 100644 apps/web/modules/bookings/hooks/useSelectedBookingUid.ts create mode 100644 apps/web/modules/bookings/lib/weekUtils.ts create mode 100644 packages/features/bookings/services/BookingDetailsService.ts create mode 100644 packages/features/data-table/hooks/useDisplayedFilterCount.ts create mode 100644 packages/prisma/migrations/20251127102536_add_from_reschdule_index_to_booking/migration.sql create mode 100644 packages/trpc/server/routers/viewer/bookings/getBookingDetails.handler.ts create mode 100644 packages/trpc/server/routers/viewer/bookings/getBookingDetails.schema.ts diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx index 1b6fab7cd2..b00c69ce9a 100644 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx +++ b/apps/web/app/(use-page-wrapper)/(main-nav)/bookings/[status]/page.tsx @@ -1,6 +1,6 @@ import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir"; import type { PageProps } from "app/_types"; -import { _generateMetadata, getTranslate } from "app/_utils"; +import { _generateMetadata } from "app/_utils"; import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; import { z } from "zod"; @@ -16,8 +16,6 @@ import { buildLegacyRequest } from "@lib/buildLegacyCtx"; import { validStatuses } from "~/bookings/lib/validStatuses"; import BookingsList from "~/bookings/views/bookings-view"; -import { ViewToggleButton } from "./ViewToggleButton"; - const querySchema = z.object({ status: z.enum(validStatuses), }); @@ -36,7 +34,6 @@ const Page = async ({ params }: PageProps) => { if (!parsed.success) { redirect("/bookings/upcoming"); } - const t = await getTranslate(); const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); let canReadOthersBookings = false; @@ -64,10 +61,7 @@ const Page = async ({ params }: PageProps) => { : false; return ( - : null}> + { - const { bookingsEmpty, bookingStatus } = props; + const { className, bookingsEmpty, bookingStatus } = props; const [openDialog, setOpenDialog] = useState(false); const { isSuccess, isPending, data } = trpc.viewer.apps.integrations.useQuery({ variant: "other", @@ -28,7 +29,7 @@ const WipeMyCalActionButton = (props: IWipeMyCalActionButtonProps) => { return ( <> {data && isSuccess && !isPending && credentialId && ( -
+
+ ); +} diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 9ed190f43a..394f9d96b9 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -8,7 +8,6 @@ import { getSuccessPageLocationMessage, guessEventLocationType } from "@calcom/a import dayjs from "@calcom/dayjs"; // TODO: Use browser locale, implement Intl in Dayjs maybe? import "@calcom/dayjs/locales"; -import { Dialog } from "@calcom/features/components/controlled-dialog"; import { formatTime } from "@calcom/lib/dayjs"; import { useCopy } from "@calcom/lib/hooks/useCopy"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -23,7 +22,6 @@ import type { Ensure } from "@calcom/types/utils"; import classNames from "@calcom/ui/classNames"; import { Badge } from "@calcom/ui/components/badge"; import { Button } from "@calcom/ui/components/button"; -import { DialogContent, DialogFooter, DialogClose } from "@calcom/ui/components/dialog"; import { Dropdown, DropdownItem, @@ -35,30 +33,29 @@ import { DropdownMenuTrigger, DropdownMenuPortal, } from "@calcom/ui/components/dropdown"; -import { TextAreaField } from "@calcom/ui/components/form"; import { Icon } from "@calcom/ui/components/icon"; import { MeetingTimeInTimezones } from "@calcom/ui/components/popover"; -import { TableActions } from "@calcom/ui/components/table"; -import type { ActionType } from "@calcom/ui/components/table"; import { showToast } from "@calcom/ui/components/toast"; import { Tooltip } from "@calcom/ui/components/tooltip"; import assignmentReasonBadgeTitleMap from "@lib/booking/assignmentReasonBadgeTitleMap"; import { buildBookingLink } from "../../modules/bookings/lib/buildBookingLink"; +import type { BookingAttendee } from "../../modules/bookings/types"; +import { AcceptBookingButton } from "./AcceptBookingButton"; +import { RejectBookingButton } from "./RejectBookingButton"; import { BookingActionsDropdown } from "./actions/BookingActionsDropdown"; import { useBookingActionsStoreContext, BookingActionsStoreProvider, } from "./actions/BookingActionsStoreProvider"; import { - getPendingActions, - getCancelEventAction, shouldShowPendingActions, shouldShowRecurringCancelAction, shouldShowIndividualReportButton, type BookingActionContext, getReportAction, + isActionDisabled, } from "./actions/bookingActions"; import type { BookingItemProps } from "./types"; @@ -146,36 +143,11 @@ function BookingListItem(booking: BookingItemProps) { t, i18n: { language }, } = useLocale(); - const utils = trpc.useUtils(); - const [rejectionReason, setRejectionReason] = useState(""); - const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false); - const attendeeList = booking.attendees.map((attendee) => { - return { - name: attendee.name, - email: attendee.email, - id: attendee.id, - noShow: attendee.noShow || false, - phoneNumber: attendee.phoneNumber, - }; - }); - - const mutation = trpc.viewer.bookings.confirm.useMutation({ - onSuccess: (data) => { - if (data?.status === BookingStatus.REJECTED) { - setRejectionDialogIsOpen(false); - showToast(t("booking_rejection_success"), "success"); - } else { - showToast(t("booking_confirmation_success"), "success"); - } - utils.viewer.bookings.invalidate(); - utils.viewer.me.bookingUnconfirmedCount.invalidate(); - }, - onError: () => { - showToast(t("booking_confirmation_failed"), "error"); - utils.viewer.bookings.invalidate(); - }, - }); + const attendeeList = booking.attendees.map((attendee) => ({ + ...attendee, + noShow: attendee.noShow || false, + })); const isUpcoming = new Date(booking.endTime) >= new Date(); const isOngoing = isUpcoming && new Date() >= new Date(booking.startTime); @@ -217,22 +189,6 @@ function BookingListItem(booking: BookingItemProps) { const isDisabledRescheduling = booking.eventType.disableRescheduling; const cardCharged = booking?.payment[0]?.success; - const bookingConfirm = async (confirm: boolean) => { - let body = { - bookingId: booking.id, - confirmed: confirm, - reason: rejectionReason, - }; - /** - * Only pass down the recurring event id when we need to confirm the entire series, which happens in - * the "Recurring" tab and "Unconfirmed" tab, to support confirming discretionally in the "Recurring" tab. - */ - if ((isTabRecurring || isTabUnconfirmed) && isRecurring) { - body = Object.assign({}, body, { recurringEventId: booking.recurringEventId }); - } - mutation.mutate(body); - }; - const getSeatReferenceUid = () => { return userSeat?.referenceUid; }; @@ -265,20 +221,6 @@ function BookingListItem(booking: BookingItemProps) { t, } as BookingActionContext; - const basePendingActions = getPendingActions(actionContext); - const pendingActions: ActionType[] = basePendingActions.map((action) => ({ - ...action, - onClick: - action.id === "reject" - ? () => setRejectionDialogIsOpen(true) - : action.id === "confirm" - ? () => bookingConfirm(true) - : undefined, - disabled: action.disabled || mutation.isPending, - })) as ActionType[]; - - const cancelEventAction = getCancelEventAction(actionContext); - const RequestSentMessage = () => { return ( @@ -313,6 +255,7 @@ function BookingListItem(booking: BookingItemProps) { const showPendingPayment = paymentAppData.enabled && booking.payment.length && !booking.paid; const setIsOpenReportDialog = useBookingActionsStoreContext((state) => state.setIsOpenReportDialog); + const setIsCancelDialogOpen = useBookingActionsStoreContext((state) => state.setIsCancelDialogOpen); const reportAction = getReportAction(actionContext); const reportActionWithHandler = { @@ -321,232 +264,236 @@ function BookingListItem(booking: BookingItemProps) { }; return ( - <> - - -
- - {t("rejection_reason")} - (Optional) - - } - value={rejectionReason} - onChange={(e) => setRejectionReason(e.target.value)} - /> -
- - - - - -
-
-
-
-
-
- {eventTypeColor && ( -
- )} - -
-
{startTime}
-
- {formatTime(booking.startTime, userTimeFormat, userTimeZone)} -{" "} - {formatTime(booking.endTime, userTimeFormat, userTimeZone)} - -
- {!isPending && ( - - )} -
-
-
-
-
- - {/* Time and Badges for mobile */} -
-
-
{startTime}
-
- {formatTime(booking.startTime, userTimeFormat, userTimeZone)} -{" "} - {formatTime(booking.endTime, userTimeFormat, userTimeZone)} - -
-
- - {isPending && ( - - {t("unconfirmed")} - - )} - {booking.eventType?.team && ( - - {booking.eventType.team.name} - - )} - {showPendingPayment && ( - - {t("pending_payment")} - - )} - {isRescheduled && ( - - - {t("rescheduled")} - - - )} - {recurringDates !== undefined && ( -
- -
- )} -
- +
+
+
+
+ {eventTypeColor &&
} +
-
- {title} - - - {showPendingPayment && ( - - {t("pending_payment")} - - )} -
- {booking.description && ( -
- "{booking.description}" -
- )} - {booking.attendees.length !== 0 && ( - {startTime}
+
+ {formatTime(booking.startTime, userTimeFormat, userTimeZone)} -{" "} + {formatTime(booking.endTime, userTimeFormat, userTimeZone)} + - )} - {isCancelled && booking.rescheduled && ( -
- +
+ {!isPending && ( + )}
-
- {shouldShowPendingActions(actionContext) && } - - {shouldShowRecurringCancelAction(actionContext) && } - {shouldShowIndividualReportButton(actionContext) && ( -
-
- )} - {isRejected &&
{t("rejected")}
} - {isCancelled && booking.rescheduled && ( -
- -
- )} -
- +
+ + {/* Time and Badges for mobile */} +
+
+
{startTime}
+
+ {formatTime(booking.startTime, userTimeFormat, userTimeZone)} -{" "} + {formatTime(booking.endTime, userTimeFormat, userTimeZone)} + +
+
+ + {isPending && ( + + {t("unconfirmed")} + + )} + {booking.eventType?.team && ( + + {booking.eventType.team.name} + + )} + {showPendingPayment && ( + + {t("pending_payment")} + + )} + {isRescheduled && ( + + + {t("rescheduled")} + + + )} + {recurringDates !== undefined && ( +
+ +
+ )} +
+ +
+
+ {title} + + + {showPendingPayment && ( + + {t("pending_payment")} + + )} +
+ {booking.description && ( +
+ "{booking.description}" +
+ )} + {booking.attendees.length !== 0 && ( + + )} + {isCancelled && booking.rescheduled && ( +
+ +
+ )} +
+
+
+
+ {shouldShowPendingActions(actionContext) && ( +
+ + +
+ )} + {shouldShowRecurringCancelAction(actionContext) && ( + + )} + {isCancelled && booking.rescheduled && ( +
+ +
+ )} + {shouldShowIndividualReportButton(actionContext) && ( +
+
+ )} + +
- + +
); } const BookingItemBadges = ({ booking, isPending, + isRejected, recurringDates, userTimeFormat, userTimeZone, @@ -554,6 +501,7 @@ const BookingItemBadges = ({ }: { booking: BookingItemProps; isPending: boolean; + isRejected: boolean; recurringDates: Date[] | undefined; userTimeFormat: number | null | undefined; userTimeZone: string | undefined; @@ -575,6 +523,11 @@ const BookingItemBadges = ({ )} + {isRejected && !isRescheduled && booking.assignmentReason.length === 0 && ( + + {t("rejected")} + + )} {booking.eventType?.team && ( {booking.eventType.team.name} @@ -727,21 +680,13 @@ const FirstAttendee = ({ ); }; -type AttendeeProps = { - name?: string; - email: string; - phoneNumber: string | null; - id: number; - noShow: boolean; -}; - type NoShowProps = { bookingUid: string; isBookingInPast: boolean; }; -const Attendee = (attendeeProps: AttendeeProps & NoShowProps) => { - const { email, name, bookingUid, isBookingInPast, noShow, phoneNumber } = attendeeProps; +const Attendee = (attendeeProps: BookingAttendee & NoShowProps) => { + const { email, name, bookingUid, isBookingInPast, noShow, phoneNumber, user } = attendeeProps; const { t } = useLocale(); const utils = trpc.useUtils(); @@ -758,6 +703,8 @@ const Attendee = (attendeeProps: AttendeeProps & NoShowProps) => { }, }); + const displayName = user?.name || name || user?.email || email; + return ( @@ -767,10 +714,10 @@ const Attendee = (attendeeProps: AttendeeProps & NoShowProps) => { className="radix-state-open:text-blue-500 transition hover:text-blue-500"> {noShow ? ( <> - {name || email} + {displayName} ) : ( - <>{name || email} + <>{displayName} )} @@ -825,20 +772,12 @@ const Attendee = (attendeeProps: AttendeeProps & NoShowProps) => { }; type GroupedAttendeeProps = { - attendees: AttendeeProps[]; + attendees: BookingAttendee[]; bookingUid: string; }; const GroupedAttendees = (groupedAttendeeProps: GroupedAttendeeProps) => { - const { bookingUid } = groupedAttendeeProps; - const attendees = groupedAttendeeProps.attendees.map((attendee) => { - return { - id: attendee.id, - email: attendee.email, - name: attendee.name, - noShow: attendee.noShow || false, - }; - }); + const { bookingUid, attendees } = groupedAttendeeProps; const { t } = useLocale(); const utils = trpc.useUtils(); const noShowMutation = trpc.viewer.loggedInViewerRouter.markNoShow.useMutation({ @@ -850,11 +789,14 @@ const GroupedAttendees = (groupedAttendeeProps: GroupedAttendeeProps) => { showToast(err.message, "error"); }, }); - const { control, handleSubmit } = useForm<{ - attendees: AttendeeProps[]; - }>({ + + type FormValues = { + attendees: Array<{ id: number; email: string; noShow: boolean }>; + }; + + const { control, handleSubmit } = useForm({ defaultValues: { - attendees, + attendees: attendees.map((a) => ({ id: a.id, email: a.email, noShow: a.noShow || false })), }, mode: "onBlur", }); @@ -864,8 +806,11 @@ const GroupedAttendees = (groupedAttendeeProps: GroupedAttendeeProps) => { name: "attendees", }); - const onSubmit = (data: { attendees: AttendeeProps[] }) => { - const filteredData = data.attendees.slice(1); + const onSubmit = (data: FormValues) => { + const filteredData = data.attendees.slice(1).map((attendee) => ({ + email: attendee.email, + noShow: attendee.noShow, + })); noShowMutation.mutate({ bookingUid, attendees: filteredData }); setOpenDropdown(false); }; @@ -882,30 +827,46 @@ const GroupedAttendees = (groupedAttendeeProps: GroupedAttendeeProps) => { {t("plus_more", { count: attendees.length - 1 })} - + {t("mark_as_no_show_title")}
- {fields.slice(1).map((field, index) => ( - ( - { - e.preventDefault(); - onChange(!value); - }}> - {field.email} - - )} - /> - ))} + {fields.slice(1).map((field, index) => { + const attendee = attendees[index + 1]; + const displayName = + attendee.user?.name || attendee.name || attendee.user?.email || attendee.email; + const hasName = attendee.name || attendee.user?.name; + + return ( + ( + { + e.preventDefault(); + onChange(!value); + }}> +
+ {hasName ? ( + <> +
{displayName}
+
{field.email}
+ + ) : ( +
{field.email}
+ )} +
+
+ )} + /> + ); + })}
- + {t("guests")} - {guests.slice(1).map((guest) => ( - - { - e.preventDefault(); - setSelectedEmail(guest.email); - }}> - {guest.email} - - - ))} + {guests.slice(1).map((guest) => { + const displayName = guest.user?.name || guest.name || guest.user?.email || guest.email; + const hasName = guest.name || guest.user?.name; + + return ( + + { + e.preventDefault(); + setSelectedEmail(guest.email); + }}> +
+ {hasName ? ( + <> +
{displayName}
+
{guest.email}
+ + ) : ( +
{guest.email}
+ )} +
+
+
+ ); + })} -
+