diff --git a/src/__tests__/root-page-manager-mode.test.ts b/src/__tests__/root-page-manager-mode.test.ts deleted file mode 100644 index 26f57e4..0000000 --- a/src/__tests__/root-page-manager-mode.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; - -vi.mock("next/navigation", () => ({ - redirect: (location: string) => { - throw new Error(`redirect:${location}`); - }, -})); - -vi.mock("@/lib/db/prisma", () => { - throw new Error("Business Prisma must not load while importing the Manager root."); -}); - -vi.mock("@/features/welcome/pages/business-welcome-page", () => { - throw new Error("Business welcome page must not load while importing the Manager root."); -}); - -import RootPage from "@/app/page"; - -describe("root deployment entry point", () => { - const originalManagerMode = process.env.MANAGER_MODE; - - afterEach(() => { - if (originalManagerMode === undefined) delete process.env.MANAGER_MODE; - else process.env.MANAGER_MODE = originalManagerMode; - }); - - it("redirects Manager root without evaluating business page or Prisma", async () => { - process.env.MANAGER_MODE = "true"; - - await expect(RootPage()).rejects.toThrow("redirect:/manager"); - }); -}); diff --git a/src/app/page.tsx b/src/app/page.tsx index c88976c..bd28ae5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,10 +1,125 @@ +import { Scissors } from "lucide-react"; +import Link from "next/link"; import { redirect } from "next/navigation"; +import { auth } from "@/auth"; +import { Button } from "@/components/ui/button"; +import { getBranding } from "@/lib/business/branding"; +import { getPrimaryBusiness } from "@/lib/business/get-primary-business"; +import { hasPermission, isSuperAdmin } from "@/lib/auth/permissions"; +import { Permission } from "@/generated/prisma/enums"; +import { prisma } from "@/lib/db/prisma"; +import { WelcomeInstallCard } from "@/features/pwa/components/welcome-install-card"; -/** Deployment-mode entry point. Keep this module database-free: Next can - * evaluate the root route module before running the page function. */ -export default async function RootPage() { - if (process.env.MANAGER_MODE === "true") redirect("/manager"); +/** + * The unauthenticated entry point — always shown at `/` before + * authentication, per the brief's "Welcome routing" requirements. + * Authenticated visitors never see it: they're routed straight to their + * role's calendar (barber → /staff, owner/super admin → /owner/calendar) + * or customer home below. This is also where a bare sign-in with no + * `callbackUrl` ends up — see safeRedirectPath's fallback in + * sign-in-form.tsx — so it's the one place that has to get every role + * right, not just staffProfile holders. Unreachable at all before + * installation completes — src/proxy.ts redirects every non-exempt route + * (including this one) to /install until ApplicationInstallation flips + * to INSTALLED. + */ +export default async function WelcomePage() { + const session = await auth(); + if (session?.user) { + const staffProfile = await prisma.staffProfile.findUnique({ where: { userId: session.user.id } }); + if (staffProfile) { + const canManageStaff = await hasPermission(session.user.id, Permission.MANAGE_STAFF); + redirect(canManageStaff ? "/owner" : "/staff"); + } + // A Super Administrator has no StaffProfile by design (see + // isSuperAdmin) — without this check they'd fall through to the + // customer /home below instead of the staff shell + Calendar tab + // their nav role (see lib/navigation/nav-config.ts) actually gives + // them once they land somewhere in it. + if (await isSuperAdmin(session.user.id)) redirect("/owner/calendar"); + redirect("/home"); + } - const { default: BusinessWelcomePage } = await import("@/features/welcome/pages/business-welcome-page"); - return ; + const [branding, business] = await Promise.all([getBranding(), getPrimaryBusiness()]); + const guestBookingEnabled = business.bookingRule?.allowGuestBooking ?? false; + + // The hero card's photo — the welcome-specific background the owner + // uploaded for this exact screen takes priority (it was purpose-built for + // it, see BrandingInfo.welcomeBackgroundMobileUrl/DesktopUrl); the + // general-purpose cover shot is only a fallback for a business that + // configured a cover image but never set a dedicated welcome background. + // Desktop and mobile keep their own independent images, same as before. + const heroImageMobileUrl = branding.welcomeBackgroundMobileUrl ?? branding.welcomeBackgroundDesktopUrl ?? branding.coverImageUrl; + const heroImageDesktopUrl = branding.welcomeBackgroundDesktopUrl ?? branding.welcomeBackgroundMobileUrl ?? branding.coverImageUrl; + + const hasHeroImage = !!(heroImageMobileUrl || heroImageDesktopUrl); + + return ( +
+ {/* Full-bleed background — desktop/mobile are independent images, not one crop stretched two ways. */} + {heroImageMobileUrl && ( +