feat(web): expand mock invoice fields for Stampli column catalog

Refs: AP-47
This commit is contained in:
Adam Moussa 2026-08-11 15:20:30 -04:00
parent e23ba0f4bb
commit 2f55c36409
No known key found for this signature in database
5 changed files with 284 additions and 3 deletions

View file

@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import {
DEFAULT_VISIBLE_COLUMN_KEYS,
INVOICE_ACTION_OPTIONS,
INVOICE_COLUMN_CATALOG,
LOCKED_COLUMN_KEYS,
} from "@/dashboard/column-catalog";
describe("INVOICE_COLUMN_CATALOG", () => {
it("locks Stampli Visible defaults and locked columns", () => {
expect(DEFAULT_VISIBLE_COLUMN_KEYS).toEqual([
"bolt",
"bell",
"invoiceId",
"userStatus",
"vendor",
"lastUpdate",
"invoiceDate",
"dueDate",
"amount",
"tagging",
"fs",
"eye",
]);
expect(LOCKED_COLUMN_KEYS).toEqual(["bolt", "bell", "invoiceId", "vendor"]);
});
it("includes Hidden catalog entries", () => {
const hidden = INVOICE_COLUMN_CATALOG.filter((entry) => !entry.defaultVisible).map(
(entry) => entry.key,
);
expect(hidden).toEqual([
"description",
"type",
"location",
"paymentAccount",
"paymentDate",
"processingBegan",
"relatedInvoice",
"rowCount",
]);
});
it("locks the ten Action filter options", () => {
expect(INVOICE_ACTION_OPTIONS).toHaveLength(10);
expect(INVOICE_ACTION_OPTIONS).toContain("Approve invoice");
});
});

View file

@ -0,0 +1,77 @@
/** Locked Stampli HTML column catalog (AP-41 / AP-47). */
export type InvoiceColumnKey =
| "bolt"
| "bell"
| "invoiceId"
| "userStatus"
| "vendor"
| "lastUpdate"
| "invoiceDate"
| "dueDate"
| "amount"
| "tagging"
| "fs"
| "eye"
| "description"
| "type"
| "location"
| "paymentAccount"
| "paymentDate"
| "processingBegan"
| "relatedInvoice"
| "rowCount";
export type InvoiceColumnCatalogEntry = {
key: InvoiceColumnKey;
label: string;
locked: boolean;
defaultVisible: boolean;
};
export const INVOICE_COLUMN_CATALOG: InvoiceColumnCatalogEntry[] = [
{ key: "bolt", label: "Bolt", locked: true, defaultVisible: true },
{ key: "bell", label: "Bell", locked: true, defaultVisible: true },
{ key: "invoiceId", label: "Invoice", locked: true, defaultVisible: true },
{ key: "userStatus", label: "Action", locked: false, defaultVisible: true },
{ key: "vendor", label: "Vendor", locked: true, defaultVisible: true },
{ key: "lastUpdate", label: "Last Update", locked: false, defaultVisible: true },
{ key: "invoiceDate", label: "Invoice Date", locked: false, defaultVisible: true },
{ key: "dueDate", label: "Due Date", locked: false, defaultVisible: true },
{ key: "amount", label: "Sum", locked: false, defaultVisible: true },
{ key: "tagging", label: "Tagging", locked: false, defaultVisible: true },
{ key: "fs", label: "F.S.", locked: false, defaultVisible: true },
{ key: "eye", label: "Eye", locked: false, defaultVisible: true },
{ key: "description", label: "Description", locked: false, defaultVisible: false },
{ key: "type", label: "Invoice Type", locked: false, defaultVisible: false },
{ key: "location", label: "Location", locked: false, defaultVisible: false },
{ key: "paymentAccount", label: "Payment Account", locked: false, defaultVisible: false },
{ key: "paymentDate", label: "Payment Date", locked: false, defaultVisible: false },
{ key: "processingBegan", label: "Processing Began", locked: false, defaultVisible: false },
{ key: "relatedInvoice", label: "Related Invoice", locked: false, defaultVisible: false },
{ key: "rowCount", label: "Row count", locked: false, defaultVisible: false },
];
export const DEFAULT_VISIBLE_COLUMN_KEYS: InvoiceColumnKey[] = INVOICE_COLUMN_CATALOG.filter(
(entry) => entry.defaultVisible,
).map((entry) => entry.key);
export const LOCKED_COLUMN_KEYS: InvoiceColumnKey[] = INVOICE_COLUMN_CATALOG.filter(
(entry) => entry.locked,
).map((entry) => entry.key);
/** Locked Action filter options from Stampli HTML (AP-41 / AP-43). */
export const INVOICE_ACTION_OPTIONS = [
"A field update request has been submitted",
"A question has been submitted",
"Approve invoice",
"Attach receipt",
"Filed for my attention",
"Inquiry for you",
"Recalled invoice",
"Received response",
"View Comment",
"Waiting for response",
] as const;
export type InvoiceActionOption = (typeof INVOICE_ACTION_OPTIONS)[number];

View file

@ -1,5 +1,9 @@
import BoltOutlinedIcon from "@mui/icons-material/BoltOutlined";
import NotificationsNoneOutlinedIcon from "@mui/icons-material/NotificationsNoneOutlined";
import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
import { Link as MuiLink } from "@mui/material";
import type { GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import type { InvoiceColumnKey } from "@/dashboard/column-catalog";
import type { MockInvoice } from "@/mocks";
import { Link } from "@/router";
@ -95,17 +99,79 @@ export const invoiceAttentionColumns: GridColDef<MockInvoice>[] = [
renderCell: invoiceNumberLink,
},
{
field: "status",
field: "actionLabel",
headerName: "Action",
flex: 1.2,
minWidth: 140,
valueFormatter: (value: string) => (value === "pending_approval" ? "Approve invoice" : value),
},
{ field: "vendorName", headerName: "Vendor", flex: 1.4, minWidth: 160 },
dateColumn("dueDate", "Due Date"),
moneyColumn("amountDue", "Σ Sum"),
];
const iconColumn = (
field: "hasBolt" | "hasBell" | "hasEye",
headerName: string,
Icon: typeof BoltOutlinedIcon,
): GridColDef<MockInvoice> => ({
field,
headerName,
width: 56,
sortable: false,
filterable: false,
renderCell: (params) => (params.value ? <Icon fontSize="small" color="action" /> : null),
});
/** Full catalog column defs keyed by AP-41 HTML column keys (AP-47). */
export const catalogColumnDefs: Record<InvoiceColumnKey, GridColDef<MockInvoice>> = {
bolt: iconColumn("hasBolt", "", BoltOutlinedIcon),
bell: iconColumn("hasBell", "", NotificationsNoneOutlinedIcon),
invoiceId: {
field: "invoiceNumbers",
headerName: "Invoice",
flex: 1,
minWidth: 120,
renderCell: invoiceNumberLink,
},
userStatus: { field: "actionLabel", headerName: "Action", flex: 1.2, minWidth: 140 },
vendor: { field: "vendorName", headerName: "Vendor", flex: 1.4, minWidth: 160 },
lastUpdate: dateColumn("lastUpdate", "Last Update"),
invoiceDate: dateColumn("invoiceDate", "Invoice Date"),
dueDate: dateColumn("dueDate", "Due Date"),
amount: moneyColumn("amountDue", "Sum"),
tagging: { field: "tagging", headerName: "Tagging", flex: 0.8, minWidth: 100 },
fs: { field: "fs", headerName: "F.S.", flex: 0.6, minWidth: 80 },
eye: iconColumn("hasEye", "", VisibilityOutlinedIcon),
description: { field: "description", headerName: "Description", flex: 1.4, minWidth: 160 },
type: { field: "invoiceType", headerName: "Invoice Type", flex: 1, minWidth: 120 },
location: { field: "location", headerName: "Location", flex: 1, minWidth: 120 },
paymentAccount: {
field: "paymentAccount",
headerName: "Payment Account",
flex: 1.2,
minWidth: 140,
},
paymentDate: dateColumn("payDate", "Payment Date"),
processingBegan: dateColumn("processingBegan", "Processing Began"),
relatedInvoice: {
field: "relatedInvoice",
headerName: "Related Invoice",
flex: 1,
minWidth: 120,
valueFormatter: (value: string | null) => value ?? "—",
},
rowCount: {
field: "rowCount",
headerName: "Row count",
width: 100,
type: "number",
},
};
export function columnsFromCatalogKeys(keys: InvoiceColumnKey[]): GridColDef<MockInvoice>[] {
return keys.map((key) => catalogColumnDefs[key]);
}
export const readyToPayColumns: GridColDef<MockInvoice>[] = [
{
field: "invoiceNumber",

View file

@ -54,6 +54,32 @@ describe("mocks module", () => {
expect(getMockInvoices().length).toBeGreaterThanOrEqual(6);
});
it("seeds Stampli catalog fields on every invoice (AP-47)", () => {
const invoices = getMockInvoices();
expect(invoices.length).toBeGreaterThan(0);
for (const invoice of invoices) {
expect(typeof invoice.hasBolt).toBe("boolean");
expect(typeof invoice.hasBell).toBe("boolean");
expect(typeof invoice.hasEye).toBe("boolean");
expect(invoice.actionLabel.length).toBeGreaterThan(0);
expect(invoice.lastUpdate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
expect(invoice.invoiceDate).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.fs).toBe("string");
expect(typeof invoice.description).toBe("string");
expect(typeof invoice.invoiceType).toBe("string");
expect(typeof invoice.location).toBe("string");
expect(typeof invoice.paymentAccount).toBe("string");
expect(invoice.relatedInvoice === null || typeof invoice.relatedInvoice === "string").toBe(
true,
);
expect(invoice.rowCount).toBe(invoice.lines.length);
}
const pending = invoices.filter((invoice) => invoice.status === "pending_approval");
expect(pending.every((invoice) => invoice.actionLabel === "Approve invoice")).toBe(true);
});
it("seeds at least two invoices per dashboard tab", () => {
const invoices = getMockInvoices();
const pending = invoices.filter((invoice) => invoice.status === "pending_approval");

View file

@ -1,3 +1,4 @@
import { INVOICE_ACTION_OPTIONS } from "@/dashboard/column-catalog";
import { syncMockApprovalsForInvoice } from "@/mocks/approvals";
import { areMocksEnabled } from "@/mocks/env";
@ -45,6 +46,21 @@ export type MockInvoice = {
lines: MockInvoiceLine[];
comments: MockInvoiceComment[];
activity: MockInvoiceActivity[];
hasBolt: boolean;
hasBell: boolean;
hasEye: boolean;
actionLabel: string;
lastUpdate: string;
invoiceDate: string;
processingBegan: string;
tagging: string;
fs: string;
description: string;
invoiceType: string;
location: string;
paymentAccount: string;
relatedInvoice: string | null;
rowCount: number;
};
export type MockInvoiceHeaderPatch = Partial<
@ -63,6 +79,50 @@ export type MockInvoiceHeaderPatch = Partial<
const SAMPLE_DOCUMENT_URL = "/fixtures/sample-invoice.pdf";
type MockInvoiceCore = Omit<
MockInvoice,
| "hasBolt"
| "hasBell"
| "hasEye"
| "actionLabel"
| "lastUpdate"
| "invoiceDate"
| "processingBegan"
| "tagging"
| "fs"
| "description"
| "invoiceType"
| "location"
| "paymentAccount"
| "relatedInvoice"
| "rowCount"
>;
function enrichInvoice(core: MockInvoiceCore, index: number): MockInvoice {
const actionLabel =
core.status === "pending_approval"
? "Approve invoice"
: INVOICE_ACTION_OPTIONS[index % INVOICE_ACTION_OPTIONS.length];
return {
...core,
hasBolt: index % 3 === 0,
hasBell: index % 2 === 0,
hasEye: true,
actionLabel,
lastUpdate: core.activity[core.activity.length - 1]?.createdAt?.slice(0, 10) ?? core.dueDate,
invoiceDate: core.dueDate,
processingBegan: core.activity[0]?.createdAt?.slice(0, 10) ?? core.dueDate,
tagging: index % 2 === 0 ? "Ops" : "",
fs: index % 2 === 0 ? "FS-1" : "",
description: core.memo,
invoiceType: "Standard",
location: "Ronkonkoma",
paymentAccount: core.paymentMethod === "ach" ? "Operating ACH" : "Operating Check",
relatedInvoice: null,
rowCount: core.lines.length,
};
}
function cloneInvoice(invoice: MockInvoice): MockInvoice {
return {
...invoice,
@ -72,7 +132,7 @@ function cloneInvoice(invoice: MockInvoice): MockInvoice {
};
}
const seedInvoices: MockInvoice[] = [
const seedInvoiceCores: MockInvoiceCore[] = [
{
id: "inv-1",
invoiceNumber: "INV-1001",
@ -414,6 +474,10 @@ const seedInvoices: MockInvoice[] = [
},
];
const seedInvoices: MockInvoice[] = seedInvoiceCores.map((core, index) =>
enrichInvoice(core, index),
);
/** Mutable in-memory invoice store for mock POC interactions. */
let invoiceStore: MockInvoice[] = seedInvoices.map((invoice) => cloneInvoice(invoice));