mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-10-05 18:51:56 +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,
|
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
|
||||||
|
|
|
||||||
|
|
@ -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,80 +41,84 @@ 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"
|
<Autocomplete
|
||||||
sx={{ mb: 2 }}
|
size="small"
|
||||||
flexWrap="wrap"
|
options={[...INVOICE_ACTION_OPTIONS]}
|
||||||
useFlexGap
|
value={filters.action}
|
||||||
>
|
onChange={(_event, value) => onFiltersChange({ ...filters, action: value })}
|
||||||
<Box data-testid="action-filter" sx={{ minWidth: 240 }}>
|
renderInput={(params) => (
|
||||||
<Autocomplete
|
<TextField
|
||||||
|
{...params}
|
||||||
|
placeholder="Action"
|
||||||
|
label={undefined}
|
||||||
|
InputLabelProps={{ shrink: false }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
options={[...INVOICE_ACTION_OPTIONS]}
|
name="vendor"
|
||||||
value={filters.action}
|
id="vendor"
|
||||||
onChange={(_event, value) => onFiltersChange({ ...filters, action: value })}
|
placeholder="Vendor"
|
||||||
renderInput={(params) => <TextField {...params} placeholder="Search" label="Action" />}
|
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>
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
<TextField
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
name="vendor"
|
aria-label="Filter actions"
|
||||||
id="vendor"
|
data-testid="filter-actions-menu"
|
||||||
label="Vendor"
|
id="default-filter-actions-menu"
|
||||||
placeholder="Vendor"
|
>
|
||||||
value={filters.vendorQuery}
|
<MoreVertIcon fontSize="small" />
|
||||||
onChange={(event) => onFiltersChange({ ...filters, vendorQuery: event.target.value })}
|
</IconButton>
|
||||||
sx={{ minWidth: 180 }}
|
<IconButton
|
||||||
slotProps={{
|
size="small"
|
||||||
htmlInput: { "data-testid": "vendor-filter" },
|
aria-label="Open filters"
|
||||||
input: {
|
data-testid="filter-toggle"
|
||||||
endAdornment: filters.vendorQuery ? (
|
onClick={onOpenFilterDrawer}
|
||||||
<InputAdornment position="end">
|
>
|
||||||
<Button
|
<Badge badgeContent={activeCount || undefined} color="primary">
|
||||||
size="small"
|
<FilterListIcon fontSize="small" />
|
||||||
onClick={() => onFiltersChange({ ...filters, vendorQuery: "" })}
|
</Badge>
|
||||||
aria-label="Clear vendor"
|
</IconButton>
|
||||||
>
|
<IconButton
|
||||||
Clear
|
size="small"
|
||||||
</Button>
|
aria-label="Open columns"
|
||||||
</InputAdornment>
|
data-testid="columns-toggle"
|
||||||
) : undefined,
|
onClick={onOpenColumnsDrawer}
|
||||||
},
|
>
|
||||||
}}
|
<ViewWeekOutlinedIcon fontSize="small" />
|
||||||
/>
|
</IconButton>
|
||||||
<Box sx={{ flexGrow: 1 }} />
|
</Stack>
|
||||||
<IconButton
|
|
||||||
size="small"
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
aria-label="Filter actions"
|
<Button variant="contained" size="small" data-testid="review-all-items">
|
||||||
data-testid="filter-actions-menu"
|
Review All Items
|
||||||
id="default-filter-actions-menu"
|
</Button>
|
||||||
>
|
{trailingActions}
|
||||||
<MoreVertIcon fontSize="small" />
|
</Stack>
|
||||||
</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}
|
|
||||||
</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