import PocketBase from 'pocketbase'; import { AppSettings, DEFAULT_SETTINGS, OrderItem, DriverLocation } from './types'; const POCKETBASE_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || 'https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io'; export const pb = new PocketBase(POCKETBASE_URL); // Disable auto-cancellation so React re-renders don't kill in-flight requests pb.autoCancellation(false); // Auth helpers export const isAuthenticated = () => pb.authStore.isValid; export const getCurrentUser = () => pb.authStore.model; export const logout = () => { pb.authStore.clear(); if (typeof window !== 'undefined') { window.location.href = '/login/'; } }; export const login = async (email: string, password: string) => { return await pb.collection('users').authWithPassword(email, password); }; // PB `order_items` schema (verified live 2026-07-07): // drv_id = relation -> users (canonical "assigned driver" field) // driver = legacy/parallel driver field (only set on 1 row) // supplier = relation -> suppliers (singular) // item_status real values: assigned, collection_pool, pending, // collected, problem, ... // drv_status real values: assigned, en_route, collected, failed, '' (empty), // unassigned // po_description = supplier-facing line ("M20×40 nut sku 11243…") // description = customer-facing line // order_qty = supplier-facing qty (falls back to quantity) // quantity = customer-facing qty // in_stock = NOT IN PB YET — add before relying on it // assigned_at = ISO timestamp, populated server-side on assignment // problem_notes = driver-written audit trail of failed collections // Unknown status values are NOT rejected by PB — they silently match // nothing. Always use the exact string below. // ---------- Hydration helper ---------- // PB query builder can't always do the supplier expand (some deployments // strip expand support). This helper takes raw records and, if supplier // isn't already expanded, batch-fetches suppliers by id and stitches // them back into item.expand.supplier. Shared by every fetch below. const hydrateSuppliers = async (items: OrderItem[]): Promise => { const missing = Array.from(new Set( items .filter((it) => !it.expand?.supplier && (it as any).supplier) .map((it) => (it as any).supplier as string) )); if (missing.length === 0) return items; let supplierMap = new Map(); try { const filter = missing.map((id) => `id = "${id}"`).join(' || '); const suppliers = await pb.collection('suppliers').getFullList({ filter }); supplierMap = new Map(suppliers.map((s: any) => [s.id, s])); } catch { return items; } return items.map((it) => { if (it.expand?.supplier) return it; const supplierId = (it as any).supplier; const supplier = supplierId ? supplierMap.get(supplierId) : null; if (!supplier) return it; return { ...it, expand: { ...(it.expand || {}), supplier }, } as OrderItem; }); }; // ---------- 1. ASSIGNED — only items with drv_id = current user ---------- // Filter uses `drv_id` (the canonical assigned-driver relation), not // `driver` (which is a legacy/parallel field only set on a handful of // rows). drv_id is the field per the project brief (2026-07-07). export const getMyAssignedItems = async (): Promise => { const user = getCurrentUser(); if (!user) return []; try { const items = await pb.collection('order_items').getFullList({ filter: `drv_id = "${user.id}" && item_status = "assigned" && ready_for_collection = true`, expand: 'supplier,order,purchase_order', sort: 'supplier.suppliername', }); return items; } catch { const items = await pb.collection('order_items').getFullList({ filter: `drv_id = "${user.id}" && item_status = "assigned" && ready_for_collection = true`, }); return hydrateSuppliers(items); } }; // Backwards-compat alias — keep old call sites working during the refactor. export const getAssignedCollections = getMyAssignedItems; // ---------- 2. POOL — every collection_pool item in the system ---------- // Pool means "open for any driver to claim". The driver app shows ALL // pool rows here so the driver can see what's available before claiming. // No driver filter on purpose. export const getPoolItems = async (): Promise => { try { const items = await pb.collection('order_items').getFullList({ filter: `item_status = "collection_pool" && ready_for_collection = true`, expand: 'supplier,order,purchase_order', sort: 'supplier.suppliername', }); return items; } catch { const items = await pb.collection('order_items').getFullList({ filter: `item_status = "collection_pool" && ready_for_collection = true`, }); return hydrateSuppliers(items); } }; // ---------- 3. PROBLEMS — every flagged item in the system ---------- // Items with problem_flag=true OR item_status='problem'. No driver // filter — problems are operational visibility, not personal lists. export const getProblemItems = async (): Promise => { try { const items = await pb.collection('order_items').getFullList({ filter: `problem_flag = true || item_status = "problem"`, expand: 'supplier,order,purchase_order', sort: '-updated', }); return items; } catch { const items = await pb.collection('order_items').getFullList({ filter: `problem_flag = true || item_status = "problem"`, }); return hydrateSuppliers(items); } }; // Update order item status when collected export const updateCollectionStatus = async ( id: string, status: string, data: { qty_collected?: number; notes?: string; collected_date?: string; problem_type?: string; problem_flag?: boolean } ) => { return await pb.collection('order_items').update(id, { item_status: status, ...data, }); }; // ---------- Display helpers ---------- // Per the project brief: // po_description is the supplier-facing line (preferred on the PO // pickup screen); description is the customer-facing line. We always // show the supplier-facing line when it's there, and fall back to the // customer-facing one for legacy rows. export const getItemDisplayDescription = (item: OrderItem): string => { const po = (item.po_description || '').trim(); if (po) return po; return (item.description || '').trim() || '—'; }; // order_qty is the supplier-facing quantity (preferred on the pickup // screen); quantity is the customer-facing quantity. Per the brief: // order_qty "falls back to quantity if blank". We honor that order. export const getItemDisplayQty = (item: OrderItem): number => { const oq = item.order_qty; if (typeof oq === 'number' && !Number.isNaN(oq) && oq > 0) return oq; const q = item.quantity; if (typeof q === 'number' && !Number.isNaN(q) && q > 0) return q; return 0; }; // ---------- Driver-side claim action ---------- // When the driver sees an open item in Pool and wants to take it: // set drv_id = me, item_status = "assigned", assigned_at = now. export const claimItemForDriver = async (itemId: string) => { const user = getCurrentUser(); if (!user) throw new Error('Not signed in'); return await pb.collection('order_items').update(itemId, { drv_id: user.id, item_status: 'assigned', drv_status: 'assigned', assigned_at: new Date().toISOString(), }); }; // Report a problem on an order item export const reportProblem = async ( orderItemId: string, problemType: string, description: string ) => { return await pb.collection('order_items').update(orderItemId, { item_status: 'problem', problem_type: problemType, problem_flag: true, problem_notes: description, }); }; // Location tracking - tries to save, fails silently if table doesn't exist export const saveDriverLocation = async (location: Omit) => { try { return await pb.collection('driver_locations').create(location); } catch { // Silently fail - table may not exist yet } }; // Get driver's location trail for today (for map polyline) export const getDriverTrail = async (driverId: string) => { try { const today = new Date(); today.setHours(0, 0, 0, 0); const records = await pb.collection('driver_locations').getFullList({ filter: `driver = "${driverId}" && timestamp >= "${today.toISOString()}"`, sort: 'timestamp', }); return records as unknown as DriverLocation[]; } catch { return []; } }; // Settings helpers const SETTINGS_KEY = '1driver_settings'; export const getSettings = (): AppSettings => { if (typeof window === 'undefined') return DEFAULT_SETTINGS; const saved = localStorage.getItem(SETTINGS_KEY); if (saved) { try { return { ...DEFAULT_SETTINGS, ...JSON.parse(saved) }; } catch { return DEFAULT_SETTINGS; } } return DEFAULT_SETTINGS; }; export const saveSettings = (settings: Partial) => { if (typeof window === 'undefined') return; const current = getSettings(); const updated = { ...current, ...settings }; localStorage.setItem(SETTINGS_KEY, JSON.stringify(updated)); return updated; };