// Purchase-order level documents for the driver: the PO PDF the office // attached, and the supplier invoice photo the driver takes once per PO. // // Why per PO: drivers used to photograph the same invoice on every // line they collected (one item ended up with 7 identical copies on // 2026-10-16). WFDASH's Collection Confirm already reads photos per PO // card — it gathers every collected line's attachments and dedupes — // so one upload onto one line of the PO is all it needs. import { pb } from './pocketbase'; import { compressPhoto } from './image'; import { OrderItem, PurchaseOrder } from './types'; /** URL of the first PDF on the purchase order, or null when the office attached none. */ export function buildPoFileUrl(po: PurchaseOrder | null | undefined): string | null { if (!po?.documents?.length) return null; const record = { id: po.id, collectionId: (po as unknown as { collectionId?: string }).collectionId ?? '', }; return pb.files.getUrl(record, po.documents[0]); } // PocketBase snake-cases the uploaded name and adds a random suffix, // so "invoice-PO123-p1.jpg" is stored as "invoice_po123_p1_ab12cd34ef.jpg". // The prefix is what lets us count pages after a reload. const INVOICE_PREFIX = 'invoice_'; const isInvoicePhoto = (filename: string) => filename.toLowerCase().startsWith(INVOICE_PREFIX); // Collected lines drop off the driver's list, and with them any // invoice page stored on them — the header would then ask for page 1 // again. So the page count is also remembered per PO on the device. const PAGES_KEY = 'wfdash-driver:invoice-pages'; function readRemembered(): Record { try { const raw = localStorage.getItem(PAGES_KEY); return raw ? (JSON.parse(raw) as Record) : {}; } catch { return {}; } } function rememberPages(poId: string, pages: number) { try { const all = readRemembered(); all[poId] = Math.max(all[poId] ?? 0, pages); localStorage.setItem(PAGES_KEY, JSON.stringify(all)); } catch { // Private mode / storage full: the count just falls back to the list. } } /** Number of invoice photo pages already uploaded for the PO these lines belong to. */ export function countInvoicePages(items: OrderItem[]): number { const onList = items.reduce( (n, item) => n + (item.order_item_attachements ?? []).filter(isInvoicePhoto).length, 0, ); const poId = items[0]?.expand?.purchase_order?.id; const remembered = poId ? readRemembered()[poId] ?? 0 : 0; return Math.max(onList, remembered); } /** * The line the PO's invoice photo goes on. Stock pickups have no * attachment field, so only customer order lines qualify. A line that * already carries an invoice page wins, so page 2 lands next to page 1. */ export function invoicePhotoTarget(items: OrderItem[]): OrderItem | null { const lines = items.filter((i) => i.kind !== 'pickup'); return ( lines.find((i) => (i.order_item_attachements ?? []).some(isInvoicePhoto)) ?? lines[0] ?? null ); } /** * Compress and append one invoice page to `target`. Returns the row's * full attachment list after the upload so the caller can refresh its * local copy (and the page count) without a refetch. Throws on PB errors. */ export async function uploadInvoicePage(params: { target: OrderItem; poNumber: string; page: number; file: File; }): Promise { const { target, poNumber, page, file } = params; const compressed = await compressPhoto(file); const ext = compressed.type === 'image/jpeg' ? 'jpg' : (compressed.name.split('.').pop() || 'jpg'); const safePo = poNumber.replace(/[^A-Za-z0-9]+/g, '') || 'po'; const named = new File([compressed], `invoice-${safePo}-p${page}.${ext}`, { type: compressed.type, lastModified: Date.now(), }); const formData = new FormData(); formData.append('order_item_attachements+', named); const updated = await pb.collection('order_items').update(target.id, formData); const poId = target.expand?.purchase_order?.id; if (poId) rememberPages(poId, page); return (updated as unknown as { order_item_attachements?: string[] }).order_item_attachements ?? []; }