Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: noShow status conflict #17659

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 99 additions & 53 deletions apps/web/components/booking/BookingListItem.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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];
Expand All @@ -73,6 +76,13 @@ type TeamEventBooking = Omit<ParsedBooking, "eventType"> & {
eventType: TeamEvent;
};
type ReroutableBooking = Ensure<TeamEventBooking, "routedFromRoutingFormReponse">;
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
Expand Down Expand Up @@ -102,6 +112,20 @@ const isBookingReroutable = (booking: ParsedBooking): booking is ReroutableBooki

function BookingListItem(booking: BookingItemProps) {
const parsedBooking = buildParsedBooking(booking);
const [attendeeList, setAttendeeList] = useState<AttendeeList[]>([]);

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 {
Expand Down Expand Up @@ -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 <SkeletonLoader />;
}

return (
<>
Expand Down Expand Up @@ -469,6 +488,7 @@ function BookingListItem(booking: BookingItemProps) {
)}
{isNoShowDialogOpen && (
<NoShowAttendeesDialog
setAttendeeList={setAttendeeList}
bookingUid={booking.uid}
attendees={attendeeList}
setIsOpen={setIsNoShowDialogOpen}
Expand Down Expand Up @@ -653,6 +673,7 @@ function BookingListItem(booking: BookingItemProps) {
)}
{booking.attendees.length !== 0 && (
<DisplayAttendees
setAttendeeList={setAttendeeList}
attendees={attendeeList}
user={booking.user}
currentEmail={userEmail}
Expand Down Expand Up @@ -820,34 +841,32 @@ type NoShowProps = {
isBookingInPast: boolean;
};

const Attendee = (attendeeProps: AttendeeProps & NoShowProps) => {
const { email, name, bookingUid, isBookingInPast, noShow: noShowAttendee, phoneNumber } = attendeeProps;
type setAttendeeList = {
setAttendeeList: Dispatch<SetStateAction<AttendeeList[]>>;
};

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({
Praashh marked this conversation as resolved.
Show resolved Hide resolved
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) => {
showToast(err.message, "error");
},
});

function toggleNoShow({
attendee,
bookingUid,
}: {
attendee: { email: string; noShow: boolean };
bookingUid: string;
}) {
noShowMutation.mutate({ bookingUid, attendees: [attendee] });
setNoShow(!noShow);
}

return (
<Dropdown open={openDropdown} onOpenChange={setOpenDropdown}>
<DropdownMenuTrigger asChild>
Expand Down Expand Up @@ -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")}
Expand All @@ -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")}
Expand All @@ -927,29 +952,31 @@ const Attendee = (attendeeProps: AttendeeProps & NoShowProps) => {

type GroupedAttendeeProps = {
attendees: AttendeeProps[];
setAttendeeList: Dispatch<SetStateAction<AttendeeList[]>>;
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: {
Expand All @@ -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 });
Expand Down Expand Up @@ -1024,32 +1055,26 @@ const GroupedAttendees = (groupedAttendeeProps: GroupedAttendeeProps) => {
};

const NoShowAttendeesDialog = ({
setAttendeeList,
attendees,
isOpen,
setIsOpen,
bookingUid,
}: {
setAttendeeList: Dispatch<SetStateAction<AttendeeList[]>>;
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");
Expand All @@ -1062,7 +1087,7 @@ const NoShowAttendeesDialog = ({
return (
<Dialog open={isOpen} onOpenChange={() => setIsOpen(false)}>
<DialogContent title={t("mark_as_no_show_title")} description={t("no_show_description")}>
{noShowAttendees.map((attendee) => (
{attendees.map((attendee) => (
<form
key={attendee.id}
onSubmit={(e) => {
Expand Down Expand Up @@ -1156,12 +1181,14 @@ const GroupedGuests = ({ guests }: { guests: AttendeeProps[] }) => {
};

const DisplayAttendees = ({
setAttendeeList,
attendees,
user,
currentEmail,
bookingUid,
isBookingInPast,
}: {
setAttendeeList: Dispatch<SetStateAction<AttendeeList[]>>;
attendees: AttendeeProps[];
user: UserProps | null;
currentEmail?: string | null;
Expand All @@ -1175,25 +1202,44 @@ const DisplayAttendees = ({
<div className="text-emphasis text-sm">
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
{attendees.length > 1 ? <span>,&nbsp;</span> : <span>&nbsp;{t("and")}&nbsp;</span>}
<Attendee {...attendees[0]} bookingUid={bookingUid} isBookingInPast={isBookingInPast} />
<Attendee
{...attendees[0]}
bookingUid={bookingUid}
isBookingInPast={isBookingInPast}
setAttendeeList={setAttendeeList}
/>
{attendees.length > 1 && (
<>
<div className="text-emphasis inline-block text-sm">&nbsp;{t("and")}&nbsp;</div>
{attendees.length > 2 ? (
<Tooltip
content={attendees.slice(1).map((attendee) => (
<p key={attendee.email}>
<Attendee {...attendee} bookingUid={bookingUid} isBookingInPast={isBookingInPast} />
<Attendee
{...attendee}
bookingUid={bookingUid}
isBookingInPast={isBookingInPast}
setAttendeeList={setAttendeeList}
/>
</p>
))}>
{isBookingInPast ? (
<GroupedAttendees attendees={attendees} bookingUid={bookingUid} />
<GroupedAttendees
attendees={attendees}
bookingUid={bookingUid}
setAttendeeList={setAttendeeList}
/>
) : (
<GroupedGuests guests={attendees} />
)}
</Tooltip>
) : (
<Attendee {...attendees[1]} bookingUid={bookingUid} isBookingInPast={isBookingInPast} />
<Attendee
{...attendees[1]}
bookingUid={bookingUid}
isBookingInPast={isBookingInPast}
setAttendeeList={setAttendeeList}
/>
)}
</>
)}
Expand Down
8 changes: 4 additions & 4 deletions packages/features/handleMarkNoShow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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) => {
Expand Down
Loading