* refactor: apply biome formatting to apps/web (batch 1) Formats apps/web non-modules directories: - apps/web/app - apps/web/lib - apps/web/pages - apps/web/styles - apps/web/server - apps/web/test - Root-level .ts and .mjs files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web/modules (batch 2) Formats smaller apps/web/modules directories: - onboarding, data-table, users, apps, auth - integration-attribute-sync, shell, availability - feature-flags, team, webhooks, getting-started - feature-opt-in, troubleshooter, schedules, notifications - signup-view.tsx Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web/modules (batch 3) Formats medium apps/web/modules directories: - bookings - event-types - insights - embed Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web/modules/ee (batch 4) Formats apps/web/modules/ee directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web (batch 5) Formats remaining apps/web directories: - apps/web/modules/settings - apps/web/modules/booking-audit - apps/web/playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to apps/web/components (batch 6) Formats remaining apps/web/components files. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to new apps/web files from main Formats newly added/modified files from main merge: - WorkflowStepContainer.tsx (resolved merge conflicts) - Newly moved files (blocklist, form-builder, schedules, etc.) - Other files modified in main Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: apply biome formatting to new apps/web files from main Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
311 lines
10 KiB
TypeScript
311 lines
10 KiB
TypeScript
import { isWithinMinimumRescheduleNotice } from "@calcom/features/bookings/lib/reschedule/isWithinMinimumRescheduleNotice";
|
|
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
|
|
import type { ActionType } from "@calcom/ui/components/table";
|
|
|
|
import type { BookingItemProps } from "../types";
|
|
|
|
export interface BookingActionContext {
|
|
booking: BookingItemProps;
|
|
isUpcoming: boolean;
|
|
isOngoing: boolean;
|
|
isBookingInPast: boolean;
|
|
isCancelled: boolean;
|
|
isConfirmed: boolean;
|
|
isRejected: boolean;
|
|
isPending: boolean;
|
|
isRescheduled: boolean;
|
|
isRecurring: boolean;
|
|
isTabRecurring: boolean;
|
|
isTabUnconfirmed: boolean;
|
|
isBookingFromRoutingForm: boolean;
|
|
isDisabledCancelling: boolean;
|
|
isDisabledRescheduling: boolean;
|
|
isCalVideoLocation: boolean;
|
|
showPendingPayment: boolean;
|
|
isAttendee: boolean;
|
|
cardCharged: boolean;
|
|
attendeeList: Array<{
|
|
name: string;
|
|
email: string;
|
|
id: number;
|
|
noShow: boolean;
|
|
phoneNumber: string | null;
|
|
}>;
|
|
getSeatReferenceUid: () => string | undefined;
|
|
t: (key: string) => string;
|
|
}
|
|
|
|
export function getPendingActions(context: BookingActionContext): ActionType[] {
|
|
const { booking, isPending, isTabRecurring, isTabUnconfirmed, isRecurring, showPendingPayment, t } =
|
|
context;
|
|
|
|
const actions: ActionType[] = [];
|
|
|
|
// For bookings with payment, only confirm if the booking is paid for
|
|
// Original logic: (isPending && !paymentAppData.enabled) || (paymentAppData.enabled && !!paymentAppData.price && booking.paid)
|
|
if ((isPending && !showPendingPayment) || (showPendingPayment && booking.paid)) {
|
|
actions.push({
|
|
id: "confirm",
|
|
bookingUid: booking.uid,
|
|
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
|
|
icon: "check" as const,
|
|
disabled: false, // This would be controlled by mutation state in the component
|
|
});
|
|
}
|
|
|
|
actions.push({
|
|
id: "reject",
|
|
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("reject_all") : t("reject"),
|
|
icon: "ban",
|
|
disabled: false, // This would be controlled by mutation state in the component
|
|
});
|
|
|
|
return actions;
|
|
}
|
|
|
|
export function getCancelEventAction(context: BookingActionContext): ActionType {
|
|
const { booking, isTabRecurring, isRecurring, t } = context;
|
|
|
|
return {
|
|
id: "cancel",
|
|
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel_event"),
|
|
icon: "circle-x",
|
|
color: "destructive",
|
|
disabled: isActionDisabled("cancel", context),
|
|
bookingUid: booking.uid,
|
|
};
|
|
}
|
|
|
|
export function getVideoOptionsActions(context: BookingActionContext): ActionType[] {
|
|
const { booking, isBookingInPast, isConfirmed, isCalVideoLocation, t } = context;
|
|
|
|
return [
|
|
{
|
|
id: "view_recordings",
|
|
label: t("view_recordings"),
|
|
icon: "video",
|
|
disabled: !(isBookingInPast && isConfirmed && isCalVideoLocation && booking.isRecorded),
|
|
},
|
|
{
|
|
id: "meeting_session_details",
|
|
label: t("view_session_details"),
|
|
icon: "info",
|
|
disabled: !(isBookingInPast && isConfirmed && isCalVideoLocation),
|
|
},
|
|
];
|
|
}
|
|
|
|
export function getEditEventActions(context: BookingActionContext): ActionType[] {
|
|
const {
|
|
booking,
|
|
isBookingInPast,
|
|
isDisabledRescheduling,
|
|
isBookingFromRoutingForm,
|
|
getSeatReferenceUid,
|
|
isAttendee,
|
|
t,
|
|
} = context;
|
|
const seatReferenceUid = getSeatReferenceUid();
|
|
|
|
const isReassignableRoundRobin =
|
|
booking.eventType.schedulingType === SchedulingType.ROUND_ROBIN &&
|
|
(!booking.eventType.hostGroups || booking.eventType.hostGroups.length <= 1);
|
|
const isManagedChildEvent = booking.eventType.parentId != null;
|
|
const isReassignable = isReassignableRoundRobin || isManagedChildEvent;
|
|
|
|
const actions: (ActionType | null)[] = [
|
|
{
|
|
id: "reschedule",
|
|
icon: "clock",
|
|
label: t("reschedule_booking"),
|
|
href: `/reschedule/${booking.uid}${
|
|
booking.seatsReferences.length && isAttendee && seatReferenceUid
|
|
? `?seatReferenceUid=${seatReferenceUid}`
|
|
: ""
|
|
}`,
|
|
disabled: isActionDisabled("reschedule", {
|
|
...context,
|
|
booking,
|
|
isBookingInPast,
|
|
isDisabledRescheduling,
|
|
}),
|
|
},
|
|
{
|
|
id: "reschedule_request",
|
|
icon: "send",
|
|
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
|
label: t("send_reschedule_request"),
|
|
disabled:
|
|
isActionDisabled("reschedule_request", {
|
|
...context,
|
|
booking,
|
|
isBookingInPast,
|
|
isDisabledRescheduling,
|
|
}) || booking.seatsReferences.length > 0,
|
|
},
|
|
isBookingFromRoutingForm
|
|
? {
|
|
id: "reroute",
|
|
label: t("reroute"),
|
|
icon: "waypoints",
|
|
disabled: isActionDisabled("reroute", context),
|
|
}
|
|
: null,
|
|
{
|
|
id: "change_location",
|
|
label: t("edit_location"),
|
|
icon: "map-pin",
|
|
disabled: isActionDisabled("change_location", context),
|
|
},
|
|
booking.eventType?.disableGuests
|
|
? null
|
|
: {
|
|
id: "add_members",
|
|
label: t("additional_guests"),
|
|
icon: "user-plus",
|
|
disabled: isActionDisabled("add_members", context),
|
|
},
|
|
isReassignable
|
|
? {
|
|
id: "reassign",
|
|
label: t("reassign"),
|
|
icon: "users",
|
|
disabled: isActionDisabled("reassign", context),
|
|
}
|
|
: null,
|
|
];
|
|
|
|
return actions.filter(Boolean) as ActionType[];
|
|
}
|
|
|
|
export function getReportAction(context: BookingActionContext): ActionType {
|
|
const { booking, t } = context;
|
|
|
|
return {
|
|
id: "report",
|
|
label: t("report_booking"),
|
|
icon: "flag",
|
|
color: "destructive",
|
|
disabled: !!booking.report,
|
|
};
|
|
}
|
|
|
|
export function getAfterEventActions(context: BookingActionContext): ActionType[] {
|
|
const { booking, cardCharged, attendeeList, t } = context;
|
|
|
|
const actions: (ActionType | null)[] = [
|
|
...getVideoOptionsActions(context),
|
|
booking.status === BookingStatus.ACCEPTED && booking.paid && booking.payment[0]?.paymentOption === "HOLD"
|
|
? {
|
|
id: "charge_card",
|
|
label: cardCharged ? t("no_show_fee_charged") : t("collect_no_show_fee"),
|
|
icon: "credit-card",
|
|
disabled: cardCharged,
|
|
}
|
|
: null,
|
|
{
|
|
id: "no_show",
|
|
label:
|
|
attendeeList.length === 1 && attendeeList[0].noShow ? t("unmark_as_no_show") : t("mark_as_no_show"),
|
|
icon: attendeeList.length === 1 && attendeeList[0].noShow ? "eye" : "eye-off",
|
|
disabled: false, // This would be controlled by booking state in the component
|
|
},
|
|
];
|
|
|
|
return actions.filter(Boolean) as ActionType[];
|
|
}
|
|
|
|
export function shouldShowPendingActions(context: BookingActionContext): boolean {
|
|
const { isPending, isUpcoming, isCancelled } = context;
|
|
return isPending && isUpcoming && !isCancelled;
|
|
}
|
|
|
|
export function shouldShowEditActions(context: BookingActionContext): boolean {
|
|
const { isPending, isTabRecurring, isRecurring, isCancelled } = context;
|
|
return !isPending && !(isTabRecurring && isRecurring) && !isCancelled;
|
|
}
|
|
|
|
export function shouldShowRecurringCancelAction(context: BookingActionContext): boolean {
|
|
const { isTabRecurring, isRecurring } = context;
|
|
return isTabRecurring && isRecurring;
|
|
}
|
|
|
|
export function shouldShowIndividualReportButton(context: BookingActionContext): boolean {
|
|
const { booking, isPending, isUpcoming, isCancelled, isRejected } = context;
|
|
const hasDropdown = shouldShowEditActions(context);
|
|
return !booking.report && !hasDropdown && (isCancelled || isRejected || (isPending && isUpcoming));
|
|
}
|
|
|
|
export function isActionDisabled(actionId: string, context: BookingActionContext): boolean {
|
|
const {
|
|
booking,
|
|
isBookingInPast,
|
|
isDisabledRescheduling,
|
|
isDisabledCancelling,
|
|
isAttendee,
|
|
isCancelled,
|
|
isRejected,
|
|
} = context;
|
|
|
|
switch (actionId) {
|
|
case "reschedule":
|
|
case "reschedule_request":
|
|
// Only apply minimum reschedule notice restriction if user is NOT the organizer
|
|
// If user is an attendee (or not authenticated), apply the restriction
|
|
const isUserOrganizer =
|
|
!isAttendee &&
|
|
booking.loggedInUser?.userId &&
|
|
booking.user?.id &&
|
|
booking.loggedInUser.userId === booking.user.id;
|
|
const isWithinMinimumNotice =
|
|
!isUserOrganizer &&
|
|
isWithinMinimumRescheduleNotice(
|
|
new Date(booking.startTime),
|
|
booking.eventType.minimumRescheduleNotice ?? null
|
|
);
|
|
return (
|
|
isCancelled ||
|
|
isRejected ||
|
|
(isBookingInPast && !booking.eventType.allowReschedulingPastBookings) ||
|
|
isDisabledRescheduling ||
|
|
isWithinMinimumNotice
|
|
);
|
|
case "cancel":
|
|
return isDisabledCancelling || isBookingInPast || isCancelled || isRejected;
|
|
case "view_recordings":
|
|
return !(isBookingInPast && booking.status === BookingStatus.ACCEPTED && context.isCalVideoLocation);
|
|
case "meeting_session_details":
|
|
return !(isBookingInPast && booking.status === BookingStatus.ACCEPTED && context.isCalVideoLocation);
|
|
case "charge_card":
|
|
return context.cardCharged;
|
|
case "reroute":
|
|
case "reassign":
|
|
case "change_location":
|
|
case "add_members":
|
|
return isBookingInPast || isCancelled || isRejected;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function getActionLabel(actionId: string, context: BookingActionContext): string {
|
|
const { isTabRecurring, isRecurring, attendeeList, cardCharged, t } = context;
|
|
|
|
switch (actionId) {
|
|
case "reject":
|
|
return (isTabRecurring || context.isTabUnconfirmed) && isRecurring ? t("reject_all") : t("reject");
|
|
case "confirm":
|
|
return (isTabRecurring || context.isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm");
|
|
case "cancel":
|
|
return isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel_event");
|
|
case "no_show":
|
|
return attendeeList.length === 1 && attendeeList[0].noShow
|
|
? t("unmark_as_no_show")
|
|
: t("mark_as_no_show");
|
|
case "charge_card":
|
|
return cardCharged ? t("no_show_fee_charged") : t("collect_no_show_fee");
|
|
default:
|
|
return t(actionId);
|
|
}
|
|
}
|