feat: Show routing reason on booking page [CAL-4814] (#18053)

This commit is contained in:
Joe Au-Yeung
2024-12-10 14:31:48 -07:00
committed by GitHub
parent a466e8eef7
commit 44d6c1f3fb
4 changed files with 245 additions and 162 deletions
+217 -162
View File
@@ -1,3 +1,4 @@
import type { AssignmentReason } from "@prisma/client";
import Link from "next/link";
import { useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
@@ -45,6 +46,8 @@ import {
Tooltip,
} from "@calcom/ui";
import assignmentReasonBadgeTitleMap from "@lib/booking/assignmentReasonBadgeTitleMap";
import { AddGuestsDialog } from "@components/dialog/AddGuestsDialog";
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
@@ -508,72 +511,94 @@ function BookingListItem(booking: BookingItemProps) {
</DialogFooter>
</DialogContent>
</Dialog>
<tr data-testid="booking-item" className="hover:bg-muted group flex flex-col transition sm:flex-row">
<td className="hidden align-top ltr:pl-3 rtl:pr-6 sm:table-cell sm:min-w-[12rem]">
<div className="flex h-full items-center">
{eventTypeColor && <div className="h-[70%] w-0.5" style={{ backgroundColor: eventTypeColor }} />}
<Link href={bookingLink} className="ml-3">
<div className="cursor-pointer py-4">
<div className="text-emphasis text-sm leading-6">{startTime}</div>
<div className="text-subtle text-sm">
{formatTime(booking.startTime, userTimeFormat, userTimeZone)} -{" "}
{formatTime(booking.endTime, userTimeFormat, userTimeZone)}
<MeetingTimeInTimezones
timeFormat={userTimeFormat}
userTimezone={userTimeZone}
startTime={booking.startTime}
endTime={booking.endTime}
attendees={booking.attendees}
/>
</div>
{!isPending && (
<div>
{(provider?.label || locationToDisplay?.startsWith("https://")) &&
locationToDisplay.startsWith("http") && (
<a
href={locationToDisplay}
onClick={(e) => e.stopPropagation()}
target="_blank"
title={locationToDisplay}
rel="noreferrer"
className="text-sm leading-6 text-blue-600 hover:underline dark:text-blue-400">
<div className="flex items-center gap-2">
{provider?.iconUrl && (
<img
src={provider.iconUrl}
className="h-4 w-4 rounded-sm"
alt={`${provider?.label} logo`}
/>
)}
{provider?.label
? t("join_event_location", { eventLocationType: provider?.label })
: t("join_meeting")}
</div>
</a>
)}
<tr data-testid="booking-item" className="hover:bg-muted group transition ">
<div className="flex flex-col sm:flex-row">
<td className="hidden align-top ltr:pl-3 rtl:pr-6 sm:table-cell sm:min-w-[12rem]">
<div className="flex h-full items-center">
{eventTypeColor && (
<div className="h-[70%] w-0.5" style={{ backgroundColor: eventTypeColor }} />
)}
<Link href={bookingLink} className="ml-3">
<div className="cursor-pointer py-4">
<div className="text-emphasis text-sm leading-6">{startTime}</div>
<div className="text-subtle text-sm">
{formatTime(booking.startTime, userTimeFormat, userTimeZone)} -{" "}
{formatTime(booking.endTime, userTimeFormat, userTimeZone)}
<MeetingTimeInTimezones
timeFormat={userTimeFormat}
userTimezone={userTimeZone}
startTime={booking.startTime}
endTime={booking.endTime}
attendees={booking.attendees}
/>
</div>
)}
{!isPending && (
<div>
{(provider?.label || locationToDisplay?.startsWith("https://")) &&
locationToDisplay.startsWith("http") && (
<a
href={locationToDisplay}
onClick={(e) => e.stopPropagation()}
target="_blank"
title={locationToDisplay}
rel="noreferrer"
className="text-sm leading-6 text-blue-600 hover:underline dark:text-blue-400">
<div className="flex items-center gap-2">
{provider?.iconUrl && (
<img
src={provider.iconUrl}
className="h-4 w-4 rounded-sm"
alt={`${provider?.label} logo`}
/>
)}
{provider?.label
? t("join_event_location", { eventLocationType: provider?.label })
: t("join_meeting")}
</div>
</a>
)}
</div>
)}
</div>
</Link>
</div>
</td>
<td data-testid="title-and-attendees" className={`w-full px-4${isRejected ? " line-through" : ""}`}>
<Link href={bookingLink}>
{/* Time and Badges for mobile */}
<div className="w-full pb-2 pt-4 sm:hidden">
<div className="flex w-full items-center justify-between sm:hidden">
<div className="text-emphasis text-sm leading-6">{startTime}</div>
<div className="text-subtle pr-2 text-sm">
{formatTime(booking.startTime, userTimeFormat, userTimeZone)} -{" "}
{formatTime(booking.endTime, userTimeFormat, userTimeZone)}
<MeetingTimeInTimezones
timeFormat={userTimeFormat}
userTimezone={userTimeZone}
startTime={booking.startTime}
endTime={booking.endTime}
attendees={booking.attendees}
/>
</div>
</div>
{isPending && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
<Badge className="ltr:mr-2 rtl:ml-2 sm:hidden" variant="orange">
{t("unconfirmed")}
</Badge>
)}
{booking.eventType?.team && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
<Badge className="ltr:mr-2 rtl:ml-2 sm:hidden" variant="gray">
{booking.eventType.team.name}
</Badge>
)}
{booking.paid && !booking.payment[0] ? (
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
{t("error_collecting_card")}
{showPendingPayment && (
<Badge className="ltr:mr-2 rtl:ml-2 sm:hidden" variant="orange">
{t("pending_payment")}
</Badge>
) : booking.paid ? (
<Badge className="ltr:mr-2 rtl:ml-2" variant="green" data-testid="paid_badge">
{booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
</Badge>
) : null}
)}
{recurringDates !== undefined && (
<div className="text-muted mt-2 text-sm">
<div className="text-muted text-sm sm:hidden">
<RecurringBookingsTooltip
userTimeFormat={userTimeFormat}
userTimeZone={userTimeZone}
@@ -583,120 +608,83 @@ function BookingListItem(booking: BookingItemProps) {
</div>
)}
</div>
</Link>
</div>
</td>
<td data-testid="title-and-attendees" className={`w-full px-4${isRejected ? " line-through" : ""}`}>
<Link href={bookingLink}>
{/* Time and Badges for mobile */}
<div className="w-full pb-2 pt-4 sm:hidden">
<div className="flex w-full items-center justify-between sm:hidden">
<div className="text-emphasis text-sm leading-6">{startTime}</div>
<div className="text-subtle pr-2 text-sm">
{formatTime(booking.startTime, userTimeFormat, userTimeZone)} -{" "}
{formatTime(booking.endTime, userTimeFormat, userTimeZone)}
<MeetingTimeInTimezones
timeFormat={userTimeFormat}
userTimezone={userTimeZone}
startTime={booking.startTime}
endTime={booking.endTime}
attendees={booking.attendees}
/>
<div className="cursor-pointer py-4">
<div
title={title}
className={classNames(
"max-w-10/12 sm:max-w-56 text-emphasis text-sm font-medium leading-6 md:max-w-full",
isCancelled ? "line-through" : ""
)}>
{title}
<span> </span>
{showPendingPayment && (
<Badge className="hidden sm:inline-flex" variant="orange">
{t("pending_payment")}
</Badge>
)}
</div>
</div>
{isPending && (
<Badge className="ltr:mr-2 rtl:ml-2 sm:hidden" variant="orange">
{t("unconfirmed")}
</Badge>
)}
{booking.eventType?.team && (
<Badge className="ltr:mr-2 rtl:ml-2 sm:hidden" variant="gray">
{booking.eventType.team.name}
</Badge>
)}
{showPendingPayment && (
<Badge className="ltr:mr-2 rtl:ml-2 sm:hidden" variant="orange">
{t("pending_payment")}
</Badge>
)}
{recurringDates !== undefined && (
<div className="text-muted text-sm sm:hidden">
<RecurringBookingsTooltip
userTimeFormat={userTimeFormat}
userTimeZone={userTimeZone}
booking={booking}
recurringDates={recurringDates}
{booking.description && (
<div
className="max-w-10/12 sm:max-w-32 md:max-w-52 xl:max-w-80 text-default truncate text-sm"
title={booking.description}>
&quot;{booking.description}&quot;
</div>
)}
{booking.attendees.length !== 0 && (
<DisplayAttendees
attendees={attendeeList}
user={booking.user}
currentEmail={userEmail}
bookingUid={booking.uid}
isBookingInPast={isBookingInPast}
/>
</div>
)}
</div>
<div className="cursor-pointer py-4">
<div
title={title}
className={classNames(
"max-w-10/12 sm:max-w-56 text-emphasis text-sm font-medium leading-6 md:max-w-full",
isCancelled ? "line-through" : ""
)}>
{title}
<span> </span>
{showPendingPayment && (
<Badge className="hidden sm:inline-flex" variant="orange">
{t("pending_payment")}
</Badge>
)}
{isCancelled && booking.rescheduled && (
<div className="mt-2 inline-block md:hidden">
<RequestSentMessage />
</div>
)}
</div>
{booking.description && (
<div
className="max-w-10/12 sm:max-w-32 md:max-w-52 xl:max-w-80 text-default truncate text-sm"
title={booking.description}>
&quot;{booking.description}&quot;
</Link>
</td>
<td className="flex w-full flex-col flex-wrap items-end justify-end space-x-2 space-y-2 py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:flex-row sm:flex-nowrap sm:items-start sm:space-y-0 sm:pl-0">
{isUpcoming && !isCancelled ? (
<>
{isPending && <TableActions actions={pendingActions} />}
{isConfirmed && <TableActions actions={bookedActions} />}
{isRejected && <div className="text-subtle text-sm">{t("rejected")}</div>}
</>
) : null}
{isBookingInPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
{isBookingInPast && isConfirmed ? <TableActions actions={bookedActions} /> : null}
{(showViewRecordingsButton || showCheckRecordingButton) && (
<TableActions actions={showRecordingActions} />
)}
{isCancelled && booking.rescheduled && (
<div className="hidden h-full items-center md:flex">
<RequestSentMessage />
</div>
)}
{booking.status === "ACCEPTED" &&
booking.paid &&
booking.payment[0]?.paymentOption === "HOLD" && (
<div className="ml-2">
<TableActions actions={chargeCardActions} />
</div>
)}
{booking.attendees.length !== 0 && (
<DisplayAttendees
attendees={attendeeList}
user={booking.user}
currentEmail={userEmail}
bookingUid={booking.uid}
isBookingInPast={isBookingInPast}
/>
)}
{isCancelled && booking.rescheduled && (
<div className="mt-2 inline-block md:hidden">
<RequestSentMessage />
</div>
)}
</div>
</Link>
</td>
<td className="flex w-full flex-col flex-wrap items-end justify-end space-x-2 space-y-2 py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:flex-row sm:flex-nowrap sm:items-start sm:space-y-0 sm:pl-0">
{isUpcoming && !isCancelled ? (
<>
{isPending && <TableActions actions={pendingActions} />}
{isConfirmed && <TableActions actions={bookedActions} />}
{isRejected && <div className="text-subtle text-sm">{t("rejected")}</div>}
</>
) : null}
{isBookingInPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
{isBookingInPast && isConfirmed ? <TableActions actions={bookedActions} /> : null}
{(showViewRecordingsButton || showCheckRecordingButton) && (
<TableActions actions={showRecordingActions} />
)}
{isCancelled && booking.rescheduled && (
<div className="hidden h-full items-center md:flex">
<RequestSentMessage />
</div>
)}
{booking.status === "ACCEPTED" && booking.paid && booking.payment[0]?.paymentOption === "HOLD" && (
<div className="ml-2">
<TableActions actions={chargeCardActions} />
</div>
)}
</td>
</td>
</div>
<BookingItemBadges
booking={booking}
isPending={isPending}
recurringDates={recurringDates}
userTimeFormat={userTimeFormat}
userTimeZone={userTimeZone}
/>
</tr>
{isBookingReroutable(parsedBooking) && (
<RerouteDialog
isOpenDialog={rerouteDialogIsOpen}
@@ -708,6 +696,59 @@ function BookingListItem(booking: BookingItemProps) {
);
}
const BookingItemBadges = ({
booking,
isPending,
recurringDates,
userTimeFormat,
userTimeZone,
}: {
booking: BookingItemProps;
isPending: boolean;
recurringDates: Date[] | undefined;
userTimeFormat: number | null | undefined;
userTimeZone: string | undefined;
}) => {
const { t } = useLocale();
return (
<div className="hidden h-9 flex-row pb-4 pl-6 sm:flex">
{isPending && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
{t("unconfirmed")}
</Badge>
)}
{booking.eventType?.team && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
{booking.eventType.team.name}
</Badge>
)}
{booking?.assignmentReason.length > 0 && (
<AssignmentReasonTooltip assignmentReason={booking.assignmentReason[0]} />
)}
{booking.paid && !booking.payment[0] ? (
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
{t("error_collecting_card")}
</Badge>
) : booking.paid ? (
<Badge className="ltr:mr-2 rtl:ml-2" variant="green" data-testid="paid_badge">
{booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
</Badge>
) : null}
{recurringDates !== undefined && (
<div className="text-muted mt-2 text-sm">
<RecurringBookingsTooltip
userTimeFormat={userTimeFormat}
userTimeZone={userTimeZone}
booking={booking}
recurringDates={recurringDates}
/>
</div>
)}
</div>
);
};
interface RecurringBookingsTooltipProps {
booking: BookingItemProps;
recurringDates: Date[];
@@ -1205,4 +1246,18 @@ const DisplayAttendees = ({
);
};
const AssignmentReasonTooltip = ({ assignmentReason }: { assignmentReason: AssignmentReason }) => {
const { t } = useLocale();
const badgeTitle = assignmentReasonBadgeTitleMap(assignmentReason.reasonEnum);
return (
<Tooltip content={<p>{assignmentReason.reasonString}</p>}>
<div className="-mt-1">
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
{t(badgeTitle)}
</Badge>
</div>
</Tooltip>
);
};
export default BookingListItem;
@@ -0,0 +1,18 @@
import { AssignmentReasonEnum } from "@calcom/prisma/enums";
const assignmentReasonBadgeTitleMap = (assignmentReason: AssignmentReasonEnum) => {
switch (assignmentReason) {
case AssignmentReasonEnum.ROUTING_FORM_ROUTING || AssignmentReasonEnum.ROUTING_FORM_ROUTING_FALLBACK:
return "routed";
case AssignmentReasonEnum.REASSIGNED:
return "reassigned";
case AssignmentReasonEnum.REROUTED:
return "rerouted";
case AssignmentReasonEnum.SALESFORCE_ASSIGNMENT:
return "salesforce_assignment";
default:
return "routed";
}
};
export default assignmentReasonBadgeTitleMap;
@@ -2863,5 +2863,9 @@
"lock_attribute_for_assignment_description": "Locking would only allow assignments from Directory Sync",
"attribute_edited_successfully": "Attribute edited successfully",
"new_group_option": "New group option",
"routed": "Routed",
"reassigned": "Reassigned",
"rerouted": "Rerouted",
"salesforce_assigned": "Salesforce assignment",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1,3 +1,5 @@
import { Prisma as PrismaClientType } from "@prisma/client";
import { parseRecurringEvent, parseEventTypeColor } from "@calcom/lib";
import getAllUserBookings from "@calcom/lib/bookings/getAllUserBookings";
import type { PrismaClient } from "@calcom/prisma";
@@ -283,6 +285,10 @@ export async function getBookings({
},
},
},
assignmentReason: {
orderBy: { createdAt: PrismaClientType.SortOrder.desc },
take: 1,
},
};
const [