diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 736ed28ce86235..73e65460145285 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; -import { useState } from "react"; +import type { Dispatch, SetStateAction } from "react"; +import { useState, useEffect } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import type { getEventLocationValue } from "@calcom/app-store/locations"; @@ -52,6 +53,8 @@ import { ReassignDialog } from "@components/dialog/ReassignDialog"; import { RerouteDialog } from "@components/dialog/RerouteDialog"; import { RescheduleDialog } from "@components/dialog/RescheduleDialog"; +import SkeletonLoader from "./SkeletonLoader"; + type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"]; type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number]; @@ -73,6 +76,13 @@ type TeamEventBooking = Omit & { eventType: TeamEvent; }; type ReroutableBooking = Ensure; +type AttendeeList = { + name?: string; + email: string; + id: number; + noShow: boolean; + phoneNumber: string | null; +}; function buildParsedBooking(booking: BookingItemProps) { // The way we fetch bookings there could be eventType object even without an eventType, but id confirms its existence @@ -102,6 +112,20 @@ const isBookingReroutable = (booking: ParsedBooking): booking is ReroutableBooki function BookingListItem(booking: BookingItemProps) { const parsedBooking = buildParsedBooking(booking); + const [attendeeList, setAttendeeList] = useState([]); + + useEffect(() => { + const attendeeList: AttendeeList[] = booking.attendees.map((attendee) => { + return { + name: attendee.name || "", + email: attendee.email, + id: attendee.id, + noShow: attendee.noShow || false, + phoneNumber: attendee.phoneNumber, + }; + }); + setAttendeeList(attendeeList); + }, [booking.attendees]); const { userTimeZone, userTimeFormat, userEmail } = booking.loggedInUser; const { @@ -413,15 +437,10 @@ function BookingListItem(booking: BookingItemProps) { ]; const showPendingPayment = paymentAppData.enabled && booking.payment.length && !booking.paid; - const attendeeList = booking.attendees.map((attendee) => { - return { - name: attendee.name, - email: attendee.email, - id: attendee.id, - noShow: attendee.noShow || false, - phoneNumber: attendee.phoneNumber, - }; - }); + + if (!attendeeList || attendeeList.length === 0) { + return ; + } return ( <> @@ -469,6 +488,7 @@ function BookingListItem(booking: BookingItemProps) { )} {isNoShowDialogOpen && ( { - const { email, name, bookingUid, isBookingInPast, noShow: noShowAttendee, phoneNumber } = attendeeProps; +type setAttendeeList = { + setAttendeeList: Dispatch>; +}; + +const Attendee = (attendeeProps: AttendeeProps & NoShowProps & setAttendeeList) => { + const { email, name, bookingUid, isBookingInPast, noShow, phoneNumber, setAttendeeList } = attendeeProps; const { t } = useLocale(); - const [noShow, setNoShow] = useState(noShowAttendee); const [openDropdown, setOpenDropdown] = useState(false); const { copyToClipboard, isCopied } = useCopy(); const noShowMutation = trpc.viewer.markNoShow.useMutation({ onSuccess: async (data) => { + const newAttendee = data.attendees[0]; + setAttendeeList((oldAttendeeList: AttendeeList[]) => + oldAttendeeList.map((attendee: AttendeeList) => + attendee.email === newAttendee.email ? { ...attendee, noShow: newAttendee.noShow } : attendee + ) + ); showToast(data.message, "success"); }, onError: (err) => { @@ -837,17 +867,6 @@ const Attendee = (attendeeProps: AttendeeProps & NoShowProps) => { }, }); - function toggleNoShow({ - attendee, - bookingUid, - }: { - attendee: { email: string; noShow: boolean }; - bookingUid: string; - }) { - noShowMutation.mutate({ bookingUid, attendees: [attendee] }); - setNoShow(!noShow); - } - return ( @@ -901,7 +920,10 @@ const Attendee = (attendeeProps: AttendeeProps & NoShowProps) => { onClick={(e) => { e.preventDefault(); setOpenDropdown(false); - toggleNoShow({ attendee: { noShow: false, email }, bookingUid }); + noShowMutation.mutate({ + bookingUid, + attendees: [{ email: email, noShow: !noShow }], + }); }} StartIcon="eye"> {t("unmark_as_no_show")} @@ -912,7 +934,10 @@ const Attendee = (attendeeProps: AttendeeProps & NoShowProps) => { onClick={(e) => { e.preventDefault(); setOpenDropdown(false); - toggleNoShow({ attendee: { noShow: true, email }, bookingUid }); + noShowMutation.mutate({ + bookingUid, + attendees: [{ email: email, noShow: !noShow }], + }); }} StartIcon="eye-off"> {t("mark_as_no_show")} @@ -927,29 +952,31 @@ const Attendee = (attendeeProps: AttendeeProps & NoShowProps) => { type GroupedAttendeeProps = { attendees: AttendeeProps[]; + setAttendeeList: Dispatch>; 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, setAttendeeList } = groupedAttendeeProps; const { t } = useLocale(); const noShowMutation = trpc.viewer.markNoShow.useMutation({ onSuccess: async (data) => { + const oldAttendee = attendees[0]; + const newAttendeeList = data.attendees; + newAttendeeList.sort((a, b) => a.id - b.id); + + const updatedAttendeeList: AttendeeProps[] = newAttendeeList.map((newAttendee, index) => ({ + ...attendees[index + 1], + ...newAttendee, + })); + setAttendeeList([oldAttendee, ...updatedAttendeeList]); showToast(t(data.message), "success"); }, onError: (err) => { showToast(err.message, "error"); }, }); - const { control, handleSubmit } = useForm<{ + const { control, handleSubmit, setValue } = useForm<{ attendees: AttendeeProps[]; }>({ defaultValues: { @@ -963,6 +990,10 @@ const GroupedAttendees = (groupedAttendeeProps: GroupedAttendeeProps) => { name: "attendees", }); + useEffect(() => { + setValue("attendees", attendees); + }, [attendees]); + const onSubmit = (data: { attendees: AttendeeProps[] }) => { const filteredData = data.attendees.slice(1); noShowMutation.mutate({ bookingUid, attendees: filteredData }); @@ -1024,32 +1055,26 @@ const GroupedAttendees = (groupedAttendeeProps: GroupedAttendeeProps) => { }; const NoShowAttendeesDialog = ({ + setAttendeeList, attendees, isOpen, setIsOpen, bookingUid, }: { + setAttendeeList: Dispatch>; attendees: AttendeeProps[]; isOpen: boolean; setIsOpen: (value: boolean) => void; bookingUid: string; }) => { const { t } = useLocale(); - const [noShowAttendees, setNoShowAttendees] = useState( - attendees.map((attendee) => ({ - id: attendee.id, - email: attendee.email, - name: attendee.name, - noShow: attendee.noShow || false, - })) - ); const noShowMutation = trpc.viewer.markNoShow.useMutation({ onSuccess: async (data) => { - const newValue = data.attendees[0]; - setNoShowAttendees((old) => - old.map((attendee) => - attendee.email === newValue.email ? { ...attendee, noShow: newValue.noShow } : attendee + const newAttendee = data.attendees[0]; + setAttendeeList((oldAttendeeList: AttendeeList[]) => + oldAttendeeList.map((attendee: AttendeeList) => + attendee.email === newAttendee.email ? { ...attendee, noShow: newAttendee.noShow } : attendee ) ); showToast(t(data.message), "success"); @@ -1062,7 +1087,7 @@ const NoShowAttendeesDialog = ({ return ( setIsOpen(false)}> - {noShowAttendees.map((attendee) => ( + {attendees.map((attendee) => (
{ @@ -1156,12 +1181,14 @@ const GroupedGuests = ({ guests }: { guests: AttendeeProps[] }) => { }; const DisplayAttendees = ({ + setAttendeeList, attendees, user, currentEmail, bookingUid, isBookingInPast, }: { + setAttendeeList: Dispatch>; attendees: AttendeeProps[]; user: UserProps | null; currentEmail?: string | null; @@ -1175,7 +1202,12 @@ const DisplayAttendees = ({
{user && } {attendees.length > 1 ? :  {t("and")} } - + {attendees.length > 1 && ( <>
 {t("and")} 
@@ -1183,17 +1215,31 @@ const DisplayAttendees = ({ (

- +

))}> {isBookingInPast ? ( - + ) : ( )}
) : ( - + )} )} diff --git a/packages/features/handleMarkNoShow.ts b/packages/features/handleMarkNoShow.ts index d2269ff83ce92e..57999a35c3660a 100644 --- a/packages/features/handleMarkNoShow.ts +++ b/packages/features/handleMarkNoShow.ts @@ -12,7 +12,7 @@ import type { TNoShowInputSchema } from "@calcom/trpc/server/routers/loggedInVie import handleSendingAttendeeNoShowDataToApps from "./noShow/handleSendingAttendeeNoShowDataToApps"; -export type NoShowAttendees = { email: string; noShow: boolean }[]; +export type NoShowAttendees = { email: string; noShow: boolean; id: number }[]; const buildResultPayload = async ( bookingUid: string, @@ -55,7 +55,7 @@ class ResponsePayload { this.message = ""; } - setAttendees(attendees: { email: string; noShow: boolean }[]) { + setAttendees(attendees: { email: string; noShow: boolean; id: number }[]) { this.attendees = attendees; } @@ -106,7 +106,7 @@ const handleMarkNoShow = async ({ responsePayload.setAttendees(payload.attendees); responsePayload.setMessage(payload.message); - await handleSendingAttendeeNoShowDataToApps(bookingUid, attendees); + await handleSendingAttendeeNoShowDataToApps(bookingUid, payload.attendees); } if (noShowHost) { @@ -175,8 +175,8 @@ const updateAttendees = async ( return results .filter((x) => x.status === "fulfilled") - .map((x) => (x as PromiseFulfilledResult<{ noShow: boolean; email: string }>).value) - .map((x) => ({ email: x.email, noShow: x.noShow })); + .map((x) => (x as PromiseFulfilledResult<{ noShow: boolean; email: string; id: number }>).value) + .map((x) => ({ email: x.email, noShow: x.noShow, id: x.id })); }; const getWebhooksService = async (bookingUid: string) => {