WFDASH-WEBAPP/src/lib/collection-ai-card.ts
Dave 8efee9db42 fix(collection-ai): pre-fill only from the chip's tax invoice, AI column only with ai_check
M2: the confirm dialog pre-fill uses the card's newest document (as the
chip) and only when it is a tax invoice.
M3: the AI ticks column (and its min-width) shows only when a row has
ai_check; the POS column stays whenever in_pos is known.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:20:46 +02:00

97 lines
3.5 KiB
TypeScript

import type { ConfirmRow } from "@/lib/server/collection-confirm-service";
import type { CollectionDocType } from "@/lib/types";
type AiDoc = NonNullable<ConfirmRow["ai_doc"]>;
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
/**
* Union of the document findings across a card's distinct reads, then each
* row's own line findings (`ai_check.findings`), deduped, in order.
*/
export function cardFindings(rows: ConfirmRow[]): string[] {
const seenReads = new Set<string>();
const seen = new Set<string>();
const out: string[] = [];
const add = (finding: string) => {
if (!finding || seen.has(finding)) return;
seen.add(finding);
out.push(finding);
};
for (const row of rows) {
const doc = row.ai_doc;
if (!doc || seenReads.has(doc.read_id)) continue;
seenReads.add(doc.read_id);
for (const finding of doc.findings ?? []) add(finding);
}
for (const row of rows) {
for (const finding of row.ai_check?.findings ?? []) add(finding);
}
return out;
}
/** The card's newest document (first non-null wins on equal dates). */
export function cardDocument(rows: ConfirmRow[]): AiDoc | null {
let best: AiDoc | null = null;
for (const row of rows) {
const doc = row.ai_doc;
if (!doc) continue;
if (!best || (doc.doc_date ?? "") > (best.doc_date ?? "")) best = doc;
}
return best;
}
/**
* Pre-fill for the confirm dialog: the number and date of the card's
* document (the same newest document the chip shows), only when it is a
* tax invoice — a delivery note's number is not the invoice number.
*/
export function aiInvoiceDefaults(
rows: ConfirmRow[],
): { number: string; date?: string } | undefined {
const doc = cardDocument(rows);
if (!doc || doc.doc_type !== "tax_invoice") return undefined;
const number = (doc.document_no ?? "").trim();
if (!number) return undefined;
const date = doc.doc_date ?? "";
return /^\d{4}-\d{2}-\d{2}$/.test(date) ? { number, date } : { number };
}
/** Toast for an Add to POS response: plain success, or the counts when any row didn't go in. */
export function posResultToast(
results: Array<{ status: string }>,
): { level: "success" | "warning"; message: string } {
const count = (status: string) => results.filter((r) => r.status === status).length;
const created = count("created");
const failed = count("failed");
const needsSku = count("needs_sku");
if (failed === 0 && needsSku === 0) {
return { level: "success", message: `Added ${created} item${created === 1 ? "" : "s"} to the POS` };
}
const parts = [`${created} added`];
if (failed > 0) parts.push(`${failed} failed`);
if (needsSku > 0) parts.push(`${needsSku} need${needsSku === 1 ? "s" : ""} a SKU`);
return { level: "warning", message: parts.join(", ") };
}
export function docTypeLabel(type: CollectionDocType): string {
if (type === "tax_invoice") return "Tax invoice";
if (type === "delivery_note") return "Delivery note";
return "Document";
}
/** "2026-10-16" -> "16 Oct 2026"; anything else is returned as is. */
export function formatDocDate(iso: string): string {
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec((iso ?? "").trim());
if (!m) return iso ?? "";
const month = Number(m[2]) - 1;
if (month < 0 || month > 11) return iso;
return `${Number(m[3])} ${MONTHS[month]} ${m[1]}`;
}
export function documentChipLabel(doc: AiDoc): string {
const head = [docTypeLabel(doc.doc_type), doc.document_no].filter(Boolean).join(" ");
return [head, formatDocDate(doc.doc_date), "read from driver photo"]
.filter(Boolean)
.join(" · ");
}