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}
+ )} +
+
+
+ ); + })} -
+