From e507a6d48b9ec5d23d08262d46b26b386d305e95 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:28:35 -0400 Subject: [PATCH] feat(web): fix shell chrome, logo home, and live nav badges (#14) Refs: AP-42 --- e2e/home.spec.ts | 18 +++++++++ playwright.config.ts | 2 +- src/layout/AppShell.tsx | 75 ++++++++++++++++++++++-------------- src/layout/nav-badge.test.ts | 19 +++++++++ src/layout/nav.ts | 16 +++++--- 5 files changed, 96 insertions(+), 34 deletions(-) create mode 100644 src/layout/nav-badge.test.ts diff --git a/e2e/home.spec.ts b/e2e/home.spec.ts index f77e657..939e52b 100644 --- a/e2e/home.spec.ts +++ b/e2e/home.spec.ts @@ -8,3 +8,21 @@ test("home page shows Stampli-parity chrome and mocks", async ({ page }) => { await expect(page.getByTestId("page-title")).toHaveText("Invoice Processing"); await expect(page.getByTestId("mocks-status")).toContainText("Mocks enabled"); }); + +test("AP-42 shell chrome: wordmark home, no org label, single nav badge", async ({ page }) => { + await page.goto("/payments"); + await expect(page.getByTestId("payment-processing")).toBeVisible(); + + await page.getByTestId("app-wordmark").click(); + await expect(page.getByTestId("page-title")).toHaveText("Invoice Processing"); + + await expect(page.getByTestId("header-user-name")).toBeVisible(); + await expect(page.getByText("Seahaven", { exact: true })).toHaveCount(0); + + const invoiceNav = page.getByTestId("nav-invoice-processing"); + await expect(invoiceNav.locator(".MuiBadge-badge")).toHaveCount(1); + await expect(invoiceNav.locator(".MuiBadge-badge")).toHaveText(/^\d+$|^99\+$/); + + const paymentNav = page.getByTestId("nav-payment-processing"); + await expect(paymentNav.locator(".MuiBadge-badge")).toHaveCount(1); +}); diff --git a/playwright.config.ts b/playwright.config.ts index 93d2bfa..37e011d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -30,7 +30,7 @@ export default defineConfig({ ], webServer: [ { - command: "npm run dev -- --port 4173 --strictPort", + command: "VITE_SEA_HAVEN_BRAND=false npm run dev -- --port 4173 --strictPort", url: "http://localhost:4173", reuseExistingServer: !process.env.CI, timeout: 120_000, diff --git a/src/layout/AppShell.tsx b/src/layout/AppShell.tsx index bcc7448..a0c3d71 100644 --- a/src/layout/AppShell.tsx +++ b/src/layout/AppShell.tsx @@ -9,6 +9,7 @@ import ReceiptLongOutlinedIcon from "@mui/icons-material/ReceiptLongOutlined"; import SearchIcon from "@mui/icons-material/Search"; import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined"; import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined"; +import { isReadyToPay } from "@seahaven-ap/shared"; import { AppBar, Badge, @@ -26,10 +27,11 @@ import { Typography, } from "@mui/material"; import type { ReactElement, ReactNode } from "react"; +import { useMemo } from "react"; import { useLocation } from "react-router"; import { brand } from "@/lib/brand"; -import { navModules } from "@/layout/nav"; -import { getMockUsers } from "@/mocks"; +import { formatNavBadgeCount, navModules } from "@/layout/nav"; +import { getMockInvoices, getMockUsers } from "@/mocks"; import { useMockSession } from "@/providers/use-mock-session"; import { useNavigate, type Path } from "@/router"; @@ -64,6 +66,18 @@ export function AppShell({ children }: AppShellProps): ReactElement { const { user, setCurrentUserId } = useMockSession(); const users = getMockUsers(); + const badgeByTestId = useMemo(() => { + const invoices = getMockInvoices(); + const attentionCount = invoices.filter( + (invoice) => invoice.status === "pending_approval", + ).length; + const readyToPayCount = invoices.filter((invoice) => isReadyToPay(invoice.status)).length; + return { + "nav-invoice-processing": formatNavBadgeCount(attentionCount), + "nav-payment-processing": formatNavBadgeCount(readyToPayCount), + } as Record; + }, []); + return ( navigate("/")} sx={{ fontWeight: 800, letterSpacing: "0.08em", fontSize: "1.05rem", mr: 1, flexShrink: 0, + border: 0, + background: "transparent", + color: "inherit", + cursor: "pointer", + p: 0, + fontFamily: "inherit", }} > {brand.wordmark} @@ -136,19 +158,25 @@ export function AppShell({ children }: AppShellProps): ReactElement { - - - - - - - {brand.orgLabel} - - - {user.name} - - - + + + + + + + + {user.name} + + Mock user @@ -204,6 +232,7 @@ export function AppShell({ children }: AppShellProps): ReactElement { .map((item) => { const selected = isSelected(location.pathname, item.to); const Icon = iconByTestId[item.testId] ?? ReceiptLongOutlinedIcon; + const badgeContent = badgeByTestId[item.testId]; return ( - - - + { + it("hides zero", () => { + expect(formatNavBadgeCount(0)).toBeUndefined(); + }); + + it("shows exact counts under 100", () => { + expect(formatNavBadgeCount(1)).toBe("1"); + expect(formatNavBadgeCount(6)).toBe("6"); + expect(formatNavBadgeCount(99)).toBe("99"); + }); + + it("caps at 99+", () => { + expect(formatNavBadgeCount(100)).toBe("99+"); + expect(formatNavBadgeCount(250)).toBe("99+"); + }); +}); diff --git a/src/layout/nav.ts b/src/layout/nav.ts index cd38a6d..a16ca2b 100644 --- a/src/layout/nav.ts +++ b/src/layout/nav.ts @@ -4,8 +4,6 @@ export type NavModule = { label: string; to: Path; testId: string; - badge?: string; - secondaryBadge?: string; }; export const navModules: NavModule[] = [ @@ -14,14 +12,11 @@ export const navModules: NavModule[] = [ label: "Invoice Processing", to: "/", testId: "nav-invoice-processing", - badge: "99+", - secondaryBadge: "6", }, { label: "Payment Processing", to: "/payments", testId: "nav-payment-processing", - badge: "99+", }, { label: "Approvals", to: "/approvals", testId: "nav-approvals" }, { label: "Vendor Management", to: "/settings/vendors", testId: "nav-vendors" }, @@ -30,3 +25,14 @@ export const navModules: NavModule[] = [ { label: "Dashboards & Reports", to: "/reports", testId: "nav-reports" }, { label: "System Settings", to: "/settings", testId: "nav-settings" }, ]; + +/** Format a nav count badge: hide at 0, cap at 99+. */ +export function formatNavBadgeCount(count: number): string | undefined { + if (count <= 0) { + return undefined; + } + if (count >= 100) { + return "99+"; + } + return String(count); +}