mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-09-30 02:13:16 +00:00
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
This commit is contained in:
parent
ee2f7a9902
commit
f10dddf6f2
4 changed files with 163 additions and 1 deletions
31
e2e/invoice-filter-drawer.spec.ts
Normal file
31
e2e/invoice-filter-drawer.spec.ts
Normal file
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
53
package-lock.json
generated
53
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<GridRowSelectionModel>(emptySelection);
|
||||
const [filters, setFilters] = useState<InvoiceFilterState>(emptyFilters);
|
||||
const [columnsDrawerOpen, setColumnsDrawerOpen] = useState(false);
|
||||
const [, setFilterDrawerOpen] = useState(false);
|
||||
const [filterDrawerOpen, setFilterDrawerOpen] = useState(false);
|
||||
const [columnPrefs, setColumnPrefs] = useState<ColumnPrefs>(() => loadColumnPrefs());
|
||||
const { data: invoices = [], isLoading, isError, error } = useInvoices();
|
||||
const updatePayDate = useUpdateInvoicePayDate();
|
||||
|
|
@ -330,6 +331,12 @@ export function DashboardGrids() {
|
|||
setColumnPrefs(next);
|
||||
}}
|
||||
/>
|
||||
<FilterDrawer
|
||||
open={filterDrawerOpen}
|
||||
onClose={() => setFilterDrawerOpen(false)}
|
||||
filters={filters}
|
||||
onFiltersChange={setFilters}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
71
src/dashboard/FilterDrawer.tsx
Normal file
71
src/dashboard/FilterDrawer.tsx
Normal file
|
|
@ -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 (
|
||||
<Drawer
|
||||
anchor="right"
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
data-testid="filter-drawer"
|
||||
slotProps={{ paper: { sx: { width: 360, p: 2 } } }}
|
||||
>
|
||||
<Stack direction="row" sx={{ alignItems: "center", justifyContent: "space-between", mb: 2 }}>
|
||||
<Typography variant="h6">Filters</Typography>
|
||||
<IconButton aria-label="Close filters" onClick={onClose} size="small">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{activeCount === 0
|
||||
? "No filters applied."
|
||||
: `${activeCount} active filter${activeCount === 1 ? "" : "s"}.`}
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={1.5} sx={{ mb: 3 }}>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Action
|
||||
</Typography>
|
||||
<Typography variant="body2" data-testid="filter-drawer-action">
|
||||
{filters.action ?? "—"}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Vendor
|
||||
</Typography>
|
||||
<Typography variant="body2" data-testid="filter-drawer-vendor">
|
||||
{filters.vendorQuery.trim() || "—"}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
data-testid="filter-drawer-clear"
|
||||
disabled={activeCount === 0}
|
||||
onClick={() => onFiltersChange({ action: null, vendorQuery: "" })}
|
||||
>
|
||||
Clear all
|
||||
</Button>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue