2021-09-30 10:46:39 +00:00
|
|
|
import { BookingStatus } from "@prisma/client";
|
2022-05-18 21:05:49 +00:00
|
|
|
import { useRouter } from "next/router";
|
2022-10-24 22:37:55 +00:00
|
|
|
import { useState, useMemo } from "react";
|
2021-09-30 10:46:39 +00:00
|
|
|
|
2022-08-26 00:48:50 +00:00
|
|
|
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
2022-06-28 20:40:58 +00:00
|
|
|
import dayjs from "@calcom/dayjs";
|
2022-04-14 21:25:24 +00:00
|
|
|
import classNames from "@calcom/lib/classNames";
|
2022-10-19 09:45:44 +00:00
|
|
|
import { formatTime } from "@calcom/lib/date-fns";
|
2022-04-14 21:25:24 +00:00
|
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
2022-06-10 20:38:06 +00:00
|
|
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
2022-07-22 17:27:06 +00:00
|
|
|
import { inferQueryInput, inferQueryOutput, trpc } from "@calcom/trpc/react";
|
2022-03-16 23:36:43 +00:00
|
|
|
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
2022-07-27 02:24:00 +00:00
|
|
|
import { Icon } from "@calcom/ui/Icon";
|
2022-11-04 15:40:46 +00:00
|
|
|
import { Badge } from "@calcom/ui/components/badge";
|
|
|
|
import { Button } from "@calcom/ui/components/button";
|
2022-03-16 23:36:43 +00:00
|
|
|
import { TextArea } from "@calcom/ui/form/fields";
|
2022-10-19 09:45:44 +00:00
|
|
|
import MeetingTimeInTimezones from "@calcom/ui/v2/core/MeetingTimeInTimezones";
|
2022-10-26 19:27:28 +00:00
|
|
|
import Tooltip from "@calcom/ui/v2/core/Tooltip";
|
2022-10-21 08:54:18 +00:00
|
|
|
import showToast from "@calcom/ui/v2/core/notifications";
|
2022-03-16 23:36:43 +00:00
|
|
|
|
2022-05-04 21:05:57 +00:00
|
|
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
2022-10-24 22:37:55 +00:00
|
|
|
import { extractRecurringDates } from "@lib/parseDate";
|
2021-09-30 10:46:39 +00:00
|
|
|
|
2022-05-27 23:27:41 +00:00
|
|
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
2022-04-14 21:25:24 +00:00
|
|
|
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
2022-02-11 22:20:10 +00:00
|
|
|
import TableActions, { ActionType } from "@components/ui/TableActions";
|
2021-09-30 10:46:39 +00:00
|
|
|
|
2022-05-05 21:16:25 +00:00
|
|
|
type BookingListingStatus = inferQueryInput<"viewer.bookings">["status"];
|
|
|
|
|
2021-12-17 16:58:23 +00:00
|
|
|
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
|
2021-09-30 10:46:39 +00:00
|
|
|
|
2022-05-05 21:16:25 +00:00
|
|
|
type BookingItemProps = BookingItem & {
|
|
|
|
listingStatus: BookingListingStatus;
|
2022-10-06 19:23:22 +00:00
|
|
|
recurringBookings: inferQueryOutput<"viewer.bookings">["recurringInfo"];
|
2022-05-05 21:16:25 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
function BookingListItem(booking: BookingItemProps) {
|
2022-02-28 16:24:47 +00:00
|
|
|
// Get user so we can determine 12/24 hour format preferences
|
|
|
|
const query = useMeQuery();
|
|
|
|
const user = query.data;
|
2021-10-25 13:05:21 +00:00
|
|
|
const { t, i18n } = useLocale();
|
2021-09-30 10:46:39 +00:00
|
|
|
const utils = trpc.useContext();
|
2022-05-18 21:05:49 +00:00
|
|
|
const router = useRouter();
|
2022-02-09 18:25:58 +00:00
|
|
|
const [rejectionReason, setRejectionReason] = useState<string>("");
|
|
|
|
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
2022-08-26 21:58:08 +00:00
|
|
|
const mutation = trpc.useMutation(["viewer.bookings.confirm"], {
|
|
|
|
onSuccess: () => {
|
2022-05-27 23:27:41 +00:00
|
|
|
setRejectionDialogIsOpen(false);
|
2022-10-04 19:46:17 +00:00
|
|
|
showToast(t("booking_confirmation_success"), "success");
|
|
|
|
utils.invalidateQueries("viewer.bookings");
|
|
|
|
},
|
|
|
|
onError: () => {
|
|
|
|
showToast(t("booking_confirmation_failed"), "error");
|
2022-08-26 21:58:08 +00:00
|
|
|
utils.invalidateQueries("viewer.bookings");
|
2021-09-30 10:46:39 +00:00
|
|
|
},
|
2022-08-26 21:58:08 +00:00
|
|
|
});
|
|
|
|
|
2022-10-06 20:38:50 +00:00
|
|
|
const isUpcoming = new Date(booking.endTime) >= new Date();
|
|
|
|
const isPast = new Date(booking.endTime) < new Date();
|
|
|
|
const isCancelled = booking.status === BookingStatus.CANCELLED;
|
|
|
|
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
|
|
|
|
const isRejected = booking.status === BookingStatus.REJECTED;
|
|
|
|
const isPending = booking.status === BookingStatus.PENDING;
|
|
|
|
const isRecurring = booking.recurringEventId !== null;
|
|
|
|
const isTabRecurring = booking.listingStatus === "recurring";
|
|
|
|
const isTabUnconfirmed = booking.listingStatus === "unconfirmed";
|
|
|
|
|
2022-08-26 21:58:08 +00:00
|
|
|
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
|
2022-10-06 19:23:22 +00:00
|
|
|
* the "Recurring" tab and "Unconfirmed" tab, to support confirming discretionally in the "Recurring" tab.
|
2022-08-26 21:58:08 +00:00
|
|
|
*/
|
2022-10-06 19:23:22 +00:00
|
|
|
if ((isTabRecurring || isTabUnconfirmed) && isRecurring) {
|
2022-08-26 21:58:08 +00:00
|
|
|
body = Object.assign({}, body, { recurringEventId: booking.recurringEventId });
|
2021-09-30 10:46:39 +00:00
|
|
|
}
|
2022-08-26 21:58:08 +00:00
|
|
|
mutation.mutate(body);
|
|
|
|
};
|
|
|
|
|
2021-10-12 13:11:33 +00:00
|
|
|
const pendingActions: ActionType[] = [
|
2021-09-30 10:46:39 +00:00
|
|
|
{
|
|
|
|
id: "reject",
|
2022-10-06 19:23:22 +00:00
|
|
|
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("reject_all") : t("reject"),
|
2022-05-27 23:27:41 +00:00
|
|
|
onClick: () => {
|
2022-05-18 21:05:49 +00:00
|
|
|
setRejectionDialogIsOpen(true);
|
|
|
|
},
|
2022-08-03 16:01:29 +00:00
|
|
|
icon: Icon.FiSlash,
|
2021-09-30 10:46:39 +00:00
|
|
|
disabled: mutation.isLoading,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: "confirm",
|
2022-10-06 19:23:22 +00:00
|
|
|
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
|
2022-05-27 23:27:41 +00:00
|
|
|
onClick: () => {
|
2022-08-26 21:58:08 +00:00
|
|
|
bookingConfirm(true);
|
2022-05-18 21:05:49 +00:00
|
|
|
},
|
2022-08-03 16:01:29 +00:00
|
|
|
icon: Icon.FiCheck,
|
2021-09-30 10:46:39 +00:00
|
|
|
disabled: mutation.isLoading,
|
|
|
|
color: "primary",
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
2022-06-10 20:38:06 +00:00
|
|
|
let bookedActions: ActionType[] = [
|
2021-09-30 10:46:39 +00:00
|
|
|
{
|
|
|
|
id: "cancel",
|
2022-10-06 19:23:22 +00:00
|
|
|
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"),
|
2022-07-19 23:29:52 +00:00
|
|
|
/* When cancelling we need to let the UI and the API know if the intention is to
|
|
|
|
cancel all remaining bookings or just that booking instance. */
|
2022-10-06 19:23:22 +00:00
|
|
|
href: `/cancel/${booking.uid}${isTabRecurring && isRecurring ? "?allRemainingBookings=true" : ""}`,
|
2022-08-03 16:01:29 +00:00
|
|
|
icon: Icon.FiX,
|
2021-09-30 10:46:39 +00:00
|
|
|
},
|
|
|
|
{
|
2022-05-27 23:27:41 +00:00
|
|
|
id: "edit_booking",
|
2022-09-27 23:51:41 +00:00
|
|
|
label: t("edit"),
|
2022-04-14 21:25:24 +00:00
|
|
|
actions: [
|
|
|
|
{
|
2022-05-27 23:27:41 +00:00
|
|
|
id: "reschedule",
|
2022-08-03 16:01:29 +00:00
|
|
|
icon: Icon.FiClock,
|
2022-05-27 23:27:41 +00:00
|
|
|
label: t("reschedule_booking"),
|
2022-04-14 21:25:24 +00:00
|
|
|
href: `/reschedule/${booking.uid}`,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: "reschedule_request",
|
2022-08-03 16:01:29 +00:00
|
|
|
icon: Icon.FiSend,
|
2022-08-05 15:03:27 +00:00
|
|
|
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
2022-04-14 21:25:24 +00:00
|
|
|
label: t("send_reschedule_request"),
|
2022-05-27 23:27:41 +00:00
|
|
|
onClick: () => {
|
2022-05-18 21:05:49 +00:00
|
|
|
setIsOpenRescheduleDialog(true);
|
|
|
|
},
|
2022-04-14 21:25:24 +00:00
|
|
|
},
|
2022-05-27 23:27:41 +00:00
|
|
|
{
|
|
|
|
id: "change_location",
|
|
|
|
label: t("edit_location"),
|
|
|
|
onClick: () => {
|
|
|
|
setIsOpenLocationDialog(true);
|
|
|
|
},
|
2022-08-03 16:01:29 +00:00
|
|
|
icon: Icon.FiMapPin,
|
2022-05-27 23:27:41 +00:00
|
|
|
},
|
2022-04-14 21:25:24 +00:00
|
|
|
],
|
2021-09-30 10:46:39 +00:00
|
|
|
},
|
|
|
|
];
|
|
|
|
|
2022-10-06 19:23:22 +00:00
|
|
|
if (isTabRecurring && isRecurring) {
|
2022-06-10 20:38:06 +00:00
|
|
|
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
|
|
|
}
|
|
|
|
|
2022-04-14 21:25:24 +00:00
|
|
|
const RequestSentMessage = () => {
|
|
|
|
return (
|
2022-09-02 20:39:44 +00:00
|
|
|
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
2022-08-03 16:01:29 +00:00
|
|
|
<Icon.FiSend className="-mt-[1px] w-4 rotate-45" />
|
2022-04-14 21:25:24 +00:00
|
|
|
<p className="ml-2 ">{t("reschedule_request_sent")}</p>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
2021-09-30 10:46:39 +00:00
|
|
|
|
2022-04-14 21:25:24 +00:00
|
|
|
const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
|
|
|
|
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
|
2022-05-27 23:27:41 +00:00
|
|
|
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
|
|
|
|
const setLocationMutation = trpc.useMutation("viewer.bookings.editLocation", {
|
|
|
|
onSuccess: () => {
|
|
|
|
showToast(t("location_updated"), "success");
|
|
|
|
setIsOpenLocationDialog(false);
|
|
|
|
utils.invalidateQueries("viewer.bookings");
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2022-08-26 00:48:50 +00:00
|
|
|
const saveLocation = (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => {
|
2022-05-27 23:27:41 +00:00
|
|
|
let newLocation = newLocationType as string;
|
2022-08-26 00:48:50 +00:00
|
|
|
const eventLocationType = getEventLocationType(newLocationType);
|
|
|
|
if (eventLocationType?.organizerInputType) {
|
2022-05-27 23:27:41 +00:00
|
|
|
newLocation = details[Object.keys(details)[0]];
|
|
|
|
}
|
|
|
|
setLocationMutation.mutate({ bookingId: booking.id, newLocation });
|
|
|
|
};
|
2022-05-05 21:16:25 +00:00
|
|
|
|
2022-11-03 19:14:19 +00:00
|
|
|
const [, recurringDates] = useMemo(() => {
|
|
|
|
if (
|
|
|
|
booking.recurringBookings !== undefined &&
|
|
|
|
booking.eventType.recurringEvent?.freq !== undefined &&
|
|
|
|
booking.recurringEventId
|
|
|
|
) {
|
2022-10-24 22:37:55 +00:00
|
|
|
return extractRecurringDates(booking, user?.timeZone, i18n);
|
|
|
|
}
|
|
|
|
return [[], []];
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, [user?.timeZone, i18n.language, booking.recurringBookings]);
|
2022-05-18 21:05:49 +00:00
|
|
|
|
2022-08-26 00:48:50 +00:00
|
|
|
const location = booking.location || "";
|
2022-06-03 11:28:33 +00:00
|
|
|
|
2022-10-19 09:45:44 +00:00
|
|
|
const onClickTableData = () => {
|
2022-05-27 23:27:41 +00:00
|
|
|
router.push({
|
|
|
|
pathname: "/success",
|
|
|
|
query: {
|
|
|
|
date: booking.startTime,
|
2022-06-06 09:41:11 +00:00
|
|
|
// TODO: Booking when fetched should have id 0 already(for Dynamic Events).
|
|
|
|
type: booking.eventType.id || 0,
|
2022-05-27 23:27:41 +00:00
|
|
|
eventSlug: booking.eventType.slug,
|
2022-10-16 01:38:39 +00:00
|
|
|
username: user?.username || "",
|
2022-05-30 18:17:50 +00:00
|
|
|
name: booking.attendees[0] ? booking.attendees[0].name : undefined,
|
|
|
|
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
|
2022-06-03 11:28:33 +00:00
|
|
|
location: location,
|
2022-05-27 23:27:41 +00:00
|
|
|
eventName: booking.eventType.eventName || "",
|
|
|
|
bookingId: booking.id,
|
|
|
|
recur: booking.recurringEventId,
|
2022-06-06 16:54:47 +00:00
|
|
|
reschedule: isConfirmed,
|
2022-05-30 18:17:50 +00:00
|
|
|
listingStatus: booking.listingStatus,
|
|
|
|
status: booking.status,
|
2022-05-27 23:27:41 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
};
|
2022-10-17 17:28:57 +00:00
|
|
|
|
2021-09-30 10:46:39 +00:00
|
|
|
return (
|
2022-02-09 18:25:58 +00:00
|
|
|
<>
|
2022-04-14 21:25:24 +00:00
|
|
|
<RescheduleDialog
|
|
|
|
isOpenDialog={isOpenRescheduleDialog}
|
|
|
|
setIsOpenDialog={setIsOpenRescheduleDialog}
|
|
|
|
bookingUId={booking.uid}
|
|
|
|
/>
|
2022-05-27 23:27:41 +00:00
|
|
|
<EditLocationDialog
|
|
|
|
booking={booking}
|
|
|
|
saveLocation={saveLocation}
|
|
|
|
isOpenDialog={isOpenSetLocationDialog}
|
|
|
|
setShowLocationModal={setIsOpenLocationDialog}
|
|
|
|
/>
|
2022-04-14 21:25:24 +00:00
|
|
|
|
|
|
|
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
|
2022-02-09 18:25:58 +00:00
|
|
|
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
|
|
|
|
<DialogContent>
|
|
|
|
<DialogHeader title={t("rejection_reason_title")} />
|
|
|
|
|
|
|
|
<p className="-mt-4 text-sm text-gray-500">{t("rejection_reason_description")}</p>
|
|
|
|
<p className="mt-6 mb-2 text-sm font-bold text-black">
|
|
|
|
{t("rejection_reason")}
|
|
|
|
<span className="font-normal text-gray-500"> (Optional)</span>
|
|
|
|
</p>
|
|
|
|
<TextArea
|
|
|
|
name={t("rejection_reason")}
|
|
|
|
value={rejectionReason}
|
|
|
|
onChange={(e) => setRejectionReason(e.target.value)}
|
|
|
|
className="mb-5 sm:mb-6"
|
|
|
|
/>
|
|
|
|
|
|
|
|
<DialogFooter>
|
|
|
|
<DialogClose>
|
|
|
|
<Button color="secondary">{t("cancel")}</Button>
|
|
|
|
</DialogClose>
|
|
|
|
|
|
|
|
<Button
|
|
|
|
disabled={mutation.isLoading}
|
|
|
|
onClick={() => {
|
2022-08-26 21:58:08 +00:00
|
|
|
bookingConfirm(false);
|
2022-02-09 18:25:58 +00:00
|
|
|
}}>
|
|
|
|
{t("rejection_confirmation")}
|
|
|
|
</Button>
|
|
|
|
</DialogFooter>
|
|
|
|
</DialogContent>
|
|
|
|
</Dialog>
|
|
|
|
|
2022-09-27 23:51:41 +00:00
|
|
|
<tr className="flex flex-col hover:bg-neutral-50 sm:flex-row">
|
2022-10-19 09:45:44 +00:00
|
|
|
<td
|
|
|
|
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
|
|
|
onClick={onClickTableData}>
|
2022-05-27 23:27:41 +00:00
|
|
|
<div className="cursor-pointer py-4">
|
|
|
|
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
|
|
|
<div className="text-sm text-gray-500">
|
2022-10-19 09:45:44 +00:00
|
|
|
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
|
|
|
|
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
|
|
|
|
<MeetingTimeInTimezones
|
|
|
|
timeFormat={user?.timeFormat}
|
|
|
|
userTimezone={user?.timeZone}
|
|
|
|
startTime={booking.startTime}
|
|
|
|
endTime={booking.endTime}
|
|
|
|
attendees={booking.attendees}
|
|
|
|
/>
|
2022-05-27 23:27:41 +00:00
|
|
|
</div>
|
2022-09-27 23:51:41 +00:00
|
|
|
{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?.eventType?.price && !booking.paid && (
|
|
|
|
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
|
|
|
{t("pending_payment")}
|
|
|
|
</Badge>
|
|
|
|
)}
|
|
|
|
<div className="mt-2 text-sm text-gray-400">
|
2022-10-24 22:37:55 +00:00
|
|
|
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
2022-05-27 23:27:41 +00:00
|
|
|
</div>
|
2022-05-05 21:16:25 +00:00
|
|
|
</div>
|
2022-02-09 18:25:58 +00:00
|
|
|
</td>
|
2022-10-19 09:45:44 +00:00
|
|
|
<td className={"w-full px-4" + (isRejected ? " line-through" : "")} onClick={onClickTableData}>
|
2022-09-27 23:51:41 +00:00
|
|
|
{/* Time and Badges for mobile */}
|
|
|
|
<div className="w-full pt-4 pb-2 sm:hidden">
|
|
|
|
<div className="flex w-full items-center justify-between sm:hidden">
|
|
|
|
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
|
|
|
<div className="pr-2 text-sm text-gray-500">
|
2022-10-19 09:45:44 +00:00
|
|
|
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
|
|
|
|
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
|
|
|
|
<MeetingTimeInTimezones
|
|
|
|
timeFormat={user?.timeFormat}
|
|
|
|
userTimezone={user?.timeZone}
|
|
|
|
startTime={booking.startTime}
|
|
|
|
endTime={booking.endTime}
|
|
|
|
attendees={booking.attendees}
|
|
|
|
/>
|
2022-09-27 23:51:41 +00:00
|
|
|
</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>
|
|
|
|
)}
|
2022-09-09 15:02:31 +00:00
|
|
|
{!!booking?.eventType?.price && !booking.paid && (
|
2022-09-27 23:51:41 +00:00
|
|
|
<Badge className="ltr:mr-2 rtl:ml-2 sm:hidden" variant="orange">
|
2022-09-09 15:02:31 +00:00
|
|
|
{t("pending_payment")}
|
|
|
|
</Badge>
|
|
|
|
)}
|
2022-09-27 23:51:41 +00:00
|
|
|
<div className="text-sm text-gray-400 sm:hidden">
|
2022-10-24 22:37:55 +00:00
|
|
|
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
2022-09-27 23:51:41 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="cursor-pointer py-4">
|
2022-05-27 23:27:41 +00:00
|
|
|
<div
|
|
|
|
title={booking.title}
|
|
|
|
className={classNames(
|
2022-09-30 10:47:20 +00:00
|
|
|
"max-w-10/12 sm:max-w-56 text-sm font-medium leading-6 text-neutral-900 md:max-w-full",
|
2022-05-27 23:27:41 +00:00
|
|
|
isCancelled ? "line-through" : ""
|
|
|
|
)}>
|
|
|
|
{booking.title}
|
2022-09-12 20:41:59 +00:00
|
|
|
<span> </span>
|
|
|
|
|
|
|
|
{!!booking?.eventType?.price && !booking.paid && (
|
|
|
|
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
|
|
|
|
)}
|
2022-05-27 23:27:41 +00:00
|
|
|
</div>
|
|
|
|
{booking.description && (
|
|
|
|
<div
|
2022-10-26 19:27:28 +00:00
|
|
|
className="max-w-10/12 sm:max-w-40 md:max-w-56 xl:max-w-80 lg:max-w-64 truncate text-sm text-gray-600"
|
2022-05-27 23:27:41 +00:00
|
|
|
title={booking.description}>
|
|
|
|
"{booking.description}"
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{booking.attendees.length !== 0 && (
|
2022-09-12 20:41:59 +00:00
|
|
|
<DisplayAttendees
|
|
|
|
attendees={booking.attendees}
|
|
|
|
user={booking.user}
|
|
|
|
currentEmail={user?.email}
|
|
|
|
/>
|
2022-02-09 18:25:58 +00:00
|
|
|
)}
|
2022-05-27 23:27:41 +00:00
|
|
|
{isCancelled && booking.rescheduled && (
|
|
|
|
<div className="mt-2 inline-block text-left text-sm md:hidden">
|
|
|
|
<RequestSentMessage />
|
|
|
|
</div>
|
2022-02-09 18:25:58 +00:00
|
|
|
)}
|
2021-09-30 10:46:39 +00:00
|
|
|
</div>
|
2022-02-09 18:25:58 +00:00
|
|
|
</td>
|
2022-09-30 10:47:20 +00:00
|
|
|
<td className="py-4 pl-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4 sm:pl-0">
|
2022-02-09 18:25:58 +00:00
|
|
|
{isUpcoming && !isCancelled ? (
|
|
|
|
<>
|
2022-06-06 16:54:47 +00:00
|
|
|
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
|
|
|
|
{isConfirmed && <TableActions actions={bookedActions} />}
|
|
|
|
{isRejected && <div className="text-sm text-gray-500">{t("rejected")}</div>}
|
2022-02-09 18:25:58 +00:00
|
|
|
</>
|
|
|
|
) : null}
|
2022-09-14 11:02:26 +00:00
|
|
|
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
2022-04-14 21:25:24 +00:00
|
|
|
{isCancelled && booking.rescheduled && (
|
|
|
|
<div className="hidden h-full items-center md:flex">
|
|
|
|
<RequestSentMessage />
|
|
|
|
</div>
|
|
|
|
)}
|
2022-02-09 18:25:58 +00:00
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
</>
|
2021-09-30 10:46:39 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-09-27 23:51:41 +00:00
|
|
|
interface RecurringBookingsTooltipProps {
|
|
|
|
booking: BookingItemProps;
|
|
|
|
recurringDates: Date[];
|
|
|
|
}
|
|
|
|
|
2022-10-24 22:37:55 +00:00
|
|
|
const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookingsTooltipProps) => {
|
|
|
|
// Get user so we can determine 12/24 hour format preferences
|
|
|
|
const query = useMeQuery();
|
|
|
|
const user = query.data;
|
2022-09-27 23:51:41 +00:00
|
|
|
const { t } = useLocale();
|
2022-10-06 19:23:22 +00:00
|
|
|
const now = new Date();
|
2022-10-24 22:37:55 +00:00
|
|
|
const recurringCount = recurringDates.filter((date) => {
|
|
|
|
return date >= now;
|
|
|
|
}).length;
|
2022-09-27 23:51:41 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
(booking.recurringBookings &&
|
|
|
|
booking.eventType?.recurringEvent?.freq &&
|
2022-10-06 19:23:22 +00:00
|
|
|
(booking.listingStatus === "recurring" ||
|
|
|
|
booking.listingStatus === "unconfirmed" ||
|
|
|
|
booking.listingStatus === "cancelled") && (
|
2022-09-27 23:51:41 +00:00
|
|
|
<div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
|
|
|
|
<div className="flex">
|
|
|
|
<Tooltip
|
2022-10-24 22:37:55 +00:00
|
|
|
content={recurringDates.map((aDate, key) => (
|
2022-10-06 19:23:22 +00:00
|
|
|
<p key={key} className={classNames(recurringDates[key] < now && "line-through")}>
|
2022-10-24 22:37:55 +00:00
|
|
|
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)}
|
|
|
|
{" - "}
|
|
|
|
{dayjs(aDate).format("D MMMM YYYY")}
|
2022-10-06 19:23:22 +00:00
|
|
|
</p>
|
2022-09-27 23:51:41 +00:00
|
|
|
))}>
|
|
|
|
<div className="text-gray-600 dark:text-white">
|
|
|
|
<Icon.FiRefreshCcw
|
|
|
|
strokeWidth="3"
|
|
|
|
className="float-left mr-1 mt-1.5 inline-block h-3 w-3 text-gray-400"
|
|
|
|
/>
|
|
|
|
<p className="mt-1 pl-5 text-xs">
|
|
|
|
{booking.status === BookingStatus.ACCEPTED
|
|
|
|
? `${t("event_remaining", {
|
2022-10-24 22:37:55 +00:00
|
|
|
count: recurringCount,
|
2022-09-27 23:51:41 +00:00
|
|
|
})}`
|
|
|
|
: getEveryFreqFor({
|
|
|
|
t,
|
|
|
|
recurringEvent: booking.eventType.recurringEvent,
|
2022-10-24 22:37:55 +00:00
|
|
|
recurringCount,
|
2022-09-27 23:51:41 +00:00
|
|
|
})}
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
</Tooltip>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)) ||
|
|
|
|
null
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2022-09-12 20:41:59 +00:00
|
|
|
interface UserProps {
|
|
|
|
id: number;
|
|
|
|
name: string | null;
|
|
|
|
email: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
const FirstAttendee = ({
|
|
|
|
user,
|
|
|
|
currentEmail,
|
|
|
|
}: {
|
|
|
|
user: UserProps;
|
|
|
|
currentEmail: string | null | undefined;
|
|
|
|
}) => {
|
|
|
|
return user.email === currentEmail ? (
|
|
|
|
<div className="inline-block">You</div>
|
|
|
|
) : (
|
|
|
|
<a
|
|
|
|
key={user.email}
|
|
|
|
className=" hover:text-blue-500"
|
|
|
|
href={"mailto:" + user.email}
|
|
|
|
onClick={(e) => e.stopPropagation()}>
|
|
|
|
{user.name}
|
|
|
|
</a>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const Attendee: React.FC<{ email: string; children: React.ReactNode }> = ({ email, children }) => {
|
|
|
|
return (
|
|
|
|
<a className=" hover:text-blue-500" href={"mailto:" + email} onClick={(e) => e.stopPropagation()}>
|
|
|
|
{children}
|
|
|
|
</a>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
interface AttendeeProps {
|
|
|
|
name: string;
|
|
|
|
email: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
const DisplayAttendees = ({
|
|
|
|
attendees,
|
|
|
|
user,
|
|
|
|
currentEmail,
|
|
|
|
}: {
|
|
|
|
attendees: AttendeeProps[];
|
|
|
|
user: UserProps | null;
|
|
|
|
currentEmail: string | null | undefined;
|
|
|
|
}) => {
|
|
|
|
if (attendees.length === 1) {
|
|
|
|
return (
|
|
|
|
<div className="text-sm text-gray-900">
|
|
|
|
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
|
|
|
|
<span> and </span>
|
|
|
|
<Attendee email={attendees[0].email}>{attendees[0].name}</Attendee>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
} else if (attendees.length === 2) {
|
|
|
|
return (
|
|
|
|
<div className="text-sm text-gray-900">
|
|
|
|
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
|
|
|
|
<span>, </span>
|
|
|
|
<Attendee email={attendees[0].email}>{attendees[0].name}</Attendee>
|
|
|
|
<div className="inline-block text-sm text-gray-900"> and </div>
|
|
|
|
<Attendee email={attendees[1].email}>{attendees[1].name}</Attendee>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return (
|
|
|
|
<div className="text-sm text-gray-900">
|
|
|
|
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
|
|
|
|
<span>, </span>
|
|
|
|
<Attendee email={attendees[0].email}>{attendees[0].name}</Attendee>
|
|
|
|
<span> & </span>
|
|
|
|
<Tooltip
|
|
|
|
content={attendees.slice(1).map((attendee, key) => (
|
|
|
|
<p key={key}>{attendee.name}</p>
|
|
|
|
))}>
|
|
|
|
<div className="inline-block">{attendees.length - 1} more</div>
|
|
|
|
</Tooltip>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const Tag = ({ children, className = "" }: React.PropsWithChildren<{ className?: string }>) => {
|
|
|
|
return (
|
|
|
|
<span
|
|
|
|
className={`inline-flex items-center rounded-sm bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-800 ${className}`}>
|
|
|
|
{children}
|
|
|
|
</span>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2021-09-30 10:46:39 +00:00
|
|
|
export default BookingListItem;
|