feat(web): align filter toolbar layout, field shapes, and tabs

Refs: AP-43
This commit is contained in:
Adam Moussa 2026-08-11 15:40:25 -04:00
parent 0ded294835
commit 36d9fcdc6c
No known key found for this signature in database
3 changed files with 209 additions and 95 deletions

View file

@ -10,6 +10,7 @@ import {
readyToPayColumns,
} from "@/dashboard/columns";
import { EMPTY_INVOICE_FILTERS, type InvoiceFilterState } from "@/dashboard/invoice-filters";
import { InvoiceTabLabel } from "@/dashboard/invoice-tabs";
import { countSelectedRows } from "@/dashboard/selection";
import { useInvoices, useUpdateInvoicePayDate } from "@/domain/invoices/use-invoices";
import { areMocksEnabled, type MockInvoice } from "@/mocks";
@ -24,6 +25,27 @@ const emptySelection: GridRowSelectionModel = {
const emptyFilters = EMPTY_INVOICE_FILTERS;
const invoiceTabsSx = {
minHeight: 44,
"& .MuiTab-root": {
bgcolor: "rgba(47,128,237,0.12)",
mr: 0.5,
borderTopLeftRadius: 6,
borderTopRightRadius: 6,
color: "text.primary",
textTransform: "none",
},
"& .Mui-selected": {
bgcolor: "#fff !important",
color: "text.primary !important",
},
"& .MuiTabs-indicator": {
height: 3,
borderRadius: 1,
backgroundColor: "#7B61FF",
},
} as const;
const rowClickSx = {
border: 1,
borderColor: "divider",
@ -134,47 +156,87 @@ export function DashboardGrids() {
}}
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" },
}}
sx={invoiceTabsSx}
>
<Tab
label={`For My Attention (${attentionRows.length})`}
label={
<InvoiceTabLabel
title="For My Attention"
count={attentionRows.length}
active={tab === "attention"}
/>
}
value="attention"
data-testid="tab-pending"
/>
<Tab label={`New Invoices (${invoices.length})`} value="all" data-testid="tab-all" />
<Tab
label={`Approved Invoices (${approvedRows.length})`}
label={
<InvoiceTabLabel
title="New Invoices"
count={invoices.length}
active={tab === "all"}
/>
}
value="all"
data-testid="tab-all"
/>
<Tab
label={
<InvoiceTabLabel
title="Approved Invoices"
count={approvedRows.length}
active={tab === "approved"}
/>
}
value="approved"
data-testid="tab-ready"
/>
<Tab
label={`Rejected Invoices (${rejectedRows.length})`}
label={
<InvoiceTabLabel
title="Rejected Invoices"
count={rejectedRows.length}
active={tab === "rejected"}
/>
}
value="rejected"
data-testid="tab-rejected"
/>
<Tab label={`Late Invoices (${lateRows.length})`} value="late" data-testid="tab-late" />
<Tab
label={
<InvoiceTabLabel
title="Late Invoices"
count={lateRows.length}
active={tab === "late"}
urgency
/>
}
value="late"
data-testid="tab-late"
/>
</Tabs>
</Box>
<Box sx={{ bgcolor: "background.paper", p: 2 }}>
{attentionRows.length > 0 && tab === "attention" ? (
<Alert severity="info" sx={{ mb: 2 }} data-testid="attention-banner">
{attentionRows.length} invoices require approval. Show Invoices ready to approve.
</Alert>
<Typography
component="button"
type="button"
data-testid="attention-banner"
sx={{
display: "block",
mb: 1.5,
p: 0,
border: 0,
background: "none",
cursor: "pointer",
color: "primary.main",
typography: "body2",
textAlign: "left",
}}
>
{attentionRows.length} invoices require approval. Show invoices ready to approve.
</Typography>
) : null}
<InvoiceFilterToolbar

View file

@ -23,6 +23,14 @@ type InvoiceFilterToolbarProps = {
trailingActions?: ReactElement | null;
};
const fieldSx = {
minWidth: 180,
"& .MuiOutlinedInput-root": {
borderRadius: "6px",
backgroundColor: "background.paper",
},
} as const;
export function InvoiceFilterToolbar({
filters,
onFiltersChange,
@ -33,80 +41,84 @@ export function InvoiceFilterToolbar({
const activeCount = countActiveFilters(filters);
return (
<Stack
direction="row"
spacing={1}
alignItems="center"
sx={{ mb: 2 }}
flexWrap="wrap"
useFlexGap
>
<Box data-testid="action-filter" sx={{ minWidth: 240 }}>
<Autocomplete
<Stack spacing={1.5} sx={{ mb: 2 }}>
<Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap>
<Box data-testid="action-filter" sx={{ ...fieldSx, minWidth: 220 }}>
<Autocomplete
size="small"
options={[...INVOICE_ACTION_OPTIONS]}
value={filters.action}
onChange={(_event, value) => onFiltersChange({ ...filters, action: value })}
renderInput={(params) => (
<TextField
{...params}
placeholder="Action"
label={undefined}
InputLabelProps={{ shrink: false }}
/>
)}
/>
</Box>
<TextField
size="small"
options={[...INVOICE_ACTION_OPTIONS]}
value={filters.action}
onChange={(_event, value) => onFiltersChange({ ...filters, action: value })}
renderInput={(params) => <TextField {...params} placeholder="Search" label="Action" />}
name="vendor"
id="vendor"
placeholder="Vendor"
value={filters.vendorQuery}
onChange={(event) => onFiltersChange({ ...filters, vendorQuery: event.target.value })}
sx={fieldSx}
slotProps={{
htmlInput: { "data-testid": "vendor-filter", "aria-label": "Vendor" },
input: {
endAdornment: filters.vendorQuery ? (
<InputAdornment position="end">
<Button
size="small"
onClick={() => onFiltersChange({ ...filters, vendorQuery: "" })}
aria-label="Clear vendor"
>
Clear
</Button>
</InputAdornment>
) : undefined,
},
}}
/>
</Box>
<TextField
size="small"
name="vendor"
id="vendor"
label="Vendor"
placeholder="Vendor"
value={filters.vendorQuery}
onChange={(event) => onFiltersChange({ ...filters, vendorQuery: event.target.value })}
sx={{ minWidth: 180 }}
slotProps={{
htmlInput: { "data-testid": "vendor-filter" },
input: {
endAdornment: filters.vendorQuery ? (
<InputAdornment position="end">
<Button
size="small"
onClick={() => onFiltersChange({ ...filters, vendorQuery: "" })}
aria-label="Clear vendor"
>
Clear
</Button>
</InputAdornment>
) : undefined,
},
}}
/>
<Box sx={{ flexGrow: 1 }} />
<IconButton
size="small"
aria-label="Filter actions"
data-testid="filter-actions-menu"
id="default-filter-actions-menu"
>
<MoreVertIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
aria-label="Open filters"
data-testid="filter-toggle"
onClick={onOpenFilterDrawer}
>
<Badge badgeContent={activeCount || undefined} color="primary">
<FilterListIcon fontSize="small" />
</Badge>
</IconButton>
<IconButton
size="small"
aria-label="Open columns"
data-testid="columns-toggle"
onClick={onOpenColumnsDrawer}
>
<ViewWeekOutlinedIcon fontSize="small" />
</IconButton>
<Button variant="contained" size="small" data-testid="review-all-items">
Review All Items
</Button>
{trailingActions}
<Box sx={{ flexGrow: 1 }} />
<IconButton
size="small"
aria-label="Filter actions"
data-testid="filter-actions-menu"
id="default-filter-actions-menu"
>
<MoreVertIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
aria-label="Open filters"
data-testid="filter-toggle"
onClick={onOpenFilterDrawer}
>
<Badge badgeContent={activeCount || undefined} color="primary">
<FilterListIcon fontSize="small" />
</Badge>
</IconButton>
<IconButton
size="small"
aria-label="Open columns"
data-testid="columns-toggle"
onClick={onOpenColumnsDrawer}
>
<ViewWeekOutlinedIcon fontSize="small" />
</IconButton>
</Stack>
<Stack direction="row" spacing={1} alignItems="center">
<Button variant="contained" size="small" data-testid="review-all-items">
Review All Items
</Button>
{trailingActions}
</Stack>
</Stack>
);
}

View file

@ -0,0 +1,40 @@
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import { Box, Stack } from "@mui/material";
import type { ReactElement } from "react";
export function InvoiceTabLabel({
title,
count,
active = false,
urgency = false,
}: {
title: string;
count: number;
active?: boolean;
urgency?: boolean;
}): ReactElement {
return (
<Stack direction="row" spacing={0.75} alignItems="center" component="span">
<Box component="span">{title}</Box>
{urgency ? <ErrorOutlineIcon sx={{ fontSize: 16, color: "error.main" }} aria-hidden /> : null}
<Box
component="span"
sx={{
minWidth: 22,
height: 22,
px: 0.75,
borderRadius: 999,
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
fontSize: 12,
fontWeight: 700,
bgcolor: active ? "#7B61FF" : urgency ? "error.main" : "primary.main",
color: "#fff",
}}
>
{count}
</Box>
</Stack>
);
}