seahaven-ap/src/dashboard/DashboardGrids.tsx
renovate[bot] a2cbcfb7e8
fix(deps): update dependency @mui/material to v9 (#53)
* fix(deps): update dependency @mui/material to v9

* fix(deps): couple @mui/icons-material v9 and migrate system props

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-08-25 20:26:42 +00:00

234 lines
7 KiB
TypeScript

import { useMemo, useState } from "react";
import {
Alert,
Box,
Button,
Chip,
CircularProgress,
Stack,
Tab,
Tabs,
Typography,
} from "@mui/material";
import type { GridRowParams, GridRowSelectionModel } from "@mui/x-data-grid";
import { CreatePayRunButton } from "@/dashboard/CreatePayRunButton";
import { InvoiceDataGrid } from "@/dashboard/InvoiceDataGrid";
import {
allInvoicesColumns,
invoiceAttentionColumns,
readyToPayColumns,
} from "@/dashboard/columns";
import { countSelectedRows } from "@/dashboard/selection";
import { useInvoices, useUpdateInvoicePayDate } from "@/domain/invoices/use-invoices";
import { areMocksEnabled, type MockInvoice } from "@/mocks";
import { useNavigate } from "@/router";
type InvoiceTab = "attention" | "all" | "approved" | "rejected" | "late";
const emptySelection: GridRowSelectionModel = {
type: "include",
ids: new Set(),
};
const rowClickSx = {
border: 1,
borderColor: "divider",
backgroundColor: "background.paper",
cursor: "pointer",
borderRadius: 1,
} as const;
export function DashboardGrids() {
const navigate = useNavigate();
const [tab, setTab] = useState<InvoiceTab>("attention");
const [selection, setSelection] = useState<GridRowSelectionModel>(emptySelection);
const { data: invoices = [], isLoading, isError, error } = useInvoices();
const updatePayDate = useUpdateInvoicePayDate();
const openInvoice = (params: GridRowParams<MockInvoice>) => {
navigate("/invoices/:id", { params: { id: params.row.id } });
};
const attentionRows = useMemo(
() => invoices.filter((invoice) => invoice.status === "pending_approval"),
[invoices],
);
const approvedRows = useMemo(
() => invoices.filter((invoice) => invoice.status === "approved"),
[invoices],
);
const rejectedRows = useMemo(
() => invoices.filter((invoice) => invoice.status === "rejected"),
[invoices],
);
const lateRows = useMemo(() => {
const today = new Date().toISOString().slice(0, 10);
return invoices.filter(
(invoice) =>
invoice.dueDate < today && invoice.status !== "paid" && invoice.status !== "rejected",
);
}, [invoices]);
const selectedCount = countSelectedRows(
selection,
approvedRows.map((invoice) => invoice.id),
);
const processRowUpdate = async (newRow: MockInvoice, oldRow: MockInvoice) => {
if (newRow.payDate === oldRow.payDate) {
return oldRow;
}
const nextPayDate = newRow.payDate?.trim() ? newRow.payDate.trim() : null;
return updatePayDate.mutateAsync({ id: newRow.id, payDate: nextPayDate });
};
if (isLoading) {
return (
<Box sx={{ display: "flex", justifyContent: "center", py: 6 }}>
<CircularProgress aria-label="Loading invoices" />
</Box>
);
}
if (isError) {
return (
<Alert severity="error">
{error instanceof Error ? error.message : "Failed to load invoices"}
</Alert>
);
}
return (
<Stack spacing={0} data-testid="invoice-processing">
<Box sx={{ bgcolor: "#E8EEF7", px: 1, pt: 1 }}>
<Tabs
value={tab}
onChange={(_event, value: InvoiceTab) => {
setTab(value);
setSelection(emptySelection);
}}
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" },
}}
>
<Tab
label={`For My Attention (${attentionRows.length})`}
value="attention"
data-testid="tab-pending"
/>
<Tab label={`New Invoices (${invoices.length})`} value="all" data-testid="tab-all" />
<Tab
label={`Approved Invoices (${approvedRows.length})`}
value="approved"
data-testid="tab-ready"
/>
<Tab
label={`Rejected Invoices (${rejectedRows.length})`}
value="rejected"
data-testid="tab-rejected"
/>
<Tab label={`Late Invoices (${lateRows.length})`} 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>
) : null}
<Stack
direction="row"
spacing={1}
sx={{
alignItems: "center",
mb: 2,
}}
>
<Chip label="Action" variant="outlined" size="small" />
<Chip label="Vendor" variant="outlined" size="small" />
<Box sx={{ flexGrow: 1 }} />
<Button variant="contained" size="small" data-testid="review-all-items">
Review All Items
</Button>
{tab === "approved" ? <CreatePayRunButton selectedCount={selectedCount} /> : null}
</Stack>
{tab === "attention" && (
<InvoiceDataGrid
data-testid="grid-pending"
rows={attentionRows}
columns={invoiceAttentionColumns}
onRowClick={openInvoice}
sx={rowClickSx}
/>
)}
{tab === "all" && (
<InvoiceDataGrid
data-testid="grid-all"
rows={invoices}
columns={allInvoicesColumns}
onRowClick={openInvoice}
sx={rowClickSx}
/>
)}
{tab === "approved" && (
<InvoiceDataGrid
data-testid="grid-ready"
rows={approvedRows}
columns={readyToPayColumns}
checkboxSelection
rowSelectionModel={selection}
onRowSelectionModelChange={(model) => setSelection(model)}
processRowUpdate={processRowUpdate}
onProcessRowUpdateError={(processError) => {
console.error(processError);
}}
sx={rowClickSx}
/>
)}
{tab === "rejected" && (
<InvoiceDataGrid
data-testid="grid-rejected"
rows={rejectedRows}
columns={allInvoicesColumns}
onRowClick={openInvoice}
sx={rowClickSx}
/>
)}
{tab === "late" && (
<InvoiceDataGrid
data-testid="grid-late"
rows={lateRows}
columns={allInvoicesColumns}
onRowClick={openInvoice}
sx={rowClickSx}
/>
)}
<Typography variant="body2" data-testid="mocks-status" sx={{ mt: 2 }}>
Mocks {areMocksEnabled() ? "enabled" : "disabled"} — {invoices.length} invoices
</Typography>
</Box>
</Stack>
);
}