fix: keep queue model server safe
This commit is contained in:
parent
d00b5f1204
commit
03ac13d951
4 changed files with 57 additions and 68 deletions
|
|
@ -2,7 +2,8 @@ import { Topbar } from "@/components/layout/sidebar";
|
|||
import { pbAdmin } from "@/lib/pb";
|
||||
import { composeFilter, filterOpt, siteFilter, type SearchParams } from "@/lib/site-scope";
|
||||
import { getSiteScope } from "@/lib/site-scope-server";
|
||||
import { WorkQueueView, mapCandidateRecord, matchesCandidate } from "./work-queue-view";
|
||||
import { WorkQueueView } from "./work-queue-view";
|
||||
import { mapCandidateRecord, matchesCandidate } from "./work-queue-model";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import { displayCandidateStage } from "@/lib/dn-pipeline/stages";
|
||||
|
||||
export type QueueStage = "matching" | "needs_review" | "ready_to_post" | "posted" | "dismissed";
|
||||
export interface WorkQueueCandidate {
|
||||
id: string; siteId: string; siteName: string; supplierId: string; supplierName: string;
|
||||
purchaseOrderId: string; purchaseOrderNumber: string; documentId: string; documentNumber: string;
|
||||
documentType: string; documentDate: string; batchId: string; batchName: string; stage: QueueStage;
|
||||
reason: string; matchMethod: string; unresolvedLineItems: number;
|
||||
}
|
||||
|
||||
export function mapCandidateRecord(row: Record<string, any>): WorkQueueCandidate {
|
||||
const batch = row.expand?.batch ?? {};
|
||||
const site = batch.expand?.site ?? row.expand?.delivery_note?.expand?.site ?? {};
|
||||
const supplier = row.expand?.supplier ?? row.expand?.purchase_order?.expand?.supplier_ref ?? {};
|
||||
const po = row.expand?.purchase_order ?? {};
|
||||
const deliveryNote = row.expand?.delivery_note ?? {};
|
||||
const legacyPosted = row.status === "posted" || row.status === "promoted" || row.stage === "posted" || Boolean(row.posted_at);
|
||||
const checks = Array.isArray(row.checks) ? row.checks : [];
|
||||
const unresolvedLineItems = Array.isArray(row.items) ? row.items.filter((item: any) => !item?.purchase_order_item && !item?.purchaseOrderItemId).length : Number(row.unresolved_line_items ?? 0);
|
||||
const reason = checks.find((check: any) => check?.passed === false)?.reason ?? row.reason ?? "Needs review";
|
||||
return {
|
||||
id: String(row.id ?? ""), siteId: String(row.site ?? batch.site ?? deliveryNote.site ?? ""), siteName: String(site.name ?? "Unknown site"),
|
||||
supplierId: String(row.supplier ?? po.supplier_ref ?? ""), supplierName: String(supplier.name ?? row.supplier_name_raw ?? po.supplier ?? ""),
|
||||
purchaseOrderId: String(row.purchase_order ?? deliveryNote.purchase_order ?? ""), purchaseOrderNumber: String(row.doc_po_number ?? po.purchase_order_number ?? row.po_number_raw ?? ""),
|
||||
documentId: String(row.delivery_note ?? deliveryNote.id ?? ""), documentNumber: String(row.doc_number ?? (legacyPosted ? row.delivery_note_number ?? deliveryNote.delivery_note_number : "")),
|
||||
documentType: String(row.doc_type ?? (legacyPosted ? deliveryNote.document_type : "unknown")), documentDate: String(row.doc_date || (legacyPosted ? deliveryNote.date_received : "") || row.posted_at || batch.uploaded_at || ""),
|
||||
batchId: String(row.batch ?? batch.id ?? ""), batchName: String(batch.file ?? batch.id ?? ""),
|
||||
stage: displayCandidateStage(String(row.status ?? ""), String(row.stage ?? "")), reason, matchMethod: String(row.match_method ?? ""), unresolvedLineItems,
|
||||
};
|
||||
}
|
||||
|
||||
export function matchesCandidate(candidate: WorkQueueCandidate, params: Record<string, string | undefined>): boolean {
|
||||
const value = (key: string) => params[key] ?? "";
|
||||
return [["site", candidate.siteId, candidate.siteName], ["supplier", candidate.supplierId, candidate.supplierName], ["purchase_order", candidate.purchaseOrderId, candidate.purchaseOrderNumber], ["stage", candidate.stage], ["reason", candidate.reason], ["document_type", candidate.documentType]].every(([key, ...values]) => !value(key) || values.some((item) => String(item).toLowerCase() === value(key).toLowerCase()));
|
||||
}
|
||||
|
||||
export function buildQueueHref(current: Record<string, string | undefined>, changes: Record<string, string | undefined>): string {
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries({ ...current, ...changes })) if (value) params.set(key, value);
|
||||
const query = params.toString();
|
||||
return `/dashboard/financial/delivery-notes/work-queue${query ? `?${query}` : ""}`;
|
||||
}
|
||||
|
|
@ -4,29 +4,8 @@ import Link from "next/link";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { displayCandidateStage } from "@/lib/dn-pipeline/stages";
|
||||
|
||||
export type QueueStage = "matching" | "needs_review" | "ready_to_post" | "posted" | "dismissed";
|
||||
|
||||
export interface WorkQueueCandidate {
|
||||
id: string;
|
||||
siteId: string;
|
||||
siteName: string;
|
||||
supplierId: string;
|
||||
supplierName: string;
|
||||
purchaseOrderId: string;
|
||||
purchaseOrderNumber: string;
|
||||
documentId: string;
|
||||
documentNumber: string;
|
||||
documentType: string;
|
||||
documentDate: string;
|
||||
batchId: string;
|
||||
batchName: string;
|
||||
stage: QueueStage;
|
||||
reason: string;
|
||||
matchMethod: string;
|
||||
unresolvedLineItems: number;
|
||||
}
|
||||
import { buildQueueHref, type WorkQueueCandidate } from "./work-queue-model";
|
||||
export type { WorkQueueCandidate } from "./work-queue-model";
|
||||
|
||||
export interface QueuePoGroup {
|
||||
key: string;
|
||||
|
|
@ -43,35 +22,6 @@ export interface QueueGroup {
|
|||
purchaseOrders: QueuePoGroup[];
|
||||
}
|
||||
|
||||
export function mapCandidateRecord(row: Record<string, any>): WorkQueueCandidate {
|
||||
const batch = row.expand?.batch ?? {};
|
||||
const site = batch.expand?.site ?? row.expand?.delivery_note?.expand?.site ?? {};
|
||||
const supplier = row.expand?.supplier ?? row.expand?.purchase_order?.expand?.supplier_ref ?? {};
|
||||
const po = row.expand?.purchase_order ?? {};
|
||||
const deliveryNote = row.expand?.delivery_note ?? {};
|
||||
const legacyPosted = row.status === "posted" || row.status === "promoted" || row.stage === "posted" || Boolean(row.posted_at);
|
||||
const checks = Array.isArray(row.checks) ? row.checks : [];
|
||||
const unresolvedLineItems = Array.isArray(row.items)
|
||||
? row.items.filter((item: any) => !item?.purchase_order_item && !item?.purchaseOrderItemId).length
|
||||
: Number(row.unresolved_line_items ?? 0);
|
||||
const reason = checks.find((check: any) => check?.passed === false)?.reason ?? row.reason ?? "Needs review";
|
||||
return {
|
||||
id: String(row.id ?? ""), siteId: String(row.site ?? batch.site ?? deliveryNote.site ?? ""), siteName: String(site.name ?? "Unknown site"),
|
||||
supplierId: String(row.supplier ?? po.supplier_ref ?? ""), supplierName: String(supplier.name ?? row.supplier_name_raw ?? po.supplier ?? ""),
|
||||
purchaseOrderId: String(row.purchase_order ?? deliveryNote.purchase_order ?? ""), purchaseOrderNumber: String(row.doc_po_number ?? po.purchase_order_number ?? row.po_number_raw ?? ""),
|
||||
documentId: String(row.delivery_note ?? deliveryNote.id ?? ""), documentNumber: String(row.doc_number ?? (legacyPosted ? row.delivery_note_number ?? deliveryNote.delivery_note_number : "")),
|
||||
documentType: String(row.doc_type ?? (legacyPosted ? deliveryNote.document_type : "unknown")), documentDate: String(row.doc_date || (legacyPosted ? deliveryNote.date_received : "") || row.posted_at || batch.uploaded_at || ""),
|
||||
batchId: String(row.batch ?? batch.id ?? ""), batchName: String(batch.file ?? batch.id ?? ""),
|
||||
stage: displayCandidateStage(String(row.status ?? ""), String(row.stage ?? "")), reason, matchMethod: String(row.match_method ?? ""), unresolvedLineItems,
|
||||
};
|
||||
}
|
||||
|
||||
export function matchesCandidate(candidate: WorkQueueCandidate, params: Record<string, string | undefined>): boolean {
|
||||
const value = (key: string) => params[key] ?? "";
|
||||
return [["site", candidate.siteId, candidate.siteName], ["supplier", candidate.supplierId, candidate.supplierName], ["purchase_order", candidate.purchaseOrderId, candidate.purchaseOrderNumber], ["stage", candidate.stage], ["reason", candidate.reason], ["document_type", candidate.documentType]]
|
||||
.every(([key, ...values]) => !value(key) || values.some((item) => String(item).toLowerCase() === value(key).toLowerCase()));
|
||||
}
|
||||
|
||||
const EXCEPTION_LABELS = {
|
||||
unknownSupplier: "Unknown supplier",
|
||||
supplierKnownPoUnknown: "Supplier known / PO unknown",
|
||||
|
|
@ -132,18 +82,6 @@ function getException(map: Map<string, QueueGroup>, key: string, label: string):
|
|||
return created;
|
||||
}
|
||||
|
||||
export function buildQueueHref(
|
||||
current: Record<string, string | undefined>,
|
||||
changes: Record<string, string | undefined>,
|
||||
): string {
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries({ ...current, ...changes })) {
|
||||
if (value) params.set(key, value);
|
||||
}
|
||||
const query = params.toString();
|
||||
return `/dashboard/financial/delivery-notes/work-queue${query ? `?${query}` : ""}`;
|
||||
}
|
||||
|
||||
export function WorkQueueView({
|
||||
candidates,
|
||||
filters,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildQueueHref,
|
||||
groupCandidates,
|
||||
mapCandidateRecord,
|
||||
matchesCandidate,
|
||||
WorkQueueView,
|
||||
type WorkQueueCandidate,
|
||||
} from "./work-queue-view";
|
||||
import { buildQueueHref, mapCandidateRecord, matchesCandidate } from "./work-queue-model";
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { SuppliersView } from "../../suppliers/suppliers-view";
|
||||
|
||||
const candidate = (
|
||||
|
|
@ -35,6 +35,14 @@ const candidate = (
|
|||
});
|
||||
|
||||
describe("supplier/PO queue grouping", () => {
|
||||
it("keeps server-callable mapping out of the use-client view module", () => {
|
||||
const page = readFileSync(resolve(__dirname, "page.tsx"), "utf8");
|
||||
const view = readFileSync(resolve(__dirname, "work-queue-view.tsx"), "utf8");
|
||||
expect(page).toContain('from "./work-queue-model"');
|
||||
expect(view).toContain('from "./work-queue-model"');
|
||||
expect(view).not.toContain("export function mapCandidateRecord");
|
||||
});
|
||||
|
||||
it("groups known suppliers by purchase order and keeps exception groups explicit", () => {
|
||||
const groups = groupCandidates([
|
||||
candidate(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue