mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-09-30 15:03:19 +00:00
feat(web): align filter toolbar layout, field shapes, and tabs
Refs: AP-43
This commit is contained in:
parent
0ded294835
commit
36d9fcdc6c
3 changed files with 209 additions and 95 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
40
src/dashboard/invoice-tabs.tsx
Normal file
40
src/dashboard/invoice-tabs.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue