mirror of
https://github.com/Sea-Haven-Industries/seahaven-ap.git
synced 2026-10-06 21:41:56 +00:00
feat(web): expand mock invoice fields for Stampli column catalog
Refs: AP-47
This commit is contained in:
parent
e23ba0f4bb
commit
2f55c36409
5 changed files with 284 additions and 3 deletions
48
src/dashboard/column-catalog.test.ts
Normal file
48
src/dashboard/column-catalog.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
77
src/dashboard/column-catalog.ts
Normal file
77
src/dashboard/column-catalog.ts
Normal 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];
|
||||||
|
|
@ -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 { Link as MuiLink } from "@mui/material";
|
||||||
import type { GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
import type { GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
|
||||||
|
import type { InvoiceColumnKey } from "@/dashboard/column-catalog";
|
||||||
import type { MockInvoice } from "@/mocks";
|
import type { MockInvoice } from "@/mocks";
|
||||||
import { Link } from "@/router";
|
import { Link } from "@/router";
|
||||||
|
|
||||||
|
|
@ -95,17 +99,79 @@ export const invoiceAttentionColumns: GridColDef<MockInvoice>[] = [
|
||||||
renderCell: invoiceNumberLink,
|
renderCell: invoiceNumberLink,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "status",
|
field: "actionLabel",
|
||||||
headerName: "Action",
|
headerName: "Action",
|
||||||
flex: 1.2,
|
flex: 1.2,
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
valueFormatter: (value: string) => (value === "pending_approval" ? "Approve invoice" : value),
|
|
||||||
},
|
},
|
||||||
{ field: "vendorName", headerName: "Vendor", flex: 1.4, minWidth: 160 },
|
{ field: "vendorName", headerName: "Vendor", flex: 1.4, minWidth: 160 },
|
||||||
dateColumn("dueDate", "Due Date"),
|
dateColumn("dueDate", "Due Date"),
|
||||||
moneyColumn("amountDue", "Σ Sum"),
|
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>[] = [
|
export const readyToPayColumns: GridColDef<MockInvoice>[] = [
|
||||||
{
|
{
|
||||||
field: "invoiceNumber",
|
field: "invoiceNumber",
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,32 @@ describe("mocks module", () => {
|
||||||
expect(getMockInvoices().length).toBeGreaterThanOrEqual(6);
|
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", () => {
|
it("seeds at least two invoices per dashboard tab", () => {
|
||||||
const invoices = getMockInvoices();
|
const invoices = getMockInvoices();
|
||||||
const pending = invoices.filter((invoice) => invoice.status === "pending_approval");
|
const pending = invoices.filter((invoice) => invoice.status === "pending_approval");
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
|
@ -45,6 +46,21 @@ export type MockInvoice = {
|
||||||
lines: MockInvoiceLine[];
|
lines: MockInvoiceLine[];
|
||||||
comments: MockInvoiceComment[];
|
comments: MockInvoiceComment[];
|
||||||
activity: MockInvoiceActivity[];
|
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<
|
export type MockInvoiceHeaderPatch = Partial<
|
||||||
|
|
@ -63,6 +79,50 @@ export type MockInvoiceHeaderPatch = Partial<
|
||||||
|
|
||||||
const SAMPLE_DOCUMENT_URL = "/fixtures/sample-invoice.pdf";
|
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 {
|
function cloneInvoice(invoice: MockInvoice): MockInvoice {
|
||||||
return {
|
return {
|
||||||
...invoice,
|
...invoice,
|
||||||
|
|
@ -72,7 +132,7 @@ function cloneInvoice(invoice: MockInvoice): MockInvoice {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const seedInvoices: MockInvoice[] = [
|
const seedInvoiceCores: MockInvoiceCore[] = [
|
||||||
{
|
{
|
||||||
id: "inv-1",
|
id: "inv-1",
|
||||||
invoiceNumber: "INV-1001",
|
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. */
|
/** Mutable in-memory invoice store for mock POC interactions. */
|
||||||
let invoiceStore: MockInvoice[] = seedInvoices.map((invoice) => cloneInvoice(invoice));
|
let invoiceStore: MockInvoice[] = seedInvoices.map((invoice) => cloneInvoice(invoice));
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue