2026-08-10 17:32:43 -04:00
|
|
|
import { useMemo, useState } from "react";
|
|
|
|
|
import { Alert, Box, CircularProgress, Stack, Tab, Tabs, Typography } from "@mui/material";
|
2026-08-10 18:22:22 -04:00
|
|
|
import type { GridRowParams, GridRowSelectionModel } from "@mui/x-data-grid";
|
2026-08-10 17:32:43 -04:00
|
|
|
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";
|
2026-08-10 18:22:22 -04:00
|
|
|
import { useNavigate } from "@/router";
|
2026-08-10 17:32:43 -04:00
|
|
|
|
|
|
|
|
type DashboardTab = "pending" | "ready" | "all";
|
|
|
|
|
|
|
|
|
|
const emptySelection: GridRowSelectionModel = {
|
|
|
|
|
type: "include",
|
|
|
|
|
ids: new Set(),
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-10 18:22:22 -04:00
|
|
|
const rowClickSx = {
|
|
|
|
|
border: 1,
|
|
|
|
|
borderColor: "divider",
|
|
|
|
|
backgroundColor: "background.paper",
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
} as const;
|
|
|
|
|
|
2026-08-10 17:32:43 -04:00
|
|
|
export function DashboardGrids() {
|
2026-08-10 18:22:22 -04:00
|
|
|
const navigate = useNavigate();
|
2026-08-10 17:32:43 -04:00
|
|
|
const [tab, setTab] = useState<DashboardTab>("pending");
|
|
|
|
|
const [selection, setSelection] = useState<GridRowSelectionModel>(emptySelection);
|
|
|
|
|
const { data: invoices = [], isLoading, isError, error } = useInvoices();
|
|
|
|
|
const updatePayDate = useUpdateInvoicePayDate();
|
|
|
|
|
|
2026-08-10 18:22:22 -04:00
|
|
|
const openInvoice = (params: GridRowParams<MockInvoice>) => {
|
|
|
|
|
navigate("/invoices/:id", { params: { id: params.row.id } });
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-10 17:32:43 -04:00
|
|
|
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}
|
2026-08-10 18:22:22 -04:00
|
|
|
onRowClick={openInvoice}
|
|
|
|
|
sx={rowClickSx}
|
2026-08-10 17:32:43 -04:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{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);
|
|
|
|
|
}}
|
2026-08-10 18:22:22 -04:00
|
|
|
sx={rowClickSx}
|
2026-08-10 17:32:43 -04:00
|
|
|
/>
|
|
|
|
|
<Typography variant="caption" color="text.secondary">
|
2026-08-10 18:22:22 -04:00
|
|
|
Double-click Pay Date to edit (ISO YYYY-MM-DD). Changes update mock state only. Open
|
|
|
|
|
detail from the Invoice # link.
|
2026-08-10 17:32:43 -04:00
|
|
|
</Typography>
|
|
|
|
|
</Stack>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{tab === "all" && (
|
2026-08-10 18:22:22 -04:00
|
|
|
<InvoiceDataGrid
|
|
|
|
|
data-testid="grid-all"
|
|
|
|
|
rows={invoices}
|
|
|
|
|
columns={allInvoicesColumns}
|
|
|
|
|
onRowClick={openInvoice}
|
|
|
|
|
sx={rowClickSx}
|
|
|
|
|
/>
|
2026-08-10 17:32:43 -04:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Typography variant="body2" data-testid="mocks-status">
|
|
|
|
|
Mocks {areMocksEnabled() ? "enabled" : "disabled"} — {invoices.length} invoices
|
|
|
|
|
</Typography>
|
|
|
|
|
</Stack>
|
|
|
|
|
);
|
|
|
|
|
}
|