mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-09-30 20:53:18 +00:00
Merge branch 'feature/columns-slideover' into feature/filter-slideover
This commit is contained in:
commit
ed0d992bd0
6 changed files with 140 additions and 21 deletions
|
|
@ -6,7 +6,7 @@ import { FilterDrawer } from "@/dashboard/FilterDrawer";
|
|||
import { InvoiceFilterToolbar } from "@/dashboard/InvoiceFilterToolbar";
|
||||
import { CreatePayRunButton } from "@/dashboard/CreatePayRunButton";
|
||||
import { InvoiceDataGrid } from "@/dashboard/InvoiceDataGrid";
|
||||
import { columnsFromCatalogKeys } from "@/dashboard/columns";
|
||||
import { columnsForApprovedGrid, columnsFromCatalogKeys } from "@/dashboard/columns";
|
||||
import {
|
||||
loadColumnPrefs,
|
||||
resetColumnPrefs,
|
||||
|
|
@ -86,6 +86,10 @@ export function DashboardGrids() {
|
|||
() => columnsFromCatalogKeys(columnPrefs.visibleOrder),
|
||||
[columnPrefs.visibleOrder],
|
||||
);
|
||||
const approvedColumns = useMemo(
|
||||
() => columnsForApprovedGrid(columnPrefs.visibleOrder),
|
||||
[columnPrefs.visibleOrder],
|
||||
);
|
||||
|
||||
const openInvoice = (params: GridRowParams<MockInvoice>) => {
|
||||
navigate("/invoices/:id", { params: { id: params.row.id } });
|
||||
|
|
@ -235,18 +239,12 @@ export function DashboardGrids() {
|
|||
<Box sx={{ bgcolor: "background.paper", p: 2 }}>
|
||||
{filteredAttention.length > 0 && tab === "attention" ? (
|
||||
<Typography
|
||||
component="button"
|
||||
type="button"
|
||||
data-testid="attention-banner"
|
||||
variant="body2"
|
||||
sx={{
|
||||
display: "block",
|
||||
mb: 1.5,
|
||||
p: 0,
|
||||
border: 0,
|
||||
background: "none",
|
||||
cursor: "pointer",
|
||||
color: "primary.main",
|
||||
typography: "body2",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
|
|
@ -288,7 +286,7 @@ export function DashboardGrids() {
|
|||
<InvoiceDataGrid
|
||||
data-testid="grid-ready"
|
||||
rows={filteredApproved}
|
||||
columns={catalogColumns}
|
||||
columns={approvedColumns}
|
||||
checkboxSelection
|
||||
rowSelectionModel={selection}
|
||||
onRowSelectionModelChange={(model) => setSelection(model)}
|
||||
|
|
|
|||
16
src/dashboard/approved-columns.ts
Normal file
16
src/dashboard/approved-columns.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { GridColDef } from "@mui/x-data-grid";
|
||||
import type { MockInvoice } from "@/mocks";
|
||||
|
||||
/** Keep one editable pay-date column on the approved grid. */
|
||||
export function withEditablePayDate(
|
||||
columns: GridColDef<MockInvoice>[],
|
||||
payDateColumn: GridColDef<MockInvoice>,
|
||||
): GridColDef<MockInvoice>[] {
|
||||
const next = columns.map((column) =>
|
||||
column.field === "payDate" ? { ...column, editable: true } : column,
|
||||
);
|
||||
if (next.some((column) => column.field === "payDate")) {
|
||||
return next;
|
||||
}
|
||||
return [...next, { ...payDateColumn, editable: true }];
|
||||
}
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
import type { GridColDef } from "@mui/x-data-grid";
|
||||
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";
|
||||
|
||||
describe("invoice column date helpers (AP-47)", () => {
|
||||
|
|
@ -26,3 +29,22 @@ describe("invoice column date helpers (AP-47)", () => {
|
|||
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("appends an editable pay date when the catalog hides Payment Date", () => {
|
||||
const columns = withEditablePayDate([hidden], payDate);
|
||||
expect(columns.filter((column) => column.field === "payDate")).toEqual([
|
||||
{ field: "payDate", headerName: "Payment Date", editable: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps a single editable pay date when Payment Date is already visible", () => {
|
||||
const columns = withEditablePayDate([hidden, { ...payDate, editable: false }], payDate);
|
||||
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 { Box, Link as MuiLink, Stack, Typography } from "@mui/material";
|
||||
import type { GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||
import { withEditablePayDate } from "@/dashboard/approved-columns";
|
||||
import type { InvoiceColumnKey } from "@/dashboard/column-catalog";
|
||||
import { formatRelativeFromIsoDate, formatSlashDate } from "@/dashboard/invoice-date-format";
|
||||
import type { MockInvoice } from "@/mocks";
|
||||
|
|
@ -250,6 +251,11 @@ export function columnsFromCatalogKeys(keys: InvoiceColumnKey[]): GridColDef<Moc
|
|||
return keys.map((key) => catalogColumnDefs[key]);
|
||||
}
|
||||
|
||||
/** Approved tab keeps an editable pay date even when the catalog hides Payment Date. */
|
||||
export function columnsForApprovedGrid(keys: InvoiceColumnKey[]): GridColDef<MockInvoice>[] {
|
||||
return withEditablePayDate(columnsFromCatalogKeys(keys), catalogColumnDefs.paymentDate);
|
||||
}
|
||||
|
||||
export const readyToPayColumns: GridColDef<MockInvoice>[] = [
|
||||
{
|
||||
field: "invoiceNumber",
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ describe("mocks module", () => {
|
|||
expect(invoice.lastUpdateActorName.length).toBeGreaterThan(0);
|
||||
expect(invoice.lastUpdateAt.length).toBeGreaterThan(0);
|
||||
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(typeof invoice.tagging).toBe("string");
|
||||
expect(typeof invoice.fs).toBe("string");
|
||||
|
|
@ -101,6 +102,8 @@ describe("mocks module", () => {
|
|||
|
||||
const updated = updateMockInvoicePayDate("inv-1", "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");
|
||||
expect(after?.payDate).toBe("2026-08-18");
|
||||
|
|
@ -148,6 +151,7 @@ describe("mocks module", () => {
|
|||
authorUserId: "user-1",
|
||||
authorName: "Alex Rivera",
|
||||
body: "Need another look",
|
||||
createdAt: "2026-08-11T09:00:00.000Z",
|
||||
});
|
||||
expect(withComment.comments.some((comment) => comment.body === "Need another look")).toBe(true);
|
||||
|
||||
|
|
@ -168,6 +172,23 @@ describe("mocks module", () => {
|
|||
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", () => {
|
||||
const before = getMockInvoice("inv-1");
|
||||
expect(before?.rowCount).toBe(2);
|
||||
|
|
|
|||
|
|
@ -116,17 +116,46 @@ function actionLabelForStatus(status: MockInvoiceStatus, index: number): string
|
|||
return NON_PENDING_ACTION_OPTIONS[index % NON_PENDING_ACTION_OPTIONS.length];
|
||||
}
|
||||
|
||||
function derivedActivityFields(invoice: Pick<MockInvoice, "activity" | "dueDate">) {
|
||||
const latestActivity = invoice.activity[invoice.activity.length - 1];
|
||||
const lastUpdateAt = latestActivity?.createdAt ?? `${invoice.dueDate}T12:00:00.000Z`;
|
||||
function derivedActivityFields(invoice: Pick<MockInvoice, "activity" | "comments" | "dueDate">) {
|
||||
const events = [
|
||||
...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 {
|
||||
lastUpdate: lastUpdateAt.slice(0, 10),
|
||||
lastUpdateMessage: latestActivity?.message ?? "Invoice updated",
|
||||
lastUpdateActorName: latestActivity?.actorName ?? "System",
|
||||
lastUpdateMessage: latest?.message ?? "Invoice updated",
|
||||
lastUpdateActorName: latest?.actor ?? "System",
|
||||
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. */
|
||||
function withRefreshedCatalogFields(invoice: MockInvoice, index: number): MockInvoice {
|
||||
return {
|
||||
|
|
@ -134,6 +163,7 @@ function withRefreshedCatalogFields(invoice: MockInvoice, index: number): MockIn
|
|||
actionLabel: actionLabelForStatus(invoice.status, index),
|
||||
...derivedActivityFields(invoice),
|
||||
description: invoice.memo,
|
||||
paymentAccount: paymentAccountFor(invoice.paymentMethod),
|
||||
rowCount: invoice.lines.length,
|
||||
};
|
||||
}
|
||||
|
|
@ -150,14 +180,14 @@ function enrichInvoice(core: MockInvoiceCore, index: number): MockInvoice {
|
|||
lastUpdateMessage: "",
|
||||
lastUpdateActorName: "",
|
||||
lastUpdateAt: "",
|
||||
invoiceDate: core.dueDate,
|
||||
invoiceDate: invoiceDateBeforeDue(core.dueDate, index),
|
||||
processingBegan: core.activity[0]?.createdAt?.slice(0, 10) ?? core.dueDate,
|
||||
tagging: index % 2 === 0 ? "Ops" : "",
|
||||
fs: index % 2 === 0 ? "synced" : "",
|
||||
description: core.memo,
|
||||
invoiceType: "Standard",
|
||||
location: "Ronkonkoma",
|
||||
paymentAccount: core.paymentMethod === "ach" ? "Operating ACH" : "Operating Check",
|
||||
paymentAccount: paymentAccountFor(core.paymentMethod),
|
||||
relatedInvoice: null,
|
||||
rowCount: core.lines.length,
|
||||
},
|
||||
|
|
@ -549,7 +579,27 @@ export function updateMockInvoicePayDate(id: string, payDate: string | null): Mo
|
|||
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(),
|
||||
},
|
||||
],
|
||||
},
|
||||
index,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function updateMockInvoiceHeader(id: string, patch: MockInvoiceHeaderPatch): MockInvoice {
|
||||
|
|
@ -603,10 +653,16 @@ export function addMockInvoiceComment(
|
|||
createdAt: comment.createdAt ?? new Date().toISOString(),
|
||||
};
|
||||
|
||||
return replaceInvoice(index, {
|
||||
...current,
|
||||
comments: [...current.comments, nextComment],
|
||||
});
|
||||
return replaceInvoice(
|
||||
index,
|
||||
withRefreshedCatalogFields(
|
||||
{
|
||||
...current,
|
||||
comments: [...current.comments, nextComment],
|
||||
},
|
||||
index,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function transitionMockInvoiceStatus(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue