feat(web): add minimal Filter slide-over for invoice processing

Refs: AP-46
This commit is contained in:
Adam Moussa 2026-08-11 15:40:28 -04:00
parent 5a02da2e96
commit 296de20220
No known key found for this signature in database
3 changed files with 95 additions and 1 deletions

View file

@ -0,0 +1,16 @@
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();
});
});

View file

@ -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();
@ -334,6 +335,12 @@ export function DashboardGrids() {
setColumnPrefs(next);
}}
/>
<FilterDrawer
open={filterDrawerOpen}
onClose={() => setFilterDrawerOpen(false)}
filters={filters}
onFiltersChange={setFilters}
/>
</Stack>
);
}

View 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"
PaperProps={{ sx: { width: 360, p: 2 } }}
>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ 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>
);
}