WFDASHDRIVER-APP/lib/types.ts
Dave 30c30ed821 feat: invoice match on by default with the WFDASH address built in
The match never ran on drivers' phones: it was off by default and the
build had no WFDASH address, so photos were stored but nothing collected.
Now it is on for every driver (switched on once for phones saved while
it was off; a later "off" sticks) and falls back to wfdash.apphub.co.za.

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

345 lines
12 KiB
TypeScript

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<string, string> = {
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';