seahaven-ap/src/dashboard/DashboardGrids.tsx

146 lines
4.6 KiB
TypeScript
Raw Normal View History

import { useMemo, useState } from "react";
import { Alert, Box, 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, pendingApprovalColumns, 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 DashboardTab = "pending" | "ready" | "all";
const emptySelection: GridRowSelectionModel = {
type: "include",
ids: new Set(),
};
const rowClickSx = {
border: 1,
borderColor: "divider",
backgroundColor: "background.paper",
cursor: "pointer",
} as const;
export function DashboardGrids() {
const navigate = useNavigate();
const [tab, setTab] = useState<DashboardTab>("pending");
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 pendingRows = useMemo(
() => invoices.filter((invoice) => invoice.status === "pending_approval"),
[invoices],
);
const readyRows = useMemo(
() => invoices.filter((invoice) => invoice.status === "approved"),
[invoices],
);
const selectedCount = countSelectedRows(
selection,
readyRows.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={2}>
<Tabs
value={tab}
onChange={(_event, value: DashboardTab) => {
setTab(value);
setSelection(emptySelection);
}}
aria-label="Invoice dashboard tabs"
>
<Tab
label={`Pending Approval (${pendingRows.length})`}
value="pending"
data-testid="tab-pending"
/>
<Tab label={`Ready to Pay (${readyRows.length})`} value="ready" data-testid="tab-ready" />
<Tab label={`All Invoices (${invoices.length})`} value="all" data-testid="tab-all" />
</Tabs>
{tab === "pending" && (
<InvoiceDataGrid
data-testid="grid-pending"
rows={pendingRows}
columns={pendingApprovalColumns}
onRowClick={openInvoice}
sx={rowClickSx}
/>
)}
{tab === "ready" && (
<Stack spacing={1.5}>
<Box sx={{ display: "flex", justifyContent: "flex-end" }}>
<CreatePayRunButton selectedCount={selectedCount} />
</Box>
<InvoiceDataGrid
data-testid="grid-ready"
rows={readyRows}
columns={readyToPayColumns}
checkboxSelection
rowSelectionModel={selection}
onRowSelectionModelChange={(model) => setSelection(model)}
processRowUpdate={processRowUpdate}
onProcessRowUpdateError={(processError) => {
console.error(processError);
}}
sx={rowClickSx}
/>
<Typography variant="caption" color="text.secondary">
Double-click Pay Date to edit (ISO YYYY-MM-DD). Changes update mock state only. Open
detail from the Invoice # link.
</Typography>
</Stack>
)}
{tab === "all" && (
<InvoiceDataGrid
data-testid="grid-all"
rows={invoices}
columns={allInvoicesColumns}
onRowClick={openInvoice}
sx={rowClickSx}
/>
)}
<Typography variant="body2" data-testid="mocks-status">
Mocks {areMocksEnabled() ? "enabled" : "disabled"} — {invoices.length} invoices
</Typography>
</Stack>
);
}