Skip to content

BookingManagementView

Displays booking details with actions for customers to cancel or reschedule their booking. Shows confirmation prompts and handles error states gracefully.

Terminal window
npx thebookingkit add booking-management-view
import { BookingManagementView, type BookingDetail } from "@/components/booking-management-view";
import { useRouter } from "next/navigation";
export function CustomerBookingPage({ bookingId }: { bookingId: string }) {
const router = useRouter();
const booking: BookingDetail = {
bookingId: "booking-123",
eventTitle: "Haircut",
providerName: "John Barber",
startsAt: new Date().toISOString(),
endsAt: new Date(Date.now() + 3600000).toISOString(),
timezone: "America/New_York",
location: "123 Main St",
status: "confirmed",
customerName: "Jane Doe",
customerEmail: "jane@example.com",
questionResponses: { "Hair Type": "Curly" },
};
const handleCancel = async (id: string) => {
const response = await fetch(`/api/bookings/${id}/cancel`, {
method: "POST",
});
if (!response.ok) throw new Error("Failed to cancel");
};
return (
<BookingManagementView
booking={booking}
onCancel={handleCancel}
onReschedule={(id) => router.push(`/reschedule/${id}`)}
/>
);
}
export interface BookingDetail {
bookingId: string;
eventTitle: string;
providerName: string;
startsAt: string; // ISO datetime string
endsAt: string;
timezone: string;
location?: string;
status: BookingStatus;
customerName: string;
customerEmail: string;
/** Question key → response value */
questionResponses?: Record<string, string>;
}
export interface BookingManagementViewProps {
/** The booking to display */
booking: BookingDetail;
/** Called when customer cancels the booking. Should update status server-side. */
onCancel?: (bookingId: string) => Promise<void>;
/** Called when customer wants to reschedule. Open the reschedule flow. */
onReschedule?: (bookingId: string) => void;
/** Additional CSS class name */
className?: string;
/** Inline styles */
style?: React.CSSProperties;
}
import React, { useState } from "react";
import { cn } from "../utils/cn.js";
import { BookingStatusBadge, type BookingStatus } from "./booking-status-badge.js";
/** A single booking detail item */
export interface BookingDetail {
bookingId: string;
eventTitle: string;
providerName: string;
startsAt: string; // ISO datetime string
endsAt: string;
timezone: string;
location?: string;
status: BookingStatus;
customerName: string;
customerEmail: string;
/** Question key → response value */
questionResponses?: Record<string, string>;
}
/** Props for the BookingManagementView component */
export interface BookingManagementViewProps {
/** The booking to display */
booking: BookingDetail;
/** Called when customer cancels the booking. Should update status server-side. */
onCancel?: (bookingId: string) => Promise<void>;
/** Called when customer wants to reschedule. Open the reschedule flow. */
onReschedule?: (bookingId: string) => void;
/** Additional CSS class name */
className?: string;
/** Inline styles */
style?: React.CSSProperties;
}
type ManagementState =
| { mode: "view" }
| { mode: "cancel-confirm" }
| { mode: "cancelling" }
| { mode: "cancelled" }
| { mode: "error"; message: string };
/**
* Booking management view for customers arriving via a signed management URL.
*
* Displays booking details with Cancel and Reschedule actions.
* Cancellation prompts for confirmation before proceeding.
*
* @example
* ```tsx
* <BookingManagementView
* booking={booking}
* onCancel={async (id) => { await api.cancelBooking(id); }}
* onReschedule={(id) => router.push(`/reschedule/${id}`)}
* />
* ```
*/
export function BookingManagementView({
booking,
onCancel,
onReschedule,
className,
style,
}: BookingManagementViewProps) {
const [state, setState] = useState<ManagementState>({ mode: "view" });
const isCancellable =
booking.status === "pending" || booking.status === "confirmed";
const isReschedulable = booking.status === "confirmed";
const isTerminal =
booking.status === "cancelled" ||
booking.status === "rejected" ||
booking.status === "completed" ||
booking.status === "no_show";
const handleCancelClick = () => setState({ mode: "cancel-confirm" });
const handleCancelAbort = () => setState({ mode: "view" });
const handleCancelConfirm = async () => {
if (!onCancel) return;
setState({ mode: "cancelling" });
try {
await onCancel(booking.bookingId);
setState({ mode: "cancelled" });
} catch (err) {
setState({
mode: "error",
message:
err instanceof Error ? err.message : "Failed to cancel booking.",
});
}
};
const { dateStr, timeStr } = formatBookingTime(
booking.startsAt,
booking.endsAt,
booking.timezone,
);
if (state.mode === "cancelled") {
return (
<div
className={cn("tbk-management-view tbk-management-cancelled", className)}
style={style}
>
<h2>Booking Cancelled</h2>
<p>Your booking has been successfully cancelled.</p>
<dl className="tbk-detail-list">
<dt>Event</dt>
<dd>{booking.eventTitle}</dd>
<dt>Date</dt>
<dd>{dateStr}</dd>
<dt>Time</dt>
<dd>{timeStr} ({booking.timezone})</dd>
</dl>
</div>
);
}
return (
<div className={cn("tbk-management-view", className)} style={style}>
<div className="tbk-management-header">
<h2>Manage Your Booking</h2>
<BookingStatusBadge status={booking.status} />
</div>
<dl className="tbk-detail-list">
<dt>Booking ID</dt>
<dd className="tbk-booking-id">{booking.bookingId}</dd>
<dt>Service</dt>
<dd>{booking.eventTitle}</dd>
<dt>Provider</dt>
<dd>{booking.providerName}</dd>
<dt>Date</dt>
<dd>{dateStr}</dd>
<dt>Time</dt>
<dd>
{timeStr} ({booking.timezone})
</dd>
{booking.location && (
<>
<dt>Location</dt>
<dd>{booking.location}</dd>
</>
)}
<dt>Name</dt>
<dd>{booking.customerName}</dd>
<dt>Email</dt>
<dd>{booking.customerEmail}</dd>
</dl>
{booking.questionResponses &&
Object.keys(booking.questionResponses).length > 0 && (
<div className="tbk-responses-section">
<h3>Your Responses</h3>
<dl className="tbk-detail-list">
{Object.entries(booking.questionResponses).map(([key, value]) => (
<React.Fragment key={key}>
<dt>{key}</dt>
<dd>{value}</dd>
</React.Fragment>
))}
</dl>
</div>
)}
{state.mode === "error" && (
<div className="tbk-alert tbk-alert-error" role="alert">
<p>{state.message}</p>
<button
type="button"
className="tbk-button-secondary"
onClick={() => setState({ mode: "view" })}
>
Dismiss
</button>
</div>
)}
{state.mode === "cancel-confirm" && (
<div className="tbk-alert tbk-alert-warning" role="alertdialog">
<p>
Are you sure you want to cancel this booking? This action cannot be undone.
</p>
<div className="tbk-alert-actions">
<button
type="button"
className="tbk-button-danger"
onClick={handleCancelConfirm}
>
Yes, Cancel Booking
</button>
<button
type="button"
className="tbk-button-secondary"
onClick={handleCancelAbort}
>
Keep Booking
</button>
</div>
</div>
)}
{!isTerminal && state.mode !== "cancel-confirm" && (
<div className="tbk-management-actions">
{isReschedulable && onReschedule && (
<button
type="button"
className="tbk-button-secondary"
onClick={() => onReschedule(booking.bookingId)}
>
Reschedule
</button>
)}
{isCancellable && onCancel && (
<button
type="button"
className="tbk-button-danger"
onClick={handleCancelClick}
disabled={state.mode === "cancelling"}
>
{state.mode === "cancelling" ? "Cancelling..." : "Cancel Booking"}
</button>
)}
</div>
)}
{isTerminal && (
<p className="tbk-terminal-note">
This booking is {booking.status} and can no longer be modified.
</p>
)}
</div>
);
}
function formatBookingTime(
startsAt: string,
endsAt: string,
timezone: string,
): { dateStr: string; timeStr: string } {
const start = new Date(startsAt);
const end = new Date(endsAt);
const dateStr = start.toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
timeZone: timezone,
});
const startTime = start.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
timeZone: timezone,
});
const endTime = end.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
timeZone: timezone,
});
return { dateStr, timeStr: `${startTime}${endTime}` };
}