import type { Metadata } from "next"; import { Star } from "lucide-react"; import Link from "next/link"; import { auth } from "@/auth"; import { AppointmentCard } from "@/components/ui/appointment-card"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { ImageGallery } from "@/components/ui/image-gallery"; import { GalleryPlacement, Weekday } from "@/generated/prisma/enums"; import type { Prisma } from "@/generated/prisma/client"; import { computeOpeningStatus } from "@/lib/business/opening-status"; import { getPrimaryBusiness } from "@/lib/business/get-primary-business"; import { prisma } from "@/lib/db/prisma"; import { getAvailableServices } from "@/services/scheduling/availability.service"; import { listGalleryImages } from "@/services/gallery/gallery.service"; import { getLoyaltySummary, type LoyaltySummary } from "@/services/loyalty/loyalty.service"; import { resolveRewardThreshold } from "@/services/loyalty/reward-threshold"; import { getBusinessReviews } from "@/services/reviews/reviews.service"; import { HomeTabs } from "@/features/home/components/home-tabs"; import type { AppointmentStatus } from "@/types/domain"; export const metadata: Metadata = { title: "Home" }; export const dynamic = "force-dynamic"; const WEEKDAY_ORDER: Weekday[] = [ Weekday.MONDAY, Weekday.TUESDAY, Weekday.WEDNESDAY, Weekday.THURSDAY, Weekday.FRIDAY, Weekday.SATURDAY, Weekday.SUNDAY, ]; const WEEKDAY_LABEL: Record = { MONDAY: "Monday", TUESDAY: "Tuesday", WEDNESDAY: "Wednesday", THURSDAY: "Thursday", FRIDAY: "Friday", SATURDAY: "Saturday", SUNDAY: "Sunday", }; const STATUS_MAP: Record = { PENDING: "pending_approval", CONFIRMED: "confirmed", CHECKED_IN: "arrived", IN_PROGRESS: "in_progress", }; export default async function CustomerHomePage() { const [business, session] = await Promise.all([getPrimaryBusiness(), auth()]); const [services, categories, staff, reviews, heroImages, portfolioImages] = await Promise.all([ getAvailableServices(prisma, { businessId: business.id }), prisma.serviceCategory.findMany({ where: { businessId: business.id, isActive: true }, orderBy: { displayOrder: "asc" } }), prisma.staffProfile.findMany({ where: { businessId: business.id, status: "ACTIVE", isBookableOnline: true }, include: { user: { select: { name: true, image: true } } }, }), getBusinessReviews(business.id), listGalleryImages(prisma, { businessId: business.id, placement: GalleryPlacement.HERO, activeOnly: true }), listGalleryImages(prisma, { businessId: business.id, placement: GalleryPlacement.PORTFOLIO, activeOnly: true }), ]); const openingStatus = computeOpeningStatus(business.hours, business.timezone); const location = business.locations[0]; // Hero: owner-managed gallery first; falls back to a single cover/logo // image (still fully functional with zero gallery rows configured — // see docs/phase-9-summary.md, "Home Screen Redesign"). const heroGalleryImages = heroImages.length > 0 ? heroImages.map((i) => ({ id: i.id, url: i.imageUrl, caption: i.caption })) : business.coverImageUrl || business.logoUrl ? [{ id: "fallback", url: (business.coverImageUrl ?? business.logoUrl)!, caption: null }] : []; const hoursRows = WEEKDAY_ORDER.map((day) => { const dayHours = business.hours.find((h) => h.dayOfWeek === day); const hoursLabel = !dayHours || dayHours.isClosed || dayHours.opensAtMinutes === null || dayHours.closesAtMinutes === null ? "Closed" : `${String(Math.floor(dayHours.opensAtMinutes / 60)).padStart(2, "0")}:${String(dayHours.opensAtMinutes % 60).padStart(2, "0")} – ${String(Math.floor(dayHours.closesAtMinutes / 60)).padStart(2, "0")}:${String(dayHours.closesAtMinutes % 60).padStart(2, "0")}`; return { label: WEEKDAY_LABEL[day], hoursLabel }; }); // Keyless Google Maps embed — no API key/SDK needed for a simple pinned // map, unlike the JS Maps SDK. Prefers exact coordinates; falls back to // the street address when they're not set. const mapQuery = location?.latitude && location?.longitude ? `${location.latitude},${location.longitude}` : location ? `${location.addressLine1}, ${location.city} ${location.postalCode}` : null; const mapEmbedSrc = mapQuery ? `https://www.google.com/maps?q=${encodeURIComponent(mapQuery)}&output=embed` : null; type UpcomingAppointment = Prisma.AppointmentGetPayload<{ include: { services: { include: { service: true } }; staff: { include: { user: true } } }; }>; let customerProfile: Awaited> = null; let upcomingAppointment: UpcomingAppointment | null = null; let loyalty: LoyaltySummary | null = null; let preferredBarberName: string | null = null; let lastCompletedAppointment: Awaited> = null; if (session?.user?.id) { customerProfile = await prisma.customerProfile.findUnique({ where: { userId: session.user.id }, }); if (customerProfile) { [upcomingAppointment, loyalty, lastCompletedAppointment] = await Promise.all([ prisma.appointment.findFirst({ where: { customerId: customerProfile.id, status: { in: ["PENDING", "CONFIRMED", "CHECKED_IN"] }, startAt: { gte: new Date() }, }, orderBy: { startAt: "asc" }, include: { services: { include: { service: true } }, staff: { include: { user: true } } }, }), getLoyaltySummary(prisma, { customerId: customerProfile.id, businessId: business.id }), prisma.appointment.findFirst({ where: { customerId: customerProfile.id, status: "COMPLETED" }, orderBy: { startAt: "desc" }, }), ]); if (customerProfile.preferredBarberId) { const preferred = await prisma.staffProfile.findUnique({ where: { id: customerProfile.preferredBarberId }, include: { user: { select: { name: true } } }, }); preferredBarberName = preferred?.user.name ?? null; } } } return (
{/* Hero — an owner-managed gallery (see /owner/settings/gallery), matching the content width rather than bleeding edge-to-edge. */} {heroGalleryImages.length > 0 && (
)}
{/* {session?.user && (

Good to see you, {session.user.name?.split(" ")[0] ?? "there"}

)} */}

{business.name}

{location && (

{location.addressLine1}, {location.city} {location.postalCode}

)}
· {reviews.totalReviews.toLocaleString()} reviews {openingStatus.label}
{/* Upcoming appointment */} {upcomingAppointment && (
{session?.user && (

{session.user.name?.split(" ")[0] ?? "Your Next Appointment"}, Your Next Appointment

)} s.service.name).join(" + ")} person={{ id: upcomingAppointment.staffId, name: upcomingAppointment.staff.user.name ?? "Barber", }} formattedDate={upcomingAppointment.startAt.toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", timeZone: business.timezone, })} formattedTime={upcomingAppointment.startAt.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", timeZone: business.timezone, })} actions={ View details } />
)} {/* Loyalty summary */} {loyalty?.isEnabled && loyalty.accountId && ( {loyalty.availableReward ? (

Reward available — {loyalty.availableReward.label}

View
) : ( <>

Loyalty Progress

{loyalty.currentStamps}/{resolveRewardThreshold(loyalty)} stamps
)} )} {/* Preferred barber + book again */} {(preferredBarberName || lastCompletedAppointment) && (
{preferredBarberName && (

Preferred Barber

{preferredBarberName}

)} {/* {lastCompletedAppointment && ( )} */}
)} {/* Booking CTA */} ({ id: c.id, name: c.name }))} portfolioImages={portfolioImages} reviews={reviews} description={business.description} phone={business.phone} email={business.email} website={business.website} instagramUrl={business.instagramUrl} facebookUrl={business.facebookUrl} hoursRows={hoursRows} staff={staff.map((s) => ({ id: s.id, name: s.user.name ?? "Barber", avatarUrl: s.user.image ?? undefined, isPreferred: customerProfile?.preferredBarberId === s.id, }))} mapEmbedSrc={mapEmbedSrc} />
); }