import type { Metadata } from "next"; import Image from "next/image"; import Link from "next/link"; import { CalendarX2, Mail, Phone } from "lucide-react"; import { notFound, redirect } from "next/navigation"; import { AppointmentCard } from "@/components/ui/appointment-card"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { EmptyState } from "@/components/feedback/empty-state"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { TopNav } from "@/components/navigation/top-nav"; import { prisma } from "@/lib/db/prisma"; import { getActingStaff } from "@/lib/staff/get-acting-staff"; import { getLoyaltyHistory, getLoyaltySummary } from "@/services/loyalty/loyalty.service"; import { listAppointments } from "@/services/scheduling/appointment.service"; import { AddStampButton } from "@/features/staff/components/add-stamp-button"; import { CorrectStampButton } from "@/features/staff/components/correct-stamp-button"; import { CustomerNotes } from "@/features/staff/components/customer-notes"; import type { CustomerNoteItem } from "@/features/staff/components/customer-notes"; import type { AppointmentStatus } from "@/types/domain"; export const metadata: Metadata = { title: "Client 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", }; export default async function StaffCustomerDetailsPage({ params, }: { params: Promise<{ id: string }>; }) { const staff = await getActingStaff(); if (!staff) redirect("/home"); const { id } = await params; const customer = await prisma.customerProfile.findUnique({ where: { id }, include: { preferredBarber: { include: { user: { select: { name: true } } } }, user: { select: { image: true } }, }, }); if (!customer) notFound(); // "Belongs to this business" is via the appointment relation — see // docs/phase-5-summary.md, same reasoning as the clients list query. const belongsToBusiness = await prisma.appointment.findFirst({ where: { customerId: id, businessId: staff.businessId }, select: { id: true }, }); if (!belongsToBusiness) notFound(); const [upcoming, past, cancelled, notesRaw, loyalty] = await Promise.all([ listAppointments(prisma, { customerId: id, businessId: staff.businessId, statuses: ["PENDING", "CONFIRMED", "CHECKED_IN", "IN_PROGRESS"], }), listAppointments(prisma, { customerId: id, businessId: staff.businessId, statuses: ["COMPLETED"], }), listAppointments(prisma, { customerId: id, businessId: staff.businessId, statuses: ["CANCELLED", "NO_SHOW"], }), prisma.customerNote.findMany({ where: { customerId: id }, orderBy: { createdAt: "desc" }, include: { author: { include: { user: { select: { name: true } } } } }, }), getLoyaltySummary(prisma, { customerId: id, businessId: staff.businessId }), ]); const loyaltyTransactions = loyalty?.accountId ? await getLoyaltyHistory(prisma, { loyaltyAccountId: loyalty.accountId, limit: 10 }) : []; const notes: CustomerNoteItem[] = notesRaw.map((n) => ({ id: n.id, body: n.body, createdAt: n.createdAt.toISOString(), updatedAt: n.updatedAt.toISOString(), authorStaffId: n.authorStaffId, authorName: n.author.user.name ?? "Staff", })); const tz = staff.business.timezone; const renderList = (appointments: typeof upcoming, emptyLabel: string) => { if (appointments.length === 0) { return ; } return (
{appointments.map((a) => ( s.service.name).join(" + ")} person={{ id: a.staffId, name: a.staff.user.name ?? "Barber" }} formattedDate={a.startAt.toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", timeZone: tz, })} formattedTime={a.startAt.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", timeZone: tz, })} actions={ View } /> ))}
); }; return (
{customer.user?.image ? ( ) : (
{customer.firstName.charAt(0)} {customer.lastName.charAt(0)}
)}

{customer.firstName} {customer.lastName}

{customer.isGuest ? "Guest customer" : "Registered customer"}

{customer.email}

{customer.phone &&

{customer.phone}

} {customer.preferredBarber && (

Preferred barber: {customer.preferredBarber.user.name ?? "—"}

)}
{customer.phone && ( )}
{loyalty?.isEnabled && loyalty.accountId && (
Loyalty

{loyalty.currentStamps}/{loyalty.stampsRequired} stamps {loyalty.availableReward && ( · Reward available )}

{loyaltyTransactions.length > 0 && (
    {loyaltyTransactions.map((t) => (
  • {t.createdAt.toLocaleDateString("en-GB", { day: "numeric", month: "short" })}{" "} — {t.amount > 0 ? "+" : ""} {t.amount} stamp{Math.abs(t.amount) === 1 ? "" : "s"} — {t.type.replace(/_/g, " ").toLowerCase()} {t.reason ? ` (${t.reason})` : ""} {t.authoredByStaff?.user.name ? ` · ${t.authoredByStaff.user.name}` : ""}
  • ))}
)}
)}

{past.length}

Visits

{upcoming.length}

Upcoming

{cancelled.length}

Cancelled

Upcoming Past Cancelled {renderList(upcoming, "No upcoming appointments")} {renderList(past, "No past appointments")} {renderList(cancelled, "No cancelled appointments")}

Notes

); }