import type { Metadata } from "next"; import { Plus, Search, Upload, Users } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; import { EmptyState } from "@/components/feedback/empty-state"; import { TopNav } from "@/components/navigation/top-nav"; import { Permission } from "@/generated/prisma/enums"; import { hasPermission } from "@/lib/auth/permissions"; import { prisma } from "@/lib/db/prisma"; import { getActingBusinessContext } from "@/lib/business/get-acting-business-context"; import { getUnreadNotificationCount } from "@/services/notifications/notification.service"; import { clientDirectoryWhere } from "@/services/customer/client-directory"; import { matchesCustomerSearch } from "@/services/customer/customer.service"; import { getLoyaltySummariesForCustomers } from "@/services/loyalty/loyalty.service"; import { NoShowBadge } from "@/features/staff/components/no-show-badge"; import { countNoShows, evaluateStanding, getNoShowThresholds } from "@/services/customer/no-show.service"; import { InlineNotificationBell } from "@/features/notifications/components/notification-bell"; import { cn } from "@/lib/utils/cn"; export const metadata: Metadata = { title: "Clients" }; export const dynamic = "force-dynamic"; const UPCOMING_STATUSES = ["PENDING", "CONFIRMED", "CHECKED_IN"]; export default async function StaffClientsPage({ searchParams, }: { searchParams: Promise<{ q?: string; sort?: string }>; }) { const context = await getActingBusinessContext(); if (!context) redirect("/home"); const { q = "", sort } = await searchParams; const [unreadNotificationCount, canImport] = await Promise.all([ getUnreadNotificationCount(prisma, context.userId), hasPermission(context.userId, Permission.MANAGE_BUSINESS_PROFILE), ]); // Who counts as a client is defined once, in clientDirectoryWhere (shared with // the detail page so the two can't disagree). `select`-scoped rather than a // full row + appointment `include`: the card only ever reads status/startAt // off each appointment, never the service lines/notes/discount a full // include would pull down for every appointment of every client on every // page view (see the Phase 7 performance pass). const customers = await prisma.customerProfile.findMany({ where: clientDirectoryWhere(context.businessId), select: { id: true, firstName: true, lastName: true, email: true, phone: true, noShowResetAt: true, appointments: { where: { businessId: context.businessId }, orderBy: { startAt: "desc" }, select: { status: true, startAt: true }, }, }, }); const noShowThresholds = await getNoShowThresholds(prisma, context.businessId); const loyaltySummaries = await getLoyaltySummariesForCustomers(prisma, { businessId: context.businessId, customerIds: customers.map((c) => c.id), }); const filtered = customers.filter((c) => matchesCustomerSearch(c, q)); const sorted = [...filtered].sort((a, b) => { if (sort === "recent") { const aLatest = a.appointments[0]?.startAt.getTime() ?? 0; const bLatest = b.appointments[0]?.startAt.getTime() ?? 0; return bLatest - aLatest; } return `${a.lastName} ${a.firstName}`.localeCompare(`${b.lastName} ${b.firstName}`); }); const now = new Date(); return (
} action={
{canImport && (
} />
{sort && }
); }