mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-09-30 06:53:18 +00:00
* 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>
234 lines
7 KiB
TypeScript
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>
|
|
);
|
|
}
|