mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-09-30 03:23:16 +00:00
* feat(shared): add payment ladder and CSV domain package Introduce @seahaven-ap/shared via npm workspaces so FE and future API share payments-dashboard-compatible status ladders, pay-date helpers, and CSV headers. * fix(web): build shared package before vite dev Ensure fresh npm ci && npm run dev (and Playwright webServer) can resolve @seahaven-ap/shared dist exports that are gitignored.
238 lines
7.6 KiB
TypeScript
238 lines
7.6 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 { isReadyToPay } from "@seahaven-ap/shared";
|
|
import { CreatePayRunButton } from "@/dashboard/CreatePayRunButton";
|
|
import { InvoiceDataGrid } from "@/dashboard/InvoiceDataGrid";
|
|
import { pendingApprovalColumns, readyToPayColumns } from "@/dashboard/columns";
|
|
import { countSelectedRows } from "@/dashboard/selection";
|
|
import { useInvoices, useUpdateInvoicePayDate } from "@/domain/invoices/use-invoices";
|
|
import type { MockInvoice } from "@/mocks";
|
|
import { useNavigate } from "@/router";
|
|
|
|
type PaymentTab = "outside" | "select" | "pending" | "scheduled" | "errors" | "outbox";
|
|
|
|
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 PaymentProcessingGrids() {
|
|
const navigate = useNavigate();
|
|
const [tab, setTab] = useState<PaymentTab>("select");
|
|
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 selectRows = useMemo(
|
|
() => invoices.filter((invoice) => isReadyToPay(invoice.status)),
|
|
[invoices],
|
|
);
|
|
const pendingRows = useMemo(
|
|
() => invoices.filter((invoice) => invoice.status === "pending_approval"),
|
|
[invoices],
|
|
);
|
|
const scheduledRows = useMemo(
|
|
() => invoices.filter((invoice) => invoice.status === "scheduled"),
|
|
[invoices],
|
|
);
|
|
|
|
const selectedCount = countSelectedRows(
|
|
selection,
|
|
selectRows.map((invoice) => invoice.id),
|
|
);
|
|
|
|
const totalDue = selectRows.reduce((sum, row) => sum + row.amountDue, 0);
|
|
|
|
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 payments" />
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
if (isError) {
|
|
return (
|
|
<Alert severity="error">
|
|
{error instanceof Error ? error.message : "Failed to load payments"}
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Stack spacing={0} data-testid="payment-processing">
|
|
<Box sx={{ bgcolor: "#E8EEF7", px: 1, pt: 1 }}>
|
|
<Tabs
|
|
value={tab}
|
|
onChange={(_event, value: PaymentTab) => {
|
|
setTab(value);
|
|
setSelection(emptySelection);
|
|
}}
|
|
variant="scrollable"
|
|
aria-label="Payment Processing tabs"
|
|
sx={{
|
|
minHeight: 44,
|
|
"& .MuiTab-root": {
|
|
bgcolor: "rgba(47,128,237,0.18)",
|
|
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="Paying Outside Stampli (0)" value="outside" data-testid="tab-pay-outside" />
|
|
<Tab
|
|
label={`Select to Pay (${selectRows.length})`}
|
|
value="select"
|
|
data-testid="tab-select-to-pay"
|
|
/>
|
|
<Tab
|
|
label={`Pending Approval (${pendingRows.length})`}
|
|
value="pending"
|
|
data-testid="tab-pay-pending"
|
|
/>
|
|
<Tab
|
|
label={`Scheduled (${scheduledRows.length})`}
|
|
value="scheduled"
|
|
data-testid="tab-scheduled"
|
|
/>
|
|
<Tab label="Payment Export Errors (0)" value="errors" data-testid="tab-export-errors" />
|
|
<Tab label="Outbox (0)" value="outbox" data-testid="tab-outbox" />
|
|
</Tabs>
|
|
</Box>
|
|
|
|
<Box sx={{ bgcolor: "background.paper", p: 2 }}>
|
|
<Alert severity="warning" sx={{ mb: 2 }} data-testid="expedite-banner">
|
|
Expedited payments are currently unavailable for you due to various factors. (parity stub)
|
|
</Alert>
|
|
|
|
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 2 }} flexWrap="wrap">
|
|
<Chip label="Vendor Name" variant="outlined" size="small" />
|
|
<Chip label="Payment Method" variant="outlined" size="small" />
|
|
<Chip label="Bank Account" variant="outlined" size="small" />
|
|
<Chip label="Status" variant="outlined" size="small" />
|
|
<Box sx={{ flexGrow: 1 }} />
|
|
<Button variant="outlined" size="small" data-testid="bulk-edit">
|
|
Bulk Edit
|
|
</Button>
|
|
<CreatePayRunButton selectedCount={selectedCount} />
|
|
</Stack>
|
|
|
|
{tab === "select" && (
|
|
<InvoiceDataGrid
|
|
data-testid="grid-select-to-pay"
|
|
rows={selectRows}
|
|
columns={readyToPayColumns}
|
|
checkboxSelection
|
|
rowSelectionModel={selection}
|
|
onRowSelectionModelChange={(model) => setSelection(model)}
|
|
processRowUpdate={processRowUpdate}
|
|
onProcessRowUpdateError={(processError) => {
|
|
console.error(processError);
|
|
}}
|
|
onRowClick={openInvoice}
|
|
sx={rowClickSx}
|
|
/>
|
|
)}
|
|
|
|
{tab === "pending" && (
|
|
<InvoiceDataGrid
|
|
data-testid="grid-pay-pending"
|
|
rows={pendingRows}
|
|
columns={pendingApprovalColumns}
|
|
onRowClick={openInvoice}
|
|
sx={rowClickSx}
|
|
/>
|
|
)}
|
|
|
|
{tab === "scheduled" && (
|
|
<InvoiceDataGrid
|
|
data-testid="grid-scheduled"
|
|
rows={scheduledRows}
|
|
columns={readyToPayColumns}
|
|
onRowClick={openInvoice}
|
|
sx={rowClickSx}
|
|
/>
|
|
)}
|
|
|
|
{(tab === "outside" || tab === "errors" || tab === "outbox") && (
|
|
<Box
|
|
sx={{
|
|
py: 8,
|
|
textAlign: "center",
|
|
border: 1,
|
|
borderColor: "divider",
|
|
borderRadius: 1,
|
|
}}
|
|
data-testid="payment-empty-stub"
|
|
>
|
|
<Typography color="text.secondary">No Result Found</Typography>
|
|
<Typography variant="caption" color="text.secondary">
|
|
parity stub
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
|
|
<Stack direction="row" spacing={4} sx={{ mt: 2 }} data-testid="payment-footer-totals">
|
|
<Typography variant="body2">
|
|
<Box component="span" color="text.secondary">
|
|
Total Due:{" "}
|
|
</Box>
|
|
<strong>
|
|
{totalDue.toLocaleString("en-US", { style: "currency", currency: "USD" })}
|
|
</strong>
|
|
</Typography>
|
|
<Typography variant="body2">
|
|
<Box component="span" color="text.secondary">
|
|
Total To Pay:{" "}
|
|
</Box>
|
|
<strong>$0.00</strong>
|
|
</Typography>
|
|
<Typography variant="body2">
|
|
<Box component="span" color="text.secondary">
|
|
Estimated USD:{" "}
|
|
</Box>
|
|
<strong>$0.00</strong>
|
|
</Typography>
|
|
</Stack>
|
|
</Box>
|
|
</Stack>
|
|
);
|
|
}
|