From f10dddf6f284cf4e23561432f477d524647e7cee Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Fri, 25 Sep 2026 22:35:10 +0000 Subject: [PATCH] feat(web): add minimal Filter slide-over for invoice processing (AP-46) (#19) * feat(web): fix shell chrome, logo home, and live nav badges Refs: AP-42 * feat(web): add invoice grid lines and bold headers Refs: AP-44 * feat(web): support auto row height for multi-line invoice cells Refs: AP-44 * feat(web): expand mock invoice fields for Stampli column catalog Refs: AP-47 * feat(web): add Action and Vendor filters to invoice processing Refs: AP-43 * feat(web): add multi-line Stampli invoice cell renderers Refs: AP-47 * feat(web): align filter toolbar layout, field shapes, and tabs Refs: AP-43 * feat(web): add Columns slide-over with Stampli HTML defaults Refs: AP-45 * feat(web): add minimal Filter slide-over for invoice processing Refs: AP-46 * fix(web): address review feedback * fix(web): address review feedback * fix(web): address review feedback * ci: remove the pr policy caller * fix(web): keep mock catalog dates and updates in sync * fix(web): render the attention banner as text * fix(web): keep pay date editable on the approved grid * fix(web): make the attention banner non-interactive copy * fix(web): derive mock action labels from invoice status * fix(web): restore link-style attention banner copy * fix(ci): drop the unused catalog refresh index * fix(web): assert the vendor column by its grid field * fix(ci): format invoice mock helpers * fix(web): hide Payment Date when the drawer omits it * test(web): keep filters applied when the drawer closes * fix(web): count filtered rows on invoice tabs --- e2e/invoice-filter-drawer.spec.ts | 31 ++++++++++++++ package-lock.json | 53 +++++++++++++++++++++++ src/dashboard/DashboardGrids.tsx | 9 +++- src/dashboard/FilterDrawer.tsx | 71 +++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 e2e/invoice-filter-drawer.spec.ts create mode 100644 src/dashboard/FilterDrawer.tsx diff --git a/e2e/invoice-filter-drawer.spec.ts b/e2e/invoice-filter-drawer.spec.ts new file mode 100644 index 0000000..4fa4eab --- /dev/null +++ b/e2e/invoice-filter-drawer.spec.ts @@ -0,0 +1,31 @@ +import { expect, test } from "@playwright/test"; + +test.describe("Invoice Processing filter drawer (AP-46)", () => { + test("Filter drawer clears applied filters", async ({ page }) => { + await page.goto("/"); + await page.getByTestId("vendor-filter").fill("Acme"); + await page.getByTestId("filter-toggle").click(); + const drawer = page.getByTestId("filter-drawer"); + await expect(drawer).toBeVisible(); + await expect(page.getByTestId("filter-drawer-vendor")).toContainText("Acme"); + + await page.getByTestId("filter-drawer-clear").click(); + await expect(page.getByTestId("vendor-filter")).toHaveValue(""); + await expect(page.getByTestId("filter-toggle").locator(".MuiBadge-badge")).toBeHidden(); + }); + + test("closing the filter drawer leaves applied filters in place", async ({ page }) => { + await page.goto("/"); + await page.getByTestId("vendor-filter").fill("Acme"); + await page.getByTestId("filter-toggle").click(); + await expect(page.getByTestId("filter-drawer")).toBeVisible(); + await expect(page.getByTestId("filter-toggle").locator(".MuiBadge-badge")).toHaveText("1"); + + await page.getByRole("button", { name: "Close filters" }).evaluate((el: HTMLElement) => { + el.click(); + }); + await expect(page.getByTestId("filter-drawer")).toBeHidden(); + await expect(page.getByTestId("vendor-filter")).toHaveValue("Acme"); + await expect(page.getByTestId("filter-toggle").locator(".MuiBadge-badge")).toHaveText("1"); + }); +}); diff --git a/package-lock.json b/package-lock.json index 7fa5aaf..474ca5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -898,6 +898,59 @@ "react": ">=16.8.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@drizzle-team/brocli": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", diff --git a/src/dashboard/DashboardGrids.tsx b/src/dashboard/DashboardGrids.tsx index ab1f03d..7c93f7d 100644 --- a/src/dashboard/DashboardGrids.tsx +++ b/src/dashboard/DashboardGrids.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from "react"; import { Alert, Box, CircularProgress, Stack, Tab, Tabs, Typography } from "@mui/material"; import type { GridRowParams, GridRowSelectionModel } from "@mui/x-data-grid"; import { ColumnsDrawer } from "@/dashboard/ColumnsDrawer"; +import { FilterDrawer } from "@/dashboard/FilterDrawer"; import { InvoiceFilterToolbar } from "@/dashboard/InvoiceFilterToolbar"; import { CreatePayRunButton } from "@/dashboard/CreatePayRunButton"; import { InvoiceDataGrid } from "@/dashboard/InvoiceDataGrid"; @@ -76,7 +77,7 @@ export function DashboardGrids() { const [selection, setSelection] = useState(emptySelection); const [filters, setFilters] = useState(emptyFilters); const [columnsDrawerOpen, setColumnsDrawerOpen] = useState(false); - const [, setFilterDrawerOpen] = useState(false); + const [filterDrawerOpen, setFilterDrawerOpen] = useState(false); const [columnPrefs, setColumnPrefs] = useState(() => loadColumnPrefs()); const { data: invoices = [], isLoading, isError, error } = useInvoices(); const updatePayDate = useUpdateInvoicePayDate(); @@ -330,6 +331,12 @@ export function DashboardGrids() { setColumnPrefs(next); }} /> + setFilterDrawerOpen(false)} + filters={filters} + onFiltersChange={setFilters} + /> ); } diff --git a/src/dashboard/FilterDrawer.tsx b/src/dashboard/FilterDrawer.tsx new file mode 100644 index 0000000..470a880 --- /dev/null +++ b/src/dashboard/FilterDrawer.tsx @@ -0,0 +1,71 @@ +import CloseIcon from "@mui/icons-material/Close"; +import { Box, Button, Drawer, IconButton, Stack, Typography } from "@mui/material"; +import type { ReactElement } from "react"; +import { countActiveFilters, type InvoiceFilterState } from "@/dashboard/invoice-filters"; + +type FilterDrawerProps = { + open: boolean; + onClose: () => void; + filters: InvoiceFilterState; + onFiltersChange: (next: InvoiceFilterState) => void; +}; + +export function FilterDrawer({ + open, + onClose, + filters, + onFiltersChange, +}: FilterDrawerProps): ReactElement { + const activeCount = countActiveFilters(filters); + + return ( + + + Filters + + + + + + + {activeCount === 0 + ? "No filters applied." + : `${activeCount} active filter${activeCount === 1 ? "" : "s"}.`} + + + + + + Action + + + {filters.action ?? "—"} + + + + + Vendor + + + {filters.vendorQuery.trim() || "—"} + + + + + + + ); +}