Stock pickup photos were dropped: the Collect window ignored the photo for a pickup, and the PO header's Invoice button didn't show for stock orders (stock_purchase_items has no file field). - Photos for a stock order go on the stock purchase (stock_purchases.additional_attachments, multi-file after WFDASH's `npm run pb:migrate:stock-photos`), always added with `field+`. - Invoice button works on stock orders (pages counted from the stock purchase's invoice_ files); the Collect window uploads a pickup's photo there too, and warns if it didn't upload. - poDocs: one PhotoTarget (order line or stock purchase) behind invoicePhotoTarget / uploadInvoicePage / uploadStockPickupPhoto. Run the WFDASH stock-photos migration before deploying this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
331 lines
12 KiB
TypeScript
331 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;
|
|
}
|
|
|
|
export const DEFAULT_SETTINGS: AppSettings = {
|
|
locationInterval: 60,
|
|
minimumAccuracy: 50,
|
|
enableHighAccuracy: true,
|
|
darkMode: false,
|
|
fontSize: 'normal',
|
|
offlineMode: false,
|
|
};
|
|
|
|
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';
|