WFDASHDRIVER-APP/lib/invoiceCheck.ts
Dave 1c48c74bd2 feat: beta invoice match marks matching assigned items collected
After an invoice photo, exact matches are marked collected (with undo);
uncertain ones are listed. Still off unless enabled in Settings > Beta.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 20:58:33 +02:00

79 lines
2.7 KiB
TypeScript

// Beta helper: photograph the invoice and let WFDASH match it against
// everything assigned to this driver at that supplier. Exact matches (same
// product, invoice quantity = ordered quantity) are marked collected through
// the same writes the bulk-collect button uses, and can be undone.
// Off unless the driver switches it on in Settings → Beta.
import { pb, getSettings, updateStockCollectionStatus } from './pocketbase';
export interface MatchedLine {
id: string;
kind: 'customer' | 'stock';
po_number: string;
description: string;
quantity: number;
previous: Record<string, unknown>;
}
export interface ReviewLine {
id: string;
kind: 'customer' | 'stock';
po_number: string;
description: string;
reason: string;
}
export interface InvoiceMatchResult {
po_number: string;
matched: MatchedLine[];
review: ReviewLine[];
error: string;
}
/** Fired by the PO buttons; InvoiceMatchHost (once per page) does the work. */
export const RUN_EVENT = 'invoice-match:run';
/** The WFDASH address to call, or '' when the helper is off or not set up. */
export function invoiceCheckBase(): string {
const s = getSettings();
if (!s.aiCheckEnabled) return '';
const url = (s.aiCheckUrl || process.env.NEXT_PUBLIC_WFDASH_URL || '').trim();
return url.replace(/\/+$/, '');
}
export async function matchInvoice(poNumber: string): Promise<InvoiceMatchResult> {
const base = invoiceCheckBase();
if (!base) throw new Error('Invoice check is not set up (Settings → Beta).');
const res = await fetch(`${base}/api/v1/driver/invoice-match`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${pb.authStore.token}` },
body: JSON.stringify({ po_number: poNumber }),
signal: AbortSignal.timeout(120_000),
});
const body = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(body.error || `Check failed (${res.status})`);
return body as InvoiceMatchResult;
}
/** Mark one matched line collected, exactly as bulkCollect / the stock modal do. */
export async function collectMatched(line: MatchedLine): Promise<void> {
if (line.kind === 'stock') {
await updateStockCollectionStatus(line.id, { qty_collected: line.quantity, expectedQty: line.quantity });
return;
}
const now = new Date().toISOString();
await pb.collection('order_items').update(line.id, {
item_status: 'collected',
drv_status: 'collected',
drv_collected_at: now,
qty_collected: line.quantity,
collected_date: now,
});
}
/** Put a matched line back to how it was before the auto-collect. */
export async function undoMatched(line: MatchedLine): Promise<void> {
await pb
.collection(line.kind === 'stock' ? 'stock_purchase_items' : 'order_items')
.update(line.id, line.previous);
}