Files
calendar/apps/web/components/booking/actions/bookingActions.ts
T
Eunjae LeeGitHubClaude Opus 4.5Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
81e4241f85 refactor: apply biome formatting to apps/web (#27692)
* 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>
2026-02-10 23:25:24 +05:30

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);
}
}