export interface Supplier { id: string; suppliername: string; email: string; suppliernumber: string; supplieraddress: string; supply_tags: string; coordinates: string; // Raw coordinates text (e.g. "-33.9249, 18.4241") closingtime: string; } export interface Order { id: string; order_number: string; status: string; notes: string; } export interface PurchaseOrder { id: string; po_number: string; status: string; // Attached PO PDF documents (file field on the purchase_orders PB collection) documents?: string[]; // Stock orders only: photos on the stock purchase (driver's invoice // photos, operator uploads). Multi-file since pb:migrate:stock-photos. additional_attachments?: string[]; // Pre-composed email payload (subject, body, to) — driver can resend // this to the supplier via the "Email PO" button when the supplier // hasn't received it. email_payload?: { to: string; subject: string; body: string; bodyText: string; cc?: string; bcc?: string; }; // Linked supplier (expanded) expand?: { supplier?: Supplier; }; supplier?: string; total_amount?: number; order?: string; order_item?: string; updated?: string; } // Discriminator that tags a row as either an `order_items` row // (the original Assigned-tab scope) or a `stock_purchase_items` // row merged into the same list with the same stop shape. // Default for legacy rows is 'order' — components can treat any // row without an explicit kind as an order row. export type StopKind = 'order' | 'pickup'; // Raw PocketBase row shape for `stock_purchase_items`. Mirrors the // schema verified live 2026-08-17: // drv_id, status (booked | collection_pool | assigned_to_driver | // collected | ''), purchase (relation -> purchases), // stock_item (relation -> stock_items), quantity (expected), // quantity_received (audit), description (line), sku, // supplier_sku, unit, cost_price, problem_flag/problem_type/ // problem_notes, assigned_at, notes. // // The driver app never displays this raw shape — see StockPickupItem // below for the merged view that matches OrderItem's shape so the // existing card/modal/map components render stock pickups unchanged. export interface StockPurchaseItem { id: string; purchase: string; stock_item: string; drv_id?: string; // Status field names line up with the operator-side flow. The // driver-app driver-side analog is `item_status` — we map // status="assigned_to_driver" -> item_status="assigned" so the // existing chip counts and filters keep working. status: 'booked' | 'collection_pool' | 'assigned_to_driver' | 'collected' | '' | string; description: string; sku?: string; supplier_sku?: string; unit?: string; cost_price?: number; quantity: number; quantity_received?: number; notes?: string; problem_flag?: boolean; problem_type?: string; problem_notes?: string; assigned_at?: string; expand?: { purchase?: PurchaseOrder; stock_item?: StockItem; }; } // `stock_items` row shape — needed only to type the expand side of // `stock_purchase_items.expand.stock_item`. The driver app reads // `stock_item.name` (display name) and `stock_item.sku` from here. export interface StockItem { id: string; name: string; description?: string; sku?: string; unit?: string; selling_price?: number; cost_price?: number; } export interface OrderItem { // 'order' for the original order_items rows, 'pickup' for stock // pickups merged from stock_purchase_items. Components that don't // write (card/map/modal reads) can ignore this entirely. kind?: StopKind; id: string; order: string; // Customer-facing line. Use this for label / list / customer view. description: string; // Supplier-facing line. Use this on the PO pickup screen — supplier // paperwork references this exact wording. Falls back to description. po_description?: string; // Customer-side quantity. Nullable on legacy rows. quantity: number; // Supplier-side quantity. Per the brief, falls back to quantity when // blank (zero is treated as blank in getItemDisplayQty). order_qty?: number; // Pricing — operator-side (cost), customer-side (selling), and audit // source ('quote' | 'catalogue' | 'manual'). cost_price?: number; selling_price?: number | null; pricing_source?: 'quote' | 'catalogue' | 'manual' | string; matched_quote_item?: string; // Supplier part number. Use this on PO paperwork over description. sku?: string; // Workflow states. item_status is the operator-side state, drv_status // is the driver-side state. They're independent enums — an item can // be item_status='collected' (operator marked it received) while // drv_status is still 'en_route' (driver hasn't checked in), etc. item_status: | 'pending' | 'processed' | 'po_generated' | 'collection_pool' | 'assigned' | 'collected' | 'stock_verified' | 'picking' | 'ready_delivery' | 'delivered' | 'cancelled' | 'invoiced' | 'problem' | string; drv_status?: 'unassigned' | 'assigned' | 'en_route' | 'collected' | 'failed' | string; driver_status?: string; delivery_status?: 'pending' | 'ready' | 'in_transit' | 'delivered' | 'failed'; invoice_status?: 'pending' | 'invoiced' | 'paid' | 'cancelled'; // Driver assignment. The canonical driver field per the brief is // `drv_id` (relation -> users); the older `driver` field is a legacy // shadow that only a handful of rows set. Use drv_id in every filter. drv_id?: string; driver?: string; // Server-side ISO timestamp of when drv_id was set. Surface in detail. assigned_at?: string; // Audit trail of failed-collection attempts. The driver writes here. // Distinct from notes (operator-side). notes?: string; problem_notes?: string; problem_flag?: boolean; problem_type?: 'stock' | 'supplier' | 'driver' | 'delivery' | 'pricing' | 'other' | string; // Expanded relations populated by the PB `expand` query parameter // (supplier, order, purchase_order). PocketBase attaches this object // at fetch time; the driver app reads it as `item.expand?.supplier` // etc. Not a stored column — synthesized client-side. expand?: { supplier?: Supplier; order?: Order; purchase_order?: PurchaseOrder; }; // Pickup proof files. Two storage paths exist in PB; the runtime // currently writes to the legacy file field on the order_items row // (see `uploadOrderItemAttachment` in pocketbase.ts, which appends // to `order_item_attachements+` with the `+` so each upload is // preserved alongside previous ones rather than overwriting). The // separate `order_item_attachments` collection (one row per file, // schema documented near the bottom of this file) exists in PB and // is exercised by the e2e probes, but the driver app does not write // to it yet — migration is in flight, not complete. The misspelling // `order_item_attachements` is the literal PB field name; do not // "fix" it without a coordinated PB migration. // (The legacy single-photo `collection_photo` slot is also still // declared on the row but no longer written by the runtime — see // handleCollect passing `photo: null` for that reason.) order_item_attachements?: string[]; // Per-row quantities used for proving/dashboard math. qty_processed?: number; qty_assigned?: number; qty_collected?: number; qty_delivered?: number; drv_assigned_at?: string; drv_collected_at?: string; delivered_at?: string; markup?: number; markup_percent?: number; legacy_order_item_id?: string; // Backwards-compat shim the app uses in some spots is_stock_item?: boolean; collected_date?: string; collection_date?: string; } export interface DriverLocation { id: string; // PB schema note (verified live 2026-07-08): the relation field on // `driver_locations` is named `relation`, NOT `driver`. Earlier docs // and the type had this as `driver` — every save silently 400'd inside // a try{}catch{} and the map polyline was always empty. relation: string; latitude: number; longitude: number; accuracy?: number; altitude?: number; speed?: number; heading?: number; timestamp: string; } // Attached files for a single order_item — photos taken at pickup, // delivery note scans, supplier-side signed slips, etc. Created // alongside the order_item row when a driver collects the item and // optionally attaches a photo. Mirrors the `quote_item_attachments` // schema (one row per file, parent via relation). // // PB collection verified 2026-07-07: order_item_attachments // - order_item : relation -> order_items (cascadeDelete, required) // - filename : text (required, max 500) // - file : file (image/* + application/pdf, 20MB max) // - content_type : text (optional, e.g. "image/jpeg") // - size_bytes : number (optional) // - source : select (driver_proof | operator_proof | manual) // - notes : text (optional) // - uploaded_at : autodate (server-side) export type AttachmentSource = 'driver_proof' | 'operator_proof' | 'manual'; export interface OrderItemAttachment { id: string; order_item: string; filename: string; file: string; content_type?: string; size_bytes?: number; source: AttachmentSource; notes?: string; uploaded_at: string; expand?: { order_item?: OrderItem; }; } export interface AppSettings { locationInterval: number; minimumAccuracy: number; enableHighAccuracy: boolean; darkMode: boolean; fontSize: 'small' | 'normal' | 'large'; offlineMode: boolean; /** Beta: match an invoice photo and collect exact matches. On by default. */ aiCheckEnabled: boolean; /** WFDASH base URL the helper calls; empty uses the built-in address. */ aiCheckUrl: string; /** * Bumped when the invoice match default changes. A phone saved before the * match became default-on gets it switched on once; a later "off" sticks. */ aiCheckDefaultVersion: number; } export const AI_CHECK_DEFAULT_VERSION = 1; export const DEFAULT_SETTINGS: AppSettings = { locationInterval: 60, minimumAccuracy: 50, enableHighAccuracy: true, darkMode: false, fontSize: 'normal', offlineMode: false, aiCheckEnabled: true, aiCheckUrl: '', aiCheckDefaultVersion: AI_CHECK_DEFAULT_VERSION, }; export const PROBLEM_TYPES = [ { value: 'out_of_stock', label: 'Out of Stock' }, { value: 'partial_stock', label: 'Partial Stock' }, { value: 'quality_issue', label: 'Quality Issue' }, { value: 'wrong_item', label: 'Wrong Item' }, { value: 'other', label: 'Other' }, ] as const; /** * Stored `problem_type` -> the word we show the driver. * * The union is wider than PROBLEM_TYPES on purpose. `problem_type` is a * PB **select** on `order_items` whose allowed values are * stock | supplier | driver | delivery | pricing | other * plus the four PROBLEM_TYPES entries we added on 2026-10-14. On * `stock_purchase_items` the same field is a plain **text** field, so * anything can turn up there too. `collectionStatus`/`updateStockCollectionStatus` * write `stock` on their own. A generic "Problem" pill told the driver * nothing about which of these they were looking at — an out-of-stock * item and a quality complaint need different follow-ups. */ export const PROBLEM_TYPE_LABELS: Record = { out_of_stock: 'Out of stock', partial_stock: 'Partial stock', quality_issue: 'Quality issue', wrong_item: 'Wrong item', stock: 'Short / partial', supplier: 'Supplier issue', driver: 'Driver issue', delivery: 'Delivery issue', pricing: 'Pricing issue', other: 'Other', }; /** Badge text for a flagged item; falls back to the old generic label. */ export const problemBadgeLabel = (item: { problem_flag?: boolean | null; problem_type?: string | null; }): string => { if (!item.problem_flag) return ''; return PROBLEM_TYPE_LABELS[item.problem_type ?? ''] ?? 'Problem'; }; /** * True only for a flagged row the driver reported as out of stock. * * Deliberately narrow: `stock` (partial collection) and the other * operator-side types do NOT qualify, because the driver can't clear * those and they don't block a re-collection the same way. */ export const isOutOfStock = (item: { problem_flag?: boolean | null; problem_type?: string | null; }): boolean => Boolean(item.problem_flag) && item.problem_type === 'out_of_stock';