From 20d6f56f7ecd9bede85bf6f73322cdf3ac6c43ce Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:06:14 -0400 Subject: [PATCH] feat(frontend): add Stampli visual-parity chrome (AP-22) (#10) Rebuild the mock SPA shell to match the live Stampli product map (header, module rail, invoice/payment tabs, stubs) and add a Sea Haven brand flag for AP-23 cutover. --- .env.example | 4 + e2e/dashboard-grids.spec.ts | 12 +- e2e/home.spec.ts | 7 +- e2e/settings-approvals.spec.ts | 8 +- src/components/ModuleStubPage.tsx | 38 +++ src/dashboard/DashboardGrids.tsx | 198 ++++++++++----- src/dashboard/PaymentProcessingGrids.tsx | 237 ++++++++++++++++++ src/dashboard/columns.tsx | 35 ++- src/layout/AppShell.tsx | 300 +++++++++++++++++------ src/layout/nav.ts | 33 +++ src/lib/brand.ts | 29 +++ src/lib/mui-theme.ts | 56 ++++- src/pages/_app.tsx | 7 +- src/pages/billing/index.tsx | 12 + src/pages/index.tsx | 21 +- src/pages/login.tsx | 49 ++++ src/pages/past-invoices/index.tsx | 12 + src/pages/payments/index.tsx | 15 ++ src/pages/procurement/index.tsx | 12 + src/pages/recurring/index.tsx | 12 + src/pages/reports/index.tsx | 12 + src/vite-env.d.ts | 1 + 22 files changed, 948 insertions(+), 162 deletions(-) create mode 100644 src/components/ModuleStubPage.tsx create mode 100644 src/dashboard/PaymentProcessingGrids.tsx create mode 100644 src/layout/nav.ts create mode 100644 src/lib/brand.ts create mode 100644 src/pages/billing/index.tsx create mode 100644 src/pages/login.tsx create mode 100644 src/pages/past-invoices/index.tsx create mode 100644 src/pages/payments/index.tsx create mode 100644 src/pages/procurement/index.tsx create mode 100644 src/pages/recurring/index.tsx create mode 100644 src/pages/reports/index.tsx diff --git a/.env.example b/.env.example index 9a89fbd..2cec3c4 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,6 @@ # Default data path until the API is wired (AP later tickets). VITE_USE_MOCKS=true + +# AP-22 visual parity uses Stampli wordmark when false/unset. +# AP-38 Sea Haven branding cutover: set true. +VITE_SEA_HAVEN_BRAND=false diff --git a/e2e/dashboard-grids.spec.ts b/e2e/dashboard-grids.spec.ts index 3991192..160476a 100644 --- a/e2e/dashboard-grids.spec.ts +++ b/e2e/dashboard-grids.spec.ts @@ -4,7 +4,7 @@ test.describe("dashboard grids", () => { test("home page shows brand and three invoice grids", async ({ page }) => { await page.goto("/"); - await expect(page.getByRole("heading", { name: "Sea Haven AP" })).toBeVisible(); + await expect(page.getByTestId("app-wordmark")).toContainText("STAMPLI"); await expect(page.getByTestId("mocks-status")).toContainText("Mocks enabled"); await expect( @@ -18,6 +18,16 @@ test.describe("dashboard grids", () => { await expect(page.getByTestId("grid-all").locator(".MuiDataGrid-row").first()).toBeVisible(); }); + test("payment processing select to pay is reachable from module rail", async ({ page }) => { + await page.goto("/"); + await page.getByTestId("nav-payment-processing").click(); + await expect(page.getByTestId("payment-processing")).toBeVisible(); + await expect(page.getByTestId("tab-select-to-pay")).toBeVisible(); + await expect( + page.getByTestId("grid-select-to-pay").locator(".MuiDataGrid-row").first(), + ).toBeVisible(); + }); + test("ready to pay supports selection, create pay run stub, and pay-date edit", async ({ page, }) => { diff --git a/e2e/home.spec.ts b/e2e/home.spec.ts index c4a6043..f77e657 100644 --- a/e2e/home.spec.ts +++ b/e2e/home.spec.ts @@ -1,7 +1,10 @@ import { expect, test } from "@playwright/test"; -test("home page shows Sea Haven AP brand", async ({ page }) => { +test("home page shows Stampli-parity chrome and mocks", async ({ page }) => { await page.goto("/"); - await expect(page.getByRole("heading", { name: "Sea Haven AP" })).toBeVisible(); + await expect(page.getByTestId("app-wordmark")).toContainText("STAMPLI"); + await expect(page.getByTestId("module-rail")).toBeVisible(); + await expect(page.getByTestId("nav-invoice-processing")).toBeVisible(); + await expect(page.getByTestId("page-title")).toHaveText("Invoice Processing"); await expect(page.getByTestId("mocks-status")).toContainText("Mocks enabled"); }); diff --git a/e2e/settings-approvals.spec.ts b/e2e/settings-approvals.spec.ts index d43a361..e3db14b 100644 --- a/e2e/settings-approvals.spec.ts +++ b/e2e/settings-approvals.spec.ts @@ -1,12 +1,12 @@ import { expect, test } from "@playwright/test"; test.describe("settings and approvals shells", () => { - test("demo path dashboard → settings → approvals works on mocks", async ({ page }) => { + test("demo path invoice processing → settings → approvals works on mocks", async ({ page }) => { await page.goto("/"); - await expect(page.getByRole("heading", { name: "Sea Haven AP" })).toBeVisible(); - await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible(); - await expect(page.getByTestId("nav-dashboard")).toBeVisible(); + await expect(page.getByTestId("app-wordmark")).toContainText("STAMPLI"); + await expect(page.getByTestId("page-title")).toHaveText("Invoice Processing"); + await expect(page.getByTestId("nav-invoice-processing")).toBeVisible(); await expect(page.getByTestId("nav-settings")).toBeVisible(); await expect(page.getByTestId("nav-approvals")).toBeVisible(); diff --git a/src/components/ModuleStubPage.tsx b/src/components/ModuleStubPage.tsx new file mode 100644 index 0000000..4124747 --- /dev/null +++ b/src/components/ModuleStubPage.tsx @@ -0,0 +1,38 @@ +import { Box, Button, Stack, Typography } from "@mui/material"; + +type ModuleStubProps = { + title: string; + headline: string; + body: string; + testId: string; +}; + +export function ModuleStubPage({ title, headline, body, testId }: ModuleStubProps) { + return ( + + + + {title} + + + {headline} + + {body} + + parity stub — interactive UI without live module backend + + + + + + + ); +} diff --git a/src/dashboard/DashboardGrids.tsx b/src/dashboard/DashboardGrids.tsx index c7fea2d..e6b97f9 100644 --- a/src/dashboard/DashboardGrids.tsx +++ b/src/dashboard/DashboardGrids.tsx @@ -1,15 +1,29 @@ import { useMemo, useState } from "react"; -import { Alert, Box, CircularProgress, Stack, Tab, Tabs, Typography } from "@mui/material"; +import { + Alert, + Box, + Button, + Chip, + CircularProgress, + Stack, + Tab, + Tabs, + Typography, +} from "@mui/material"; import type { GridRowParams, GridRowSelectionModel } from "@mui/x-data-grid"; import { CreatePayRunButton } from "@/dashboard/CreatePayRunButton"; import { InvoiceDataGrid } from "@/dashboard/InvoiceDataGrid"; -import { allInvoicesColumns, pendingApprovalColumns, readyToPayColumns } from "@/dashboard/columns"; +import { + allInvoicesColumns, + invoiceAttentionColumns, + readyToPayColumns, +} from "@/dashboard/columns"; import { countSelectedRows } from "@/dashboard/selection"; import { useInvoices, useUpdateInvoicePayDate } from "@/domain/invoices/use-invoices"; import { areMocksEnabled, type MockInvoice } from "@/mocks"; import { useNavigate } from "@/router"; -type DashboardTab = "pending" | "ready" | "all"; +type InvoiceTab = "attention" | "all" | "approved" | "rejected" | "late"; const emptySelection: GridRowSelectionModel = { type: "include", @@ -21,11 +35,12 @@ const rowClickSx = { borderColor: "divider", backgroundColor: "background.paper", cursor: "pointer", + borderRadius: 1, } as const; export function DashboardGrids() { const navigate = useNavigate(); - const [tab, setTab] = useState("pending"); + const [tab, setTab] = useState("attention"); const [selection, setSelection] = useState(emptySelection); const { data: invoices = [], isLoading, isError, error } = useInvoices(); const updatePayDate = useUpdateInvoicePayDate(); @@ -34,25 +49,35 @@ export function DashboardGrids() { navigate("/invoices/:id", { params: { id: params.row.id } }); }; - const pendingRows = useMemo( + const attentionRows = useMemo( () => invoices.filter((invoice) => invoice.status === "pending_approval"), [invoices], ); - const readyRows = useMemo( + const approvedRows = useMemo( () => invoices.filter((invoice) => invoice.status === "approved"), [invoices], ); + const rejectedRows = useMemo( + () => invoices.filter((invoice) => invoice.status === "rejected"), + [invoices], + ); + const lateRows = useMemo(() => { + const today = new Date().toISOString().slice(0, 10); + return invoices.filter( + (invoice) => + invoice.dueDate < today && invoice.status !== "paid" && invoice.status !== "rejected", + ); + }, [invoices]); const selectedCount = countSelectedRows( selection, - readyRows.map((invoice) => invoice.id), + approvedRows.map((invoice) => invoice.id), ); const processRowUpdate = async (newRow: MockInvoice, oldRow: MockInvoice) => { if (newRow.payDate === oldRow.payDate) { return oldRow; } - const nextPayDate = newRow.payDate?.trim() ? newRow.payDate.trim() : null; return updatePayDate.mutateAsync({ id: newRow.id, payDate: nextPayDate }); }; @@ -74,42 +99,93 @@ export function DashboardGrids() { } return ( - - { - setTab(value); - setSelection(emptySelection); - }} - aria-label="Invoice dashboard tabs" - > - - - - + + + { + setTab(value); + setSelection(emptySelection); + }} + variant="scrollable" + aria-label="Invoice Processing tabs" + sx={{ + minHeight: 44, + "& .MuiTab-root": { + bgcolor: "rgba(47,128,237,0.12)", + mr: 0.5, + borderTopLeftRadius: 6, + borderTopRightRadius: 6, + color: "text.primary", + }, + "& .Mui-selected": { + bgcolor: "#fff !important", + color: "text.primary !important", + }, + "& .MuiTabs-indicator": { display: "none" }, + }} + > + + + + + + + - {tab === "pending" && ( - - )} + + {attentionRows.length > 0 && tab === "attention" ? ( + + {attentionRows.length} invoices require approval. Show Invoices ready to approve. + + ) : null} - {tab === "ready" && ( - - - - + + + + + + {tab === "approved" ? : null} + + + {tab === "attention" && ( + + )} + + {tab === "all" && ( + + )} + + {tab === "approved" && ( - - Double-click Pay Date to edit (ISO YYYY-MM-DD). Changes update mock state only. Open - detail from the Invoice # link. - - - )} + )} - {tab === "all" && ( - - )} + {tab === "rejected" && ( + + )} - - Mocks {areMocksEnabled() ? "enabled" : "disabled"} — {invoices.length} invoices - + {tab === "late" && ( + + )} + + + Mocks {areMocksEnabled() ? "enabled" : "disabled"} — {invoices.length} invoices + + ); } diff --git a/src/dashboard/PaymentProcessingGrids.tsx b/src/dashboard/PaymentProcessingGrids.tsx new file mode 100644 index 0000000..edcbc70 --- /dev/null +++ b/src/dashboard/PaymentProcessingGrids.tsx @@ -0,0 +1,237 @@ +import { useMemo, useState } from "react"; +import { + Alert, + Box, + Button, + Chip, + CircularProgress, + Stack, + Tab, + Tabs, + Typography, +} from "@mui/material"; +import type { GridRowParams, GridRowSelectionModel } from "@mui/x-data-grid"; +import { CreatePayRunButton } from "@/dashboard/CreatePayRunButton"; +import { InvoiceDataGrid } from "@/dashboard/InvoiceDataGrid"; +import { pendingApprovalColumns, readyToPayColumns } from "@/dashboard/columns"; +import { countSelectedRows } from "@/dashboard/selection"; +import { useInvoices, useUpdateInvoicePayDate } from "@/domain/invoices/use-invoices"; +import type { MockInvoice } from "@/mocks"; +import { useNavigate } from "@/router"; + +type PaymentTab = "outside" | "select" | "pending" | "scheduled" | "errors" | "outbox"; + +const emptySelection: GridRowSelectionModel = { + type: "include", + ids: new Set(), +}; + +const rowClickSx = { + border: 1, + borderColor: "divider", + backgroundColor: "background.paper", + cursor: "pointer", + borderRadius: 1, +} as const; + +export function PaymentProcessingGrids() { + const navigate = useNavigate(); + const [tab, setTab] = useState("select"); + const [selection, setSelection] = useState(emptySelection); + const { data: invoices = [], isLoading, isError, error } = useInvoices(); + const updatePayDate = useUpdateInvoicePayDate(); + + const openInvoice = (params: GridRowParams) => { + navigate("/invoices/:id", { params: { id: params.row.id } }); + }; + + const selectRows = useMemo( + () => invoices.filter((invoice) => invoice.status === "approved"), + [invoices], + ); + const pendingRows = useMemo( + () => invoices.filter((invoice) => invoice.status === "pending_approval"), + [invoices], + ); + const scheduledRows = useMemo( + () => invoices.filter((invoice) => invoice.status === "scheduled"), + [invoices], + ); + + const selectedCount = countSelectedRows( + selection, + selectRows.map((invoice) => invoice.id), + ); + + const totalDue = selectRows.reduce((sum, row) => sum + row.amountDue, 0); + + const processRowUpdate = async (newRow: MockInvoice, oldRow: MockInvoice) => { + if (newRow.payDate === oldRow.payDate) { + return oldRow; + } + const nextPayDate = newRow.payDate?.trim() ? newRow.payDate.trim() : null; + return updatePayDate.mutateAsync({ id: newRow.id, payDate: nextPayDate }); + }; + + if (isLoading) { + return ( + + + + ); + } + + if (isError) { + return ( + + {error instanceof Error ? error.message : "Failed to load payments"} + + ); + } + + return ( + + + { + setTab(value); + setSelection(emptySelection); + }} + variant="scrollable" + aria-label="Payment Processing tabs" + sx={{ + minHeight: 44, + "& .MuiTab-root": { + bgcolor: "rgba(47,128,237,0.18)", + mr: 0.5, + borderTopLeftRadius: 6, + borderTopRightRadius: 6, + color: "text.primary", + }, + "& .Mui-selected": { + bgcolor: "#fff !important", + color: "text.primary !important", + }, + "& .MuiTabs-indicator": { display: "none" }, + }} + > + + + + + + + + + + + + Expedited payments are currently unavailable for you due to various factors. (parity stub) + + + + + + + + + + + + + {tab === "select" && ( + setSelection(model)} + processRowUpdate={processRowUpdate} + onProcessRowUpdateError={(processError) => { + console.error(processError); + }} + onRowClick={openInvoice} + sx={rowClickSx} + /> + )} + + {tab === "pending" && ( + + )} + + {tab === "scheduled" && ( + + )} + + {(tab === "outside" || tab === "errors" || tab === "outbox") && ( + + No Result Found + + parity stub + + + )} + + + + + Total Due:{" "} + + + {totalDue.toLocaleString("en-US", { style: "currency", currency: "USD" })} + + + + + Total To Pay:{" "} + + $0.00 + + + + Estimated USD:{" "} + + $0.00 + + + + + ); +} diff --git a/src/dashboard/columns.tsx b/src/dashboard/columns.tsx index 730f3b5..686468e 100644 --- a/src/dashboard/columns.tsx +++ b/src/dashboard/columns.tsx @@ -85,17 +85,40 @@ export const pendingApprovalColumns: GridColDef[] = [ moneyColumn("amount", "Amount"), ]; -export const readyToPayColumns: GridColDef[] = [ - dateColumn("dueDate", "Due Date"), - dateColumn("payDate", "Pay Date", true), - { field: "vendorName", headerName: "Vendor", flex: 1.4, minWidth: 160 }, +/** Invoice Processing — For My Attention columns (AP-26). */ +export const invoiceAttentionColumns: GridColDef[] = [ { - field: "invoiceNumber", - headerName: "Invoice #", + field: "invoiceNumbers", + headerName: "Invoice", flex: 1, minWidth: 120, renderCell: invoiceNumberLink, }, + { + field: "status", + headerName: "Action", + flex: 1.2, + minWidth: 140, + valueFormatter: (value: string) => (value === "pending_approval" ? "Approve invoice" : value), + }, + { field: "vendorName", headerName: "Vendor", flex: 1.4, minWidth: 160 }, + dateColumn("dueDate", "Due Date"), + moneyColumn("amountDue", "Σ Sum"), +]; + +export const readyToPayColumns: GridColDef[] = [ + { + field: "invoiceNumber", + headerName: "Invoice", + flex: 1, + minWidth: 120, + renderCell: invoiceNumberLink, + }, + { field: "vendorName", headerName: "Vendor Name", flex: 1.4, minWidth: 160 }, + { field: "paymentMethod", headerName: "Payment Method", flex: 1, minWidth: 130 }, + dateColumn("sendPaymentOn", "Send Payment On"), + dateColumn("dueDate", "Due Date"), + dateColumn("payDate", "Pay Date", true), moneyColumn("amountDue", "Amount Due"), ]; diff --git a/src/layout/AppShell.tsx b/src/layout/AppShell.tsx index 4123e79..df48886 100644 --- a/src/layout/AppShell.tsx +++ b/src/layout/AppShell.tsx @@ -1,126 +1,282 @@ +import AccountBalanceWalletOutlinedIcon from "@mui/icons-material/AccountBalanceWalletOutlined"; +import AssessmentOutlinedIcon from "@mui/icons-material/AssessmentOutlined"; +import AutorenewOutlinedIcon from "@mui/icons-material/AutorenewOutlined"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import FolderOutlinedIcon from "@mui/icons-material/FolderOutlined"; +import Inventory2OutlinedIcon from "@mui/icons-material/Inventory2Outlined"; +import NotificationsNoneOutlinedIcon from "@mui/icons-material/NotificationsNoneOutlined"; +import PaidOutlinedIcon from "@mui/icons-material/PaidOutlined"; +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 { AppBar, + Badge, Box, - Drawer, FormControl, + IconButton, + InputAdornment, InputLabel, - List, - ListItemButton, - ListItemText, MenuItem, Select, + Stack, + TextField, Toolbar, + Tooltip, Typography, } from "@mui/material"; -import type { ReactNode } from "react"; +import type { ReactElement, ReactNode } from "react"; import { useLocation } from "react-router"; -import { useNavigate, type Path } from "@/router"; +import { brand } from "@/lib/brand"; +import { navModules } from "@/layout/nav"; import { getMockUsers } from "@/mocks"; import { useMockSession } from "@/providers/use-mock-session"; +import { useNavigate, type Path } from "@/router"; -const DRAWER_WIDTH = 240; +const RAIL_WIDTH = 96; -type NavItem = { - label: string; - to: Path; - testId: string; +const iconByTestId: Record = { + "nav-procurement": StorefrontOutlinedIcon, + "nav-invoice-processing": ReceiptLongOutlinedIcon, + "nav-payment-processing": AccountBalanceWalletOutlinedIcon, + "nav-approvals": FolderOutlinedIcon, + "nav-vendors": Inventory2OutlinedIcon, + "nav-recurring": AutorenewOutlinedIcon, + "nav-past-invoices": FolderOutlinedIcon, + "nav-reports": AssessmentOutlinedIcon, + "nav-settings": SettingsOutlinedIcon, + "nav-billing": PaidOutlinedIcon, }; -const navItems: NavItem[] = [ - { label: "Dashboard", to: "/", testId: "nav-dashboard" }, - { label: "Approvals", to: "/approvals", testId: "nav-approvals" }, - { label: "Settings", to: "/settings", testId: "nav-settings" }, -]; - type AppShellProps = { children: ReactNode; }; -export function AppShell({ children }: AppShellProps) { +function isSelected(pathname: string, to: Path): boolean { + if (to === "/") { + return pathname === "/" || pathname.startsWith("/invoices/"); + } + return pathname === to || pathname.startsWith(`${to}/`); +} + +export function AppShell({ children }: AppShellProps): ReactElement { const location = useLocation(); const navigate = useNavigate(); const { user, setCurrentUserId } = useMockSession(); const users = getMockUsers(); return ( - + theme.zIndex.drawer + 1, - borderBottom: 1, - borderColor: "divider", - bgcolor: "background.paper", - color: "text.primary", + background: brand.headerGradient, + color: "#fff", }} > - - - Sea Haven AP + + + {brand.wordmark} - - Mock user - - + + + + + ), + endAdornment: ( + + Advanced + + ), + }, + }} + /> + + + + Billy coded 1,693 fields on 99 invoices this month | Learn more + + + + + + + + + + {brand.orgLabel} + + + {user.name} + + + + + Mock user + + + + - theme.zIndex.appBar - 1, }} > - - - - {navItems.map((item) => { - const selected = - item.to === "/" - ? location.pathname === "/" - : location.pathname === item.to || location.pathname.startsWith(`${item.to}/`); - + + + + + + + {navModules + .filter((item) => !brand.hideModuleTestIds.includes(item.testId as never)) + .map((item) => { + const selected = isSelected(location.pathname, item.to); + const Icon = iconByTestId[item.testId] ?? ReceiptLongOutlinedIcon; return ( - navigate(item.to)} - > - - + + navigate(item.to)} + sx={{ + border: 0, + cursor: "pointer", + bgcolor: selected ? "rgba(47,128,237,0.12)" : "transparent", + color: selected ? "primary.main" : "text.secondary", + borderLeft: selected ? "3px solid" : "3px solid transparent", + borderColor: selected ? "primary.main" : "transparent", + borderRadius: 1, + py: 1, + px: 0.5, + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: 0.5, + position: "relative", + }} + > + + + + + + + {item.label} + + + ); })} - - - + + diff --git a/src/layout/nav.ts b/src/layout/nav.ts new file mode 100644 index 0000000..1e64be8 --- /dev/null +++ b/src/layout/nav.ts @@ -0,0 +1,33 @@ +import type { Path } from "@/router"; + +export type NavModule = { + label: string; + to: Path; + testId: string; + badge?: string; + secondaryBadge?: string; +}; + +export const navModules: NavModule[] = [ + { label: "Procurement", to: "/procurement", testId: "nav-procurement" }, + { + 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" }, + { label: "Recurring Invoices", to: "/recurring", testId: "nav-recurring" }, + { label: "My Past Invoices", to: "/past-invoices", testId: "nav-past-invoices" }, + { label: "Dashboards & Reports", to: "/reports", testId: "nav-reports" }, + { label: "System Settings", to: "/settings", testId: "nav-settings" }, + { label: "Billing", to: "/billing", testId: "nav-billing" }, +]; diff --git a/src/lib/brand.ts b/src/lib/brand.ts new file mode 100644 index 0000000..da1664a --- /dev/null +++ b/src/lib/brand.ts @@ -0,0 +1,29 @@ +/** + * Visual-parity brand config (AP-22 / AP-38). + * Default: Stampli-parity wordmark for AP-22 mocks. + * Set `VITE_SEA_HAVEN_BRAND=true` for Sea Haven cutover (AP-38). + */ +export const useSeaHavenBrand = import.meta.env.VITE_SEA_HAVEN_BRAND === "true"; + +export const brand = useSeaHavenBrand + ? { + productName: "Sea Haven AP", + wordmark: "SEA HAVEN", + orgLabel: "Sea Haven", + headerGradient: "linear-gradient(90deg, #0b3d5c 0%, #1f6f8b 100%)", + showBillyBanner: false, + hideModuleTestIds: [ + "nav-procurement", + "nav-billing", + "nav-recurring", + "nav-reports", + ] as const, + } + : { + productName: "STAMPLI", + wordmark: "STAMPLI", + orgLabel: "Seahaven", + headerGradient: "linear-gradient(90deg, #2167AE 0%, #7A2B8F 100%)", + showBillyBanner: true, + hideModuleTestIds: [] as const, + }; diff --git a/src/lib/mui-theme.ts b/src/lib/mui-theme.ts index 7a43b1a..ab28023 100644 --- a/src/lib/mui-theme.ts +++ b/src/lib/mui-theme.ts @@ -1,27 +1,73 @@ import { createTheme } from "@mui/material/styles"; +/** Stampli-parity tokens from AP-29 capture (original recreation, not scraped CSS). */ export const muiTheme = createTheme({ palette: { mode: "light", primary: { - main: "#0b3d5c", + main: "#2F80ED", + dark: "#2167AE", + light: "#5B9CF2", + contrastText: "#ffffff", }, secondary: { - main: "#1f6f8b", + main: "#7A2B8F", + }, + error: { + main: "#E74C3C", + }, + warning: { + main: "#F5A623", }, background: { - default: "#f5f7fa", + default: "#F4F6F9", paper: "#ffffff", }, + text: { + primary: "#1D2B50", + secondary: "#5A6A85", + }, + divider: "#E2E8F0", }, typography: { fontFamily: '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif', h1: { - fontFamily: '"IBM Plex Serif", Georgia, serif', + fontFamily: '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif', + fontWeight: 600, + fontSize: "1.5rem", + }, + button: { + textTransform: "none", fontWeight: 600, }, }, shape: { - borderRadius: 8, + borderRadius: 6, + }, + components: { + MuiCssBaseline: { + styleOverrides: { + body: { + backgroundColor: "#F4F6F9", + }, + }, + }, + MuiTab: { + styleOverrides: { + root: { + minHeight: 40, + textTransform: "none", + fontWeight: 600, + }, + }, + }, + MuiButton: { + styleOverrides: { + root: { + boxShadow: "none", + "&:hover": { boxShadow: "none" }, + }, + }, + }, }, }); diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index ad26712..d6688f8 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,7 +1,12 @@ -import { Outlet } from "react-router"; +import { Outlet, useLocation } from "react-router"; import { AppShell } from "@/layout/AppShell"; export default function App() { + const location = useLocation(); + if (location.pathname === "/login") { + return ; + } + return ( diff --git a/src/pages/billing/index.tsx b/src/pages/billing/index.tsx new file mode 100644 index 0000000..d4d16c8 --- /dev/null +++ b/src/pages/billing/index.tsx @@ -0,0 +1,12 @@ +import { ModuleStubPage } from "@/components/ModuleStubPage"; + +export default function BillingPage() { + return ( + + ); +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 38dd613..454b975 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,20 +1,15 @@ -import { Box, Container, Stack, Typography } from "@mui/material"; +import { Box, Typography } from "@mui/material"; import { DashboardGrids } from "@/dashboard/DashboardGrids"; export default function HomePage() { return ( - - - - - - Dashboard - - Internal accounts payable automation. - - - - + + + + Invoice Processing + + + ); } diff --git a/src/pages/login.tsx b/src/pages/login.tsx new file mode 100644 index 0000000..7c25cfd --- /dev/null +++ b/src/pages/login.tsx @@ -0,0 +1,49 @@ +import { Box, Button, Link, Paper, Stack, TextField, Typography } from "@mui/material"; +import { brand } from "@/lib/brand"; +import { useNavigate } from "@/router"; + +export default function LoginPage() { + const navigate = useNavigate(); + + return ( + + + + + {brand.wordmark} + + Sign in (mock auth — Cognito in AP-14) + + + + + + Forgot password? + + + + + ); +} diff --git a/src/pages/past-invoices/index.tsx b/src/pages/past-invoices/index.tsx new file mode 100644 index 0000000..8294dc4 --- /dev/null +++ b/src/pages/past-invoices/index.tsx @@ -0,0 +1,12 @@ +import { ModuleStubPage } from "@/components/ModuleStubPage"; + +export default function PastInvoicesPage() { + return ( + + ); +} diff --git a/src/pages/payments/index.tsx b/src/pages/payments/index.tsx new file mode 100644 index 0000000..c360766 --- /dev/null +++ b/src/pages/payments/index.tsx @@ -0,0 +1,15 @@ +import { Box, Typography } from "@mui/material"; +import { PaymentProcessingGrids } from "@/dashboard/PaymentProcessingGrids"; + +export default function PaymentsPage() { + return ( + + + + Payment Processing + + + + + ); +} diff --git a/src/pages/procurement/index.tsx b/src/pages/procurement/index.tsx new file mode 100644 index 0000000..d04c052 --- /dev/null +++ b/src/pages/procurement/index.tsx @@ -0,0 +1,12 @@ +import { ModuleStubPage } from "@/components/ModuleStubPage"; + +export default function ProcurementPage() { + return ( + + ); +} diff --git a/src/pages/recurring/index.tsx b/src/pages/recurring/index.tsx new file mode 100644 index 0000000..41ab074 --- /dev/null +++ b/src/pages/recurring/index.tsx @@ -0,0 +1,12 @@ +import { ModuleStubPage } from "@/components/ModuleStubPage"; + +export default function RecurringPage() { + return ( + + ); +} diff --git a/src/pages/reports/index.tsx b/src/pages/reports/index.tsx new file mode 100644 index 0000000..22113e0 --- /dev/null +++ b/src/pages/reports/index.tsx @@ -0,0 +1,12 @@ +import { ModuleStubPage } from "@/components/ModuleStubPage"; + +export default function ReportsPage() { + return ( + + ); +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index d7dbac9..1d76bec 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -2,6 +2,7 @@ interface ImportMetaEnv { readonly VITE_USE_MOCKS: string; + readonly VITE_SEA_HAVEN_BRAND?: string; } interface ImportMeta {