mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-09-30 05:43:18 +00:00
feat(web): fix shell chrome, logo home, and live nav badges (#14)
Refs: AP-42
This commit is contained in:
parent
ba20f365e3
commit
e507a6d48b
5 changed files with 96 additions and 34 deletions
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<string, string | undefined>;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", minHeight: "100vh", bgcolor: "background.default" }}>
|
||||
<AppBar
|
||||
|
|
@ -77,14 +91,22 @@ export function AppShell({ children }: AppShellProps): ReactElement {
|
|||
>
|
||||
<Toolbar sx={{ gap: 2, minHeight: 56, px: 2 }}>
|
||||
<Typography
|
||||
component="div"
|
||||
component="button"
|
||||
type="button"
|
||||
data-testid="app-wordmark"
|
||||
onClick={() => 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 {
|
|||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ ml: "auto" }}>
|
||||
<Stack direction="row" spacing={2} alignItems="center" sx={{ ml: "auto", flexShrink: 0 }}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="Notifications"
|
||||
size="small"
|
||||
sx={{ flexShrink: 0 }}
|
||||
>
|
||||
<Badge badgeContent={12} color="error" data-testid="notif-badge">
|
||||
<NotificationsNoneOutlinedIcon />
|
||||
</Badge>
|
||||
<Box sx={{ textAlign: "right", lineHeight: 1.2 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>
|
||||
{brand.orgLabel}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ opacity: 0.9 }}>
|
||||
</IconButton>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: 700, whiteSpace: "nowrap", flexShrink: 0 }}
|
||||
data-testid="header-user-name"
|
||||
>
|
||||
{user.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
<FormControl size="small" sx={{ minWidth: 180 }}>
|
||||
<FormControl size="small" sx={{ minWidth: 180, flexShrink: 0 }}>
|
||||
<InputLabel id="mock-user-label" sx={{ color: "rgba(255,255,255,0.8)" }}>
|
||||
Mock user
|
||||
</InputLabel>
|
||||
|
|
@ -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 (
|
||||
<Tooltip key={item.testId} title={item.label} placement="right">
|
||||
<Box
|
||||
|
|
@ -229,25 +258,15 @@ export function AppShell({ children }: AppShellProps): ReactElement {
|
|||
}}
|
||||
>
|
||||
<Badge
|
||||
badgeContent={item.badge}
|
||||
badgeContent={badgeContent}
|
||||
color="primary"
|
||||
overlap="circular"
|
||||
sx={{
|
||||
"& .MuiBadge-badge": { fontSize: "0.65rem", height: 16, minWidth: 16 },
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
badgeContent={item.secondaryBadge}
|
||||
color="secondary"
|
||||
overlap="circular"
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
sx={{
|
||||
"& .MuiBadge-badge": { fontSize: "0.6rem", height: 14, minWidth: 14 },
|
||||
}}
|
||||
>
|
||||
<Icon fontSize="small" />
|
||||
</Badge>
|
||||
</Badge>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
|
|
|
|||
19
src/layout/nav-badge.test.ts
Normal file
19
src/layout/nav-badge.test.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { formatNavBadgeCount } from "@/layout/nav";
|
||||
|
||||
describe("formatNavBadgeCount", () => {
|
||||
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+");
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue