mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-10-07 12:48:53 +00:00
Compare commits
20 commits
ee425dfb45
...
56bfc5f84f
| Author | SHA1 | Date | |
|---|---|---|---|
| 56bfc5f84f | |||
| 1ab723f68d | |||
| cf86cfed30 | |||
| 668c0076ba | |||
| 2c40943908 | |||
| 83529e6354 | |||
| aba37782f3 | |||
| 84ffdb8ffe | |||
| 3179ef848b | |||
| 923c979b4f | |||
| 1faee81fbb | |||
| f076b91aff | |||
| ec7a18c495 | |||
| f76c3f7d63 | |||
| 98e2fadea1 | |||
| f80e50d2d8 | |||
| 291f46ce7e | |||
| b575c886b2 | |||
| 5838c6b1ab | |||
| 070730e4ee |
9 changed files with 222 additions and 89 deletions
|
|
@ -53,6 +53,19 @@ test.describe("dashboard grids", () => {
|
||||||
/Pay run stub: [2-9]\d* invoice\(s\) selected/,
|
/Pay run stub: [2-9]\d* invoice\(s\) selected/,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await page.getByTestId("columns-toggle").click();
|
||||||
|
const drawer = page.getByTestId("columns-drawer");
|
||||||
|
await drawer.getByTestId("columns-hidden-search").fill("Payment Date");
|
||||||
|
await drawer
|
||||||
|
.getByTestId("columns-hidden-paymentDate")
|
||||||
|
.locator('input[type="checkbox"]')
|
||||||
|
.evaluate((el: HTMLInputElement) => {
|
||||||
|
el.click();
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
readyGrid.locator('.MuiDataGrid-columnHeader[data-field="payDate"]'),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
const payDateCell = readyGrid.locator('[data-field="payDate"]').nth(1);
|
const payDateCell = readyGrid.locator('[data-field="payDate"]').nth(1);
|
||||||
await payDateCell.dblclick();
|
await payDateCell.dblclick();
|
||||||
const editor = readyGrid
|
const editor = readyGrid
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,22 @@ test.describe("Invoice Processing columns drawer (AP-45)", () => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await page.getByTestId("columns-toggle").click();
|
await page.getByTestId("columns-toggle").click();
|
||||||
const drawer = page.getByTestId("columns-drawer");
|
const drawer = page.getByTestId("columns-drawer");
|
||||||
const vendorBox = drawer.getByTestId("columns-visible-vendor").locator('input[type="checkbox"]');
|
const vendorBox = drawer
|
||||||
|
.getByTestId("columns-visible-vendor")
|
||||||
|
.locator('input[type="checkbox"]');
|
||||||
await expect(vendorBox).toBeDisabled();
|
await expect(vendorBox).toBeDisabled();
|
||||||
await vendorBox.evaluate((el: HTMLInputElement) => {
|
await vendorBox.evaluate((el: HTMLInputElement) => {
|
||||||
el.disabled = false;
|
el.disabled = false;
|
||||||
el.click();
|
el.click();
|
||||||
});
|
});
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("grid-pending").locator('.MuiDataGrid-columnHeader[data-field="vendor"]'),
|
page
|
||||||
|
.getByTestId("grid-pending")
|
||||||
|
.locator('.MuiDataGrid-columnHeader[data-field="vendorName"]'),
|
||||||
|
).toBeVisible();
|
||||||
|
await page.getByTestId("tab-ready").click();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("grid-ready").locator('.MuiDataGrid-columnHeader[data-field="vendorName"]'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
const search = drawer.getByTestId("columns-hidden-search");
|
const search = drawer.getByTestId("columns-hidden-search");
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ test.describe("Invoice Processing filters (AP-43)", () => {
|
||||||
await page.getByTestId("vendor-filter").fill("Harbor");
|
await page.getByTestId("vendor-filter").fill("Harbor");
|
||||||
await expect(page.getByTestId("filter-toggle").locator(".MuiBadge-badge")).toHaveText("2");
|
await expect(page.getByTestId("filter-toggle").locator(".MuiBadge-badge")).toHaveText("2");
|
||||||
await expect(grid.locator(".MuiDataGrid-row")).toHaveCount(1);
|
await expect(grid.locator(".MuiDataGrid-row")).toHaveCount(1);
|
||||||
await expect(page.getByTestId("attention-banner")).toContainText("1 invoices require approval");
|
await expect(page.getByTestId("tab-pending").locator("span").last()).toHaveText("1");
|
||||||
|
await expect(page.getByTestId("attention-banner")).toContainText(
|
||||||
|
"1 invoices require approval. Show invoices ready to approve.",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { ColumnsDrawer } from "@/dashboard/ColumnsDrawer";
|
||||||
import { InvoiceFilterToolbar } from "@/dashboard/InvoiceFilterToolbar";
|
import { InvoiceFilterToolbar } from "@/dashboard/InvoiceFilterToolbar";
|
||||||
import { CreatePayRunButton } from "@/dashboard/CreatePayRunButton";
|
import { CreatePayRunButton } from "@/dashboard/CreatePayRunButton";
|
||||||
import { InvoiceDataGrid } from "@/dashboard/InvoiceDataGrid";
|
import { InvoiceDataGrid } from "@/dashboard/InvoiceDataGrid";
|
||||||
import { columnsFromCatalogKeys } from "@/dashboard/columns";
|
import { columnsForApprovedGrid, columnsFromCatalogKeys } from "@/dashboard/columns";
|
||||||
import {
|
import {
|
||||||
loadColumnPrefs,
|
loadColumnPrefs,
|
||||||
resetColumnPrefs,
|
resetColumnPrefs,
|
||||||
|
|
@ -85,6 +85,10 @@ export function DashboardGrids() {
|
||||||
() => columnsFromCatalogKeys(columnPrefs.visibleOrder),
|
() => columnsFromCatalogKeys(columnPrefs.visibleOrder),
|
||||||
[columnPrefs.visibleOrder],
|
[columnPrefs.visibleOrder],
|
||||||
);
|
);
|
||||||
|
const approvedColumns = useMemo(
|
||||||
|
() => columnsForApprovedGrid(columnPrefs.visibleOrder),
|
||||||
|
[columnPrefs.visibleOrder],
|
||||||
|
);
|
||||||
|
|
||||||
const openInvoice = (params: GridRowParams<MockInvoice>) => {
|
const openInvoice = (params: GridRowParams<MockInvoice>) => {
|
||||||
navigate("/invoices/:id", { params: { id: params.row.id } });
|
navigate("/invoices/:id", { params: { id: params.row.id } });
|
||||||
|
|
@ -176,7 +180,7 @@ export function DashboardGrids() {
|
||||||
label={
|
label={
|
||||||
<InvoiceTabLabel
|
<InvoiceTabLabel
|
||||||
title="For My Attention"
|
title="For My Attention"
|
||||||
count={attentionRows.length}
|
count={filteredAttention.length}
|
||||||
active={tab === "attention"}
|
active={tab === "attention"}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +191,7 @@ export function DashboardGrids() {
|
||||||
label={
|
label={
|
||||||
<InvoiceTabLabel
|
<InvoiceTabLabel
|
||||||
title="New Invoices"
|
title="New Invoices"
|
||||||
count={invoices.length}
|
count={filteredAll.length}
|
||||||
active={tab === "all"}
|
active={tab === "all"}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
@ -198,7 +202,7 @@ export function DashboardGrids() {
|
||||||
label={
|
label={
|
||||||
<InvoiceTabLabel
|
<InvoiceTabLabel
|
||||||
title="Approved Invoices"
|
title="Approved Invoices"
|
||||||
count={approvedRows.length}
|
count={filteredApproved.length}
|
||||||
active={tab === "approved"}
|
active={tab === "approved"}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +213,7 @@ export function DashboardGrids() {
|
||||||
label={
|
label={
|
||||||
<InvoiceTabLabel
|
<InvoiceTabLabel
|
||||||
title="Rejected Invoices"
|
title="Rejected Invoices"
|
||||||
count={rejectedRows.length}
|
count={filteredRejected.length}
|
||||||
active={tab === "rejected"}
|
active={tab === "rejected"}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +224,7 @@ export function DashboardGrids() {
|
||||||
label={
|
label={
|
||||||
<InvoiceTabLabel
|
<InvoiceTabLabel
|
||||||
title="Late Invoices"
|
title="Late Invoices"
|
||||||
count={lateRows.length}
|
count={filteredLate.length}
|
||||||
active={tab === "late"}
|
active={tab === "late"}
|
||||||
urgency
|
urgency
|
||||||
/>
|
/>
|
||||||
|
|
@ -234,22 +238,14 @@ export function DashboardGrids() {
|
||||||
<Box sx={{ bgcolor: "background.paper", p: 2 }}>
|
<Box sx={{ bgcolor: "background.paper", p: 2 }}>
|
||||||
{filteredAttention.length > 0 && tab === "attention" ? (
|
{filteredAttention.length > 0 && tab === "attention" ? (
|
||||||
<Typography
|
<Typography
|
||||||
component="button"
|
|
||||||
type="button"
|
|
||||||
data-testid="attention-banner"
|
data-testid="attention-banner"
|
||||||
sx={{
|
variant="body2"
|
||||||
display: "block",
|
sx={{ display: "block", mb: 1.5, color: "primary.main", textAlign: "left" }}
|
||||||
mb: 1.5,
|
|
||||||
p: 0,
|
|
||||||
border: 0,
|
|
||||||
background: "none",
|
|
||||||
cursor: "pointer",
|
|
||||||
color: "primary.main",
|
|
||||||
typography: "body2",
|
|
||||||
textAlign: "left",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{filteredAttention.length} invoices require approval. Show invoices ready to approve.
|
{filteredAttention.length} invoices require approval.{" "}
|
||||||
|
<Box component="span" sx={{ textDecoration: "underline" }}>
|
||||||
|
Show invoices ready to approve.
|
||||||
|
</Box>
|
||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
@ -287,7 +283,7 @@ export function DashboardGrids() {
|
||||||
<InvoiceDataGrid
|
<InvoiceDataGrid
|
||||||
data-testid="grid-ready"
|
data-testid="grid-ready"
|
||||||
rows={filteredApproved}
|
rows={filteredApproved}
|
||||||
columns={catalogColumns}
|
columns={approvedColumns}
|
||||||
checkboxSelection
|
checkboxSelection
|
||||||
rowSelectionModel={selection}
|
rowSelectionModel={selection}
|
||||||
onRowSelectionModelChange={(model) => setSelection(model)}
|
onRowSelectionModelChange={(model) => setSelection(model)}
|
||||||
|
|
|
||||||
9
src/dashboard/approved-columns.ts
Normal file
9
src/dashboard/approved-columns.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import type { GridColDef } from "@mui/x-data-grid";
|
||||||
|
import type { MockInvoice } from "@/mocks";
|
||||||
|
|
||||||
|
/** Mark a visible pay-date column editable. A hidden Payment Date stays omitted. */
|
||||||
|
export function withEditablePayDate(columns: GridColDef<MockInvoice>[]): GridColDef<MockInvoice>[] {
|
||||||
|
return columns.map((column) =>
|
||||||
|
column.field === "payDate" ? { ...column, editable: true } : column,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
import type { GridColDef } from "@mui/x-data-grid";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { withEditablePayDate } from "@/dashboard/approved-columns";
|
||||||
|
import type { MockInvoice } from "@/mocks";
|
||||||
import { formatRelativeFromIsoDate, formatSlashDate } from "@/dashboard/invoice-date-format";
|
import { formatRelativeFromIsoDate, formatSlashDate } from "@/dashboard/invoice-date-format";
|
||||||
|
|
||||||
describe("invoice column date helpers (AP-47)", () => {
|
describe("invoice column date helpers (AP-47)", () => {
|
||||||
|
|
@ -26,3 +29,20 @@ describe("invoice column date helpers (AP-47)", () => {
|
||||||
expect(formatRelativeFromIsoDate("2026-10-11", now)).toBe("in 2 months");
|
expect(formatRelativeFromIsoDate("2026-10-11", now)).toBe("in 2 months");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("withEditablePayDate", () => {
|
||||||
|
const hidden: GridColDef<MockInvoice> = { field: "vendorName", headerName: "Vendor" };
|
||||||
|
const payDate: GridColDef<MockInvoice> = { field: "payDate", headerName: "Payment Date" };
|
||||||
|
|
||||||
|
it("leaves Payment Date hidden when the catalog omits it", () => {
|
||||||
|
const columns = withEditablePayDate([hidden]);
|
||||||
|
expect(columns.some((column) => column.field === "payDate")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a single editable pay date when Payment Date is already visible", () => {
|
||||||
|
const columns = withEditablePayDate([hidden, { ...payDate, editable: false }]);
|
||||||
|
const matches = columns.filter((column) => column.field === "payDate");
|
||||||
|
expect(matches).toHaveLength(1);
|
||||||
|
expect(matches[0]?.editable).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import NotificationsNoneOutlinedIcon from "@mui/icons-material/NotificationsNone
|
||||||
import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
|
import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
|
||||||
import { Box, Link as MuiLink, Stack, Typography } from "@mui/material";
|
import { Box, Link as MuiLink, Stack, Typography } from "@mui/material";
|
||||||
import type { GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import type { GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
|
import { withEditablePayDate } from "@/dashboard/approved-columns";
|
||||||
import type { InvoiceColumnKey } from "@/dashboard/column-catalog";
|
import type { InvoiceColumnKey } from "@/dashboard/column-catalog";
|
||||||
import { formatRelativeFromIsoDate, formatSlashDate } from "@/dashboard/invoice-date-format";
|
import { formatRelativeFromIsoDate, formatSlashDate } from "@/dashboard/invoice-date-format";
|
||||||
import type { MockInvoice } from "@/mocks";
|
import type { MockInvoice } from "@/mocks";
|
||||||
|
|
@ -250,6 +251,11 @@ export function columnsFromCatalogKeys(keys: InvoiceColumnKey[]): GridColDef<Moc
|
||||||
return keys.map((key) => catalogColumnDefs[key]);
|
return keys.map((key) => catalogColumnDefs[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Approved tab keeps pay date editable when the drawer shows Payment Date. */
|
||||||
|
export function columnsForApprovedGrid(keys: InvoiceColumnKey[]): GridColDef<MockInvoice>[] {
|
||||||
|
return withEditablePayDate(columnsFromCatalogKeys(keys));
|
||||||
|
}
|
||||||
|
|
||||||
export const readyToPayColumns: GridColDef<MockInvoice>[] = [
|
export const readyToPayColumns: GridColDef<MockInvoice>[] = [
|
||||||
{
|
{
|
||||||
field: "invoiceNumber",
|
field: "invoiceNumber",
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ describe("mocks module", () => {
|
||||||
expect(invoice.lastUpdateActorName.length).toBeGreaterThan(0);
|
expect(invoice.lastUpdateActorName.length).toBeGreaterThan(0);
|
||||||
expect(invoice.lastUpdateAt.length).toBeGreaterThan(0);
|
expect(invoice.lastUpdateAt.length).toBeGreaterThan(0);
|
||||||
expect(invoice.invoiceDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
expect(invoice.invoiceDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||||
|
expect(invoice.invoiceDate).not.toBe(invoice.dueDate);
|
||||||
expect(invoice.processingBegan).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
expect(invoice.processingBegan).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||||
expect(typeof invoice.tagging).toBe("string");
|
expect(typeof invoice.tagging).toBe("string");
|
||||||
expect(typeof invoice.fs).toBe("string");
|
expect(typeof invoice.fs).toBe("string");
|
||||||
|
|
@ -83,6 +84,26 @@ describe("mocks module", () => {
|
||||||
expect(pending.every((invoice) => invoice.actionLabel === "Approve invoice")).toBe(true);
|
expect(pending.every((invoice) => invoice.actionLabel === "Approve invoice")).toBe(true);
|
||||||
const nonPending = invoices.filter((invoice) => invoice.status !== "pending_approval");
|
const nonPending = invoices.filter((invoice) => invoice.status !== "pending_approval");
|
||||||
expect(nonPending.every((invoice) => invoice.actionLabel !== "Approve invoice")).toBe(true);
|
expect(nonPending.every((invoice) => invoice.actionLabel !== "Approve invoice")).toBe(true);
|
||||||
|
expect(
|
||||||
|
invoices
|
||||||
|
.filter((invoice) => invoice.status === "approved")
|
||||||
|
.every((invoice) => invoice.actionLabel === "Approved"),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
invoices
|
||||||
|
.filter((invoice) => invoice.status === "paid")
|
||||||
|
.every((invoice) => invoice.actionLabel === "Paid"),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
invoices
|
||||||
|
.filter((invoice) => invoice.status === "scheduled")
|
||||||
|
.every((invoice) => invoice.actionLabel === "Scheduled"),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
invoices
|
||||||
|
.filter((invoice) => invoice.status === "rejected")
|
||||||
|
.every((invoice) => invoice.actionLabel === "Rejected"),
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("seeds at least two invoices per dashboard tab", () => {
|
it("seeds at least two invoices per dashboard tab", () => {
|
||||||
|
|
@ -101,6 +122,8 @@ describe("mocks module", () => {
|
||||||
|
|
||||||
const updated = updateMockInvoicePayDate("inv-1", "2026-08-18");
|
const updated = updateMockInvoicePayDate("inv-1", "2026-08-18");
|
||||||
expect(updated.payDate).toBe("2026-08-18");
|
expect(updated.payDate).toBe("2026-08-18");
|
||||||
|
expect(updated.lastUpdateMessage).toBe("Pay date updated");
|
||||||
|
expect(updated.lastUpdateAt).not.toBe(before?.lastUpdateAt);
|
||||||
|
|
||||||
const after = getMockInvoices().find((invoice) => invoice.id === "inv-1");
|
const after = getMockInvoices().find((invoice) => invoice.id === "inv-1");
|
||||||
expect(after?.payDate).toBe("2026-08-18");
|
expect(after?.payDate).toBe("2026-08-18");
|
||||||
|
|
@ -148,6 +171,7 @@ describe("mocks module", () => {
|
||||||
authorUserId: "user-1",
|
authorUserId: "user-1",
|
||||||
authorName: "Alex Rivera",
|
authorName: "Alex Rivera",
|
||||||
body: "Need another look",
|
body: "Need another look",
|
||||||
|
createdAt: "2026-08-11T09:00:00.000Z",
|
||||||
});
|
});
|
||||||
expect(withComment.comments.some((comment) => comment.body === "Need another look")).toBe(true);
|
expect(withComment.comments.some((comment) => comment.body === "Need another look")).toBe(true);
|
||||||
|
|
||||||
|
|
@ -168,6 +192,23 @@ describe("mocks module", () => {
|
||||||
expect(getMockApprovals().find((item) => item.invoiceId === "inv-2")?.status).toBe("approved");
|
expect(getMockApprovals().find((item) => item.invoiceId === "inv-2")?.status).toBe("approved");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("refreshes last update from a comment and payment account from the method", () => {
|
||||||
|
const commented = addMockInvoiceComment("inv-1", {
|
||||||
|
authorUserId: "user-1",
|
||||||
|
authorName: "Alex Rivera",
|
||||||
|
body: "Checked the total",
|
||||||
|
createdAt: "2026-09-01T15:00:00.000Z",
|
||||||
|
});
|
||||||
|
expect(commented.lastUpdateAt).toBe("2026-09-01T15:00:00.000Z");
|
||||||
|
expect(commented.lastUpdate).toBe("2026-09-01");
|
||||||
|
expect(commented.lastUpdateMessage).toBe("Checked the total");
|
||||||
|
expect(commented.lastUpdateActorName).toBe("Alex Rivera");
|
||||||
|
|
||||||
|
const method = updateMockInvoiceHeader("inv-1", { paymentMethod: "ach" });
|
||||||
|
expect(method.paymentAccount).toBe("Operating ACH");
|
||||||
|
expect(getMockInvoice("inv-1")?.paymentAccount).toBe("Operating ACH");
|
||||||
|
});
|
||||||
|
|
||||||
it("refreshes description and rowCount when header or lines change", () => {
|
it("refreshes description and rowCount when header or lines change", () => {
|
||||||
const before = getMockInvoice("inv-1");
|
const before = getMockInvoice("inv-1");
|
||||||
expect(before?.rowCount).toBe(2);
|
expect(before?.rowCount).toBe(2);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { INVOICE_ACTION_OPTIONS } from "@/dashboard/column-catalog";
|
|
||||||
import { syncMockApprovalsForInvoice } from "@/mocks/approvals";
|
import { syncMockApprovalsForInvoice } from "@/mocks/approvals";
|
||||||
import { areMocksEnabled } from "@/mocks/env";
|
import { areMocksEnabled } from "@/mocks/env";
|
||||||
|
|
||||||
|
|
@ -104,65 +103,92 @@ type MockInvoiceCore = Omit<
|
||||||
| "rowCount"
|
| "rowCount"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/** Non-pending rows never show the pending-only "Approve invoice" action. */
|
const ACTION_LABEL_BY_STATUS: Record<MockInvoiceStatus, string> = {
|
||||||
const NON_PENDING_ACTION_OPTIONS = INVOICE_ACTION_OPTIONS.filter(
|
pending_approval: "Approve invoice",
|
||||||
(option) => option !== "Approve invoice",
|
approved: "Approved",
|
||||||
);
|
scheduled: "Scheduled",
|
||||||
|
paid: "Paid",
|
||||||
|
rejected: "Rejected",
|
||||||
|
};
|
||||||
|
|
||||||
function actionLabelForStatus(status: MockInvoiceStatus, index: number): string {
|
function actionLabelForStatus(status: MockInvoiceStatus): string {
|
||||||
if (status === "pending_approval") {
|
return ACTION_LABEL_BY_STATUS[status];
|
||||||
return "Approve invoice";
|
|
||||||
}
|
|
||||||
return NON_PENDING_ACTION_OPTIONS[index % NON_PENDING_ACTION_OPTIONS.length];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function derivedActivityFields(invoice: Pick<MockInvoice, "activity" | "dueDate">) {
|
function derivedActivityFields(invoice: Pick<MockInvoice, "activity" | "comments" | "dueDate">) {
|
||||||
const latestActivity = invoice.activity[invoice.activity.length - 1];
|
const events = [
|
||||||
const lastUpdateAt = latestActivity?.createdAt ?? `${invoice.dueDate}T12:00:00.000Z`;
|
...invoice.activity.map((entry) => ({
|
||||||
|
at: entry.createdAt,
|
||||||
|
message: entry.message,
|
||||||
|
actor: entry.actorName,
|
||||||
|
})),
|
||||||
|
...invoice.comments.map((comment) => ({
|
||||||
|
at: comment.createdAt,
|
||||||
|
message: comment.body,
|
||||||
|
actor: comment.authorName,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
const latest = events.reduce<(typeof events)[number] | undefined>((current, event) => {
|
||||||
|
if (!current || event.at > current.at) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}, undefined);
|
||||||
|
const lastUpdateAt = latest?.at ?? `${invoice.dueDate}T12:00:00.000Z`;
|
||||||
return {
|
return {
|
||||||
lastUpdate: lastUpdateAt.slice(0, 10),
|
lastUpdate: lastUpdateAt.slice(0, 10),
|
||||||
lastUpdateMessage: latestActivity?.message ?? "Invoice updated",
|
lastUpdateMessage: latest?.message ?? "Invoice updated",
|
||||||
lastUpdateActorName: latestActivity?.actorName ?? "System",
|
lastUpdateActorName: latest?.actor ?? "System",
|
||||||
lastUpdateAt,
|
lastUpdateAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function paymentAccountFor(paymentMethod: MockInvoice["paymentMethod"]): string {
|
||||||
|
return paymentMethod === "ach" ? "Operating ACH" : "Operating Check";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Invoice date is independent of the due date. Offset varies per seed. */
|
||||||
|
function invoiceDateBeforeDue(dueDate: string, index: number): string {
|
||||||
|
const [year, month, day] = dueDate.split("-").map(Number);
|
||||||
|
const date = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
date.setUTCDate(date.getUTCDate() - (14 + index));
|
||||||
|
return date.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
/** Keep catalog-derived fields in sync after mock mutations. */
|
/** Keep catalog-derived fields in sync after mock mutations. */
|
||||||
function withRefreshedCatalogFields(invoice: MockInvoice, index: number): MockInvoice {
|
function withRefreshedCatalogFields(invoice: MockInvoice): MockInvoice {
|
||||||
return {
|
return {
|
||||||
...invoice,
|
...invoice,
|
||||||
actionLabel: actionLabelForStatus(invoice.status, index),
|
actionLabel: actionLabelForStatus(invoice.status),
|
||||||
...derivedActivityFields(invoice),
|
...derivedActivityFields(invoice),
|
||||||
description: invoice.memo,
|
description: invoice.memo,
|
||||||
|
paymentAccount: paymentAccountFor(invoice.paymentMethod),
|
||||||
rowCount: invoice.lines.length,
|
rowCount: invoice.lines.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function enrichInvoice(core: MockInvoiceCore, index: number): MockInvoice {
|
function enrichInvoice(core: MockInvoiceCore, index: number): MockInvoice {
|
||||||
return withRefreshedCatalogFields(
|
return withRefreshedCatalogFields({
|
||||||
{
|
...core,
|
||||||
...core,
|
hasBolt: index % 3 === 0,
|
||||||
hasBolt: index % 3 === 0,
|
hasBell: index % 2 === 0,
|
||||||
hasBell: index % 2 === 0,
|
hasEye: true,
|
||||||
hasEye: true,
|
actionLabel: actionLabelForStatus(core.status),
|
||||||
actionLabel: actionLabelForStatus(core.status, index),
|
lastUpdate: "",
|
||||||
lastUpdate: "",
|
lastUpdateMessage: "",
|
||||||
lastUpdateMessage: "",
|
lastUpdateActorName: "",
|
||||||
lastUpdateActorName: "",
|
lastUpdateAt: "",
|
||||||
lastUpdateAt: "",
|
invoiceDate: invoiceDateBeforeDue(core.dueDate, index),
|
||||||
invoiceDate: core.dueDate,
|
processingBegan: core.activity[0]?.createdAt?.slice(0, 10) ?? core.dueDate,
|
||||||
processingBegan: core.activity[0]?.createdAt?.slice(0, 10) ?? core.dueDate,
|
tagging: index % 2 === 0 ? "Ops" : "",
|
||||||
tagging: index % 2 === 0 ? "Ops" : "",
|
fs: index % 2 === 0 ? "synced" : "",
|
||||||
fs: index % 2 === 0 ? "synced" : "",
|
description: core.memo,
|
||||||
description: core.memo,
|
invoiceType: "Standard",
|
||||||
invoiceType: "Standard",
|
location: "Ronkonkoma",
|
||||||
location: "Ronkonkoma",
|
paymentAccount: paymentAccountFor(core.paymentMethod),
|
||||||
paymentAccount: core.paymentMethod === "ach" ? "Operating ACH" : "Operating Check",
|
relatedInvoice: null,
|
||||||
relatedInvoice: null,
|
rowCount: core.lines.length,
|
||||||
rowCount: core.lines.length,
|
});
|
||||||
},
|
|
||||||
index,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneInvoice(invoice: MockInvoice): MockInvoice {
|
function cloneInvoice(invoice: MockInvoice): MockInvoice {
|
||||||
|
|
@ -549,7 +575,24 @@ export function updateMockInvoicePayDate(id: string, payDate: string | null): Mo
|
||||||
throw new Error(`Mock invoice not found: ${id}`);
|
throw new Error(`Mock invoice not found: ${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return replaceInvoice(index, { ...invoiceStore[index], payDate });
|
const current = invoiceStore[index];
|
||||||
|
return replaceInvoice(
|
||||||
|
index,
|
||||||
|
withRefreshedCatalogFields({
|
||||||
|
...current,
|
||||||
|
payDate,
|
||||||
|
activity: [
|
||||||
|
...current.activity,
|
||||||
|
{
|
||||||
|
id: `activity-pay-date-${Date.now()}`,
|
||||||
|
actorUserId: "user-1",
|
||||||
|
actorName: "System",
|
||||||
|
message: "Pay date updated",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateMockInvoiceHeader(id: string, patch: MockInvoiceHeaderPatch): MockInvoice {
|
export function updateMockInvoiceHeader(id: string, patch: MockInvoiceHeaderPatch): MockInvoice {
|
||||||
|
|
@ -558,10 +601,7 @@ export function updateMockInvoiceHeader(id: string, patch: MockInvoiceHeaderPatc
|
||||||
throw new Error(`Mock invoice not found: ${id}`);
|
throw new Error(`Mock invoice not found: ${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return replaceInvoice(
|
return replaceInvoice(index, withRefreshedCatalogFields({ ...invoiceStore[index], ...patch }));
|
||||||
index,
|
|
||||||
withRefreshedCatalogFields({ ...invoiceStore[index], ...patch }, index),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateMockInvoiceLines(id: string, lines: MockInvoiceLine[]): MockInvoice {
|
export function updateMockInvoiceLines(id: string, lines: MockInvoiceLine[]): MockInvoice {
|
||||||
|
|
@ -572,13 +612,10 @@ export function updateMockInvoiceLines(id: string, lines: MockInvoiceLine[]): Mo
|
||||||
|
|
||||||
return replaceInvoice(
|
return replaceInvoice(
|
||||||
index,
|
index,
|
||||||
withRefreshedCatalogFields(
|
withRefreshedCatalogFields({
|
||||||
{
|
...invoiceStore[index],
|
||||||
...invoiceStore[index],
|
lines: lines.map((line) => ({ ...line })),
|
||||||
lines: lines.map((line) => ({ ...line })),
|
}),
|
||||||
},
|
|
||||||
index,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -603,10 +640,13 @@ export function addMockInvoiceComment(
|
||||||
createdAt: comment.createdAt ?? new Date().toISOString(),
|
createdAt: comment.createdAt ?? new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return replaceInvoice(index, {
|
return replaceInvoice(
|
||||||
...current,
|
index,
|
||||||
comments: [...current.comments, nextComment],
|
withRefreshedCatalogFields({
|
||||||
});
|
...current,
|
||||||
|
comments: [...current.comments, nextComment],
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transitionMockInvoiceStatus(
|
export function transitionMockInvoiceStatus(
|
||||||
|
|
@ -633,14 +673,11 @@ export function transitionMockInvoiceStatus(
|
||||||
|
|
||||||
const updated = replaceInvoice(
|
const updated = replaceInvoice(
|
||||||
index,
|
index,
|
||||||
withRefreshedCatalogFields(
|
withRefreshedCatalogFields({
|
||||||
{
|
...current,
|
||||||
...current,
|
status,
|
||||||
status,
|
activity: [...current.activity, nextActivity],
|
||||||
activity: [...current.activity, nextActivity],
|
}),
|
||||||
},
|
|
||||||
index,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (status === "approved" || status === "rejected") {
|
if (status === "approved" || status === "rejected") {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue