import type { Metadata } from "next"; import { CalendarPlus, Phone } from "lucide-react"; import Link from "next/link"; import { notFound } from "next/navigation"; import { auth } from "@/auth"; import { Card } from "@/components/ui/card"; import { StatusPill } from "@/components/ui/status-pill"; import { TopNav } from "@/components/navigation/top-nav"; import { getPrimaryBusiness } from "@/lib/business/get-primary-business"; import { prisma } from "@/lib/db/prisma"; import { getAppointmentById } from "@/services/scheduling/appointment.service"; import { assertCanAccessAppointment, PermissionDeniedError, } from "@/features/booking/authorization"; import { BookAgainButton } from "@/features/booking/components/book-again-button"; import { CancelAppointmentDialog } from "@/features/booking/components/cancel-appointment-dialog"; import type { AppointmentStatus } from "@/types/domain"; export const metadata: Metadata = { title: "Appointment Details" }; export const dynamic = "force-dynamic"; const STATUS_MAP: Record = { PENDING: "pending_approval", CONFIRMED: "confirmed", CHECKED_IN: "arrived", IN_PROGRESS: "in_progress", COMPLETED: "completed", CANCELLED: "cancelled", NO_SHOW: "no_show", }; const RESCHEDULABLE_STATUSES = new Set(["PENDING", "CONFIRMED", "CHECKED_IN"]); const CANCELLABLE_STATUSES = new Set(["PENDING", "CONFIRMED", "CHECKED_IN"]); const REBOOKABLE_STATUSES = new Set(["COMPLETED", "CANCELLED", "NO_SHOW"]); export default async function AppointmentDetailsPage({ params, searchParams, }: { params: Promise<{ id: string }>; searchParams: Promise<{ token?: string }>; }) { const { id } = await params; const { token } = await searchParams; const appointment = await getAppointmentById(prisma, id); if (!appointment) notFound(); const session = await auth(); try { await assertCanAccessAppointment(prisma, appointment, { sessionUserId: session?.user?.id, guestToken: token }); } catch (error) { if (error instanceof PermissionDeniedError) notFound(); // don't confirm the id exists to someone who can't see it throw error; } const business = await getPrimaryBusiness(); const totalDuration = appointment.services.reduce( (sum, s) => sum + s.durationAtBookingMinutes, 0, ); const totalPrice = appointment.services.reduce((sum, s) => sum + Number(s.priceAtBooking), 0); let cancelledByLabel: string | null = null; if (appointment.status === "CANCELLED" && appointment.cancelledByUserId) { const canceller = await prisma.user.findUnique({ where: { id: appointment.cancelledByUserId }, select: { name: true }, }); cancelledByLabel = canceller?.name ?? null; } return (
Ref: {appointment.id.slice(-8)}

{appointment.services.map((s) => s.service.name).join(" + ")}

with {appointment.staff.user.name} · {business.name}

Date {appointment.startAt.toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", timeZone: business.timezone, })}
Time {appointment.startAt.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", timeZone: business.timezone, })}
Duration {totalDuration} min
Total £{totalPrice.toFixed(2)}
{appointment.notes && (
Your Notes

{appointment.notes}

)} {appointment.status === "CANCELLED" && (
Cancellation

Cancelled{" "} {appointment.cancelledAt?.toLocaleString("en-GB", { timeZone: business.timezone })} {cancelledByLabel ? ` by ${cancelledByLabel}` : ""}

{appointment.cancellationReason && (

“{appointment.cancellationReason}”

)}
)}
{business.phone && ( )} {RESCHEDULABLE_STATUSES.has(appointment.status) && ( Reschedule )} {CANCELLABLE_STATUSES.has(appointment.status) && ( )} {REBOOKABLE_STATUSES.has(appointment.status) && ( )}
); }