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, readyToPayColumns,
} from "@/dashboard/columns"; } from "@/dashboard/columns";
import { EMPTY_INVOICE_FILTERS, type InvoiceFilterState } from "@/dashboard/invoice-filters"; import { EMPTY_INVOICE_FILTERS, type InvoiceFilterState } from "@/dashboard/invoice-filters";
import { InvoiceTabLabel } from "@/dashboard/invoice-tabs";
import { countSelectedRows } from "@/dashboard/selection"; import { countSelectedRows } from "@/dashboard/selection";
import { useInvoices, useUpdateInvoicePayDate } from "@/domain/invoices/use-invoices"; import { useInvoices, useUpdateInvoicePayDate } from "@/domain/invoices/use-invoices";
import { areMocksEnabled, type MockInvoice } from "@/mocks"; import { areMocksEnabled, type MockInvoice } from "@/mocks";
@ -24,6 +25,27 @@ const emptySelection: GridRowSelectionModel = {
const emptyFilters = EMPTY_INVOICE_FILTERS; 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 = { const rowClickSx = {
border: 1, border: 1,
borderColor: "divider", borderColor: "divider",
@ -134,47 +156,87 @@ export function DashboardGrids() {
}} }}
variant="scrollable" variant="scrollable"
aria-label="Invoice Processing tabs" aria-label="Invoice Processing tabs"
sx={{ sx={invoiceTabsSx}
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 <Tab
label={`For My Attention (${attentionRows.length})`} label={
<InvoiceTabLabel
title="For My Attention"
count={attentionRows.length}
active={tab === "attention"}
/>
}
value="attention" value="attention"
data-testid="tab-pending" data-testid="tab-pending"
/> />
<Tab label={`New Invoices (${invoices.length})`} value="all" data-testid="tab-all" />
<Tab <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" value="approved"
data-testid="tab-ready" data-testid="tab-ready"
/> />
<Tab <Tab
label={`Rejected Invoices (${rejectedRows.length})`} label={
<InvoiceTabLabel
title="Rejected Invoices"
count={rejectedRows.length}
active={tab === "rejected"}
/>
}
value="rejected" value="rejected"
data-testid="tab-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> </Tabs>
</Box> </Box>
<Box sx={{ bgcolor: "background.paper", p: 2 }}> <Box sx={{ bgcolor: "background.paper", p: 2 }}>
{attentionRows.length > 0 && tab === "attention" ? ( {attentionRows.length > 0 && tab === "attention" ? (
<Alert severity="info" sx={{ mb: 2 }} data-testid="attention-banner"> <Typography
{attentionRows.length} invoices require approval. Show Invoices ready to approve. component="button"
</Alert> 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} ) : null}
<InvoiceFilterToolbar <InvoiceFilterToolbar

View file

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