2026-05-24 19:51:29 +00:00
|
|
|
|
import PocketBase from 'pocketbase';
|
2026-07-07 23:35:26 +00:00
|
|
|
|
import {
|
|
|
|
|
|
AppSettings,
|
|
|
|
|
|
DEFAULT_SETTINGS,
|
|
|
|
|
|
OrderItem,
|
|
|
|
|
|
OrderItemAttachment,
|
|
|
|
|
|
AttachmentSource,
|
|
|
|
|
|
DriverLocation,
|
|
|
|
|
|
} from './types';
|
2026-07-07 18:43:17 +00:00
|
|
|
|
import { clearCache as clearItemsCache } from './itemsCache';
|
2026-05-24 19:51:29 +00:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
Add PWA support and UI redesign
Add full PWA support (icons, manifest, service worker, pwa lib and bridge) and introduce install/offline UI. Generate app icons via a new scripts/generate-icons.js and include multiple icon sizes in public/icons; update manifest and layout head meta/preconnect. Revamp global styles (globals.css) with new utilities, animations, spacing for bottom nav, card shadows, skeletons and accessibility tweaks. Polish major screens (login, collections, map, settings) with improved headers, loading states, touch/press feedback, and layout adjustments; add new components (InstallPrompt, OfflineIndicator, PWABridge) and update various components to match new visual/UX patterns. Update Dockerfile to generate icons and use Next.js static export output (/out) for nginx, and bump package setup to support icon generation.
2026-05-26 22:34:50 +00:00
|
|
|
|
// Disable auto-cancellation so React re-renders don't kill in-flight requests
|
|
|
|
|
|
pb.autoCancellation(false);
|
|
|
|
|
|
|
2026-05-24 19:51:29 +00:00
|
|
|
|
// Auth helpers
|
|
|
|
|
|
export const isAuthenticated = () => pb.authStore.isValid;
|
|
|
|
|
|
|
|
|
|
|
|
export const getCurrentUser = () => pb.authStore.model;
|
|
|
|
|
|
|
|
|
|
|
|
export const logout = () => {
|
|
|
|
|
|
pb.authStore.clear();
|
2026-07-07 18:43:17 +00:00
|
|
|
|
// Drop cached items so the next driver who logs in on this
|
|
|
|
|
|
// device doesn't briefly see the previous driver's list.
|
|
|
|
|
|
clearItemsCache();
|
2026-05-24 19:51:29 +00:00
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
|
window.location.href = '/login/';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const login = async (email: string, password: string) => {
|
2026-05-24 22:47:47 +00:00
|
|
|
|
return await pb.collection('users').authWithPassword(email, password);
|
2026-05-24 19:51:29 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-07 23:35:26 +00:00
|
|
|
|
// PB `order_items` schema (verified live 2026-07-08):
|
2026-07-07 15:15:42 +00:00
|
|
|
|
// 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
|
2026-07-07 23:35:26 +00:00
|
|
|
|
// ready_for_collection = NOT IN PB YET (2026-07-08) — do NOT filter on it;
|
|
|
|
|
|
// referencing any field PB doesn't have throws 400.
|
|
|
|
|
|
// item_status alone ("assigned" / "collection_pool")
|
|
|
|
|
|
// is sufficient for the current driver/pool queries.
|
2026-07-07 15:15:42 +00:00
|
|
|
|
// 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<OrderItem[]> => {
|
|
|
|
|
|
const missing = Array.from(new Set(
|
refactor(ui): rebrand app, fix supplier naming
- rename `supplier` field to `suppliers` in OrderItem interface and PocketBase expand queries
- complete visual rebrand to warm cream (#f2f1ed) and dark brown (#26251e) color palette with custom CSS variables
- add accessibility improvements: skip-to-main content link, focus-visible styles, reduced motion support, and proper ARIA labels
- update all component styling to match new theme, including offline indicator, install prompt, login page, modals, map, bottom nav and global CSS
- standardize status colors: use green for success, red for errors, retain orange (#f54e00) as primary accent CTAs
2026-07-05 20:32:20 +00:00
|
|
|
|
items
|
2026-07-07 15:15:42 +00:00
|
|
|
|
.filter((it) => !it.expand?.supplier && (it as any).supplier)
|
|
|
|
|
|
.map((it) => (it as any).supplier as string)
|
refactor(ui): rebrand app, fix supplier naming
- rename `supplier` field to `suppliers` in OrderItem interface and PocketBase expand queries
- complete visual rebrand to warm cream (#f2f1ed) and dark brown (#26251e) color palette with custom CSS variables
- add accessibility improvements: skip-to-main content link, focus-visible styles, reduced motion support, and proper ARIA labels
- update all component styling to match new theme, including offline indicator, install prompt, login page, modals, map, bottom nav and global CSS
- standardize status colors: use green for success, red for errors, retain orange (#f54e00) as primary accent CTAs
2026-07-05 20:32:20 +00:00
|
|
|
|
));
|
2026-07-07 15:15:42 +00:00
|
|
|
|
if (missing.length === 0) return items;
|
refactor(ui): rebrand app, fix supplier naming
- rename `supplier` field to `suppliers` in OrderItem interface and PocketBase expand queries
- complete visual rebrand to warm cream (#f2f1ed) and dark brown (#26251e) color palette with custom CSS variables
- add accessibility improvements: skip-to-main content link, focus-visible styles, reduced motion support, and proper ARIA labels
- update all component styling to match new theme, including offline indicator, install prompt, login page, modals, map, bottom nav and global CSS
- standardize status colors: use green for success, red for errors, retain orange (#f54e00) as primary accent CTAs
2026-07-05 20:32:20 +00:00
|
|
|
|
|
|
|
|
|
|
let supplierMap = new Map<string, any>();
|
2026-07-07 15:15:42 +00:00
|
|
|
|
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;
|
refactor(ui): rebrand app, fix supplier naming
- rename `supplier` field to `suppliers` in OrderItem interface and PocketBase expand queries
- complete visual rebrand to warm cream (#f2f1ed) and dark brown (#26251e) color palette with custom CSS variables
- add accessibility improvements: skip-to-main content link, focus-visible styles, reduced motion support, and proper ARIA labels
- update all component styling to match new theme, including offline indicator, install prompt, login page, modals, map, bottom nav and global CSS
- standardize status colors: use green for success, red for errors, retain orange (#f54e00) as primary accent CTAs
2026-07-05 20:32:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return items.map((it) => {
|
2026-07-07 15:15:42 +00:00
|
|
|
|
if (it.expand?.supplier) return it;
|
refactor(ui): rebrand app, fix supplier naming
- rename `supplier` field to `suppliers` in OrderItem interface and PocketBase expand queries
- complete visual rebrand to warm cream (#f2f1ed) and dark brown (#26251e) color palette with custom CSS variables
- add accessibility improvements: skip-to-main content link, focus-visible styles, reduced motion support, and proper ARIA labels
- update all component styling to match new theme, including offline indicator, install prompt, login page, modals, map, bottom nav and global CSS
- standardize status colors: use green for success, red for errors, retain orange (#f54e00) as primary accent CTAs
2026-07-05 20:32:20 +00:00
|
|
|
|
const supplierId = (it as any).supplier;
|
|
|
|
|
|
const supplier = supplierId ? supplierMap.get(supplierId) : null;
|
|
|
|
|
|
if (!supplier) return it;
|
|
|
|
|
|
return {
|
|
|
|
|
|
...it,
|
|
|
|
|
|
expand: { ...(it.expand || {}), supplier },
|
2026-07-07 15:15:42 +00:00
|
|
|
|
} as OrderItem;
|
refactor(ui): rebrand app, fix supplier naming
- rename `supplier` field to `suppliers` in OrderItem interface and PocketBase expand queries
- complete visual rebrand to warm cream (#f2f1ed) and dark brown (#26251e) color palette with custom CSS variables
- add accessibility improvements: skip-to-main content link, focus-visible styles, reduced motion support, and proper ARIA labels
- update all component styling to match new theme, including offline indicator, install prompt, login page, modals, map, bottom nav and global CSS
- standardize status colors: use green for success, red for errors, retain orange (#f54e00) as primary accent CTAs
2026-07-05 20:32:20 +00:00
|
|
|
|
});
|
2026-05-24 19:51:29 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-07 15:15:42 +00:00
|
|
|
|
// ---------- 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<OrderItem[]> => {
|
|
|
|
|
|
const user = getCurrentUser();
|
|
|
|
|
|
if (!user) return [];
|
|
|
|
|
|
try {
|
|
|
|
|
|
const items = await pb.collection('order_items').getFullList<OrderItem>({
|
2026-07-07 23:35:26 +00:00
|
|
|
|
filter: `drv_id = "${user.id}" && item_status = "assigned"`,
|
2026-07-07 15:15:42 +00:00
|
|
|
|
expand: 'supplier,order,purchase_order',
|
|
|
|
|
|
sort: 'supplier.suppliername',
|
|
|
|
|
|
});
|
|
|
|
|
|
return items;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
const items = await pb.collection('order_items').getFullList<OrderItem>({
|
2026-07-07 23:35:26 +00:00
|
|
|
|
filter: `drv_id = "${user.id}" && item_status = "assigned"`,
|
2026-07-07 15:15:42 +00:00
|
|
|
|
});
|
|
|
|
|
|
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<OrderItem[]> => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const items = await pb.collection('order_items').getFullList<OrderItem>({
|
2026-07-07 23:35:26 +00:00
|
|
|
|
filter: `item_status = "collection_pool"`,
|
2026-07-07 15:15:42 +00:00
|
|
|
|
expand: 'supplier,order,purchase_order',
|
|
|
|
|
|
sort: 'supplier.suppliername',
|
|
|
|
|
|
});
|
|
|
|
|
|
return items;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
const items = await pb.collection('order_items').getFullList<OrderItem>({
|
2026-07-07 23:35:26 +00:00
|
|
|
|
filter: `item_status = "collection_pool"`,
|
2026-07-07 15:15:42 +00:00
|
|
|
|
});
|
|
|
|
|
|
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<OrderItem[]> => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const items = await pb.collection('order_items').getFullList<OrderItem>({
|
|
|
|
|
|
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<OrderItem>({
|
|
|
|
|
|
filter: `problem_flag = true || item_status = "problem"`,
|
|
|
|
|
|
});
|
|
|
|
|
|
return hydrateSuppliers(items);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-07 18:43:17 +00:00
|
|
|
|
// Update order item status when collected. This is the canonical
|
|
|
|
|
|
// driver-side collection writer — the dashboard's
|
|
|
|
|
|
// `/api/v1/order-items/[id]/status` route covers the same patch from
|
|
|
|
|
|
// the operator side, so keep the field set aligned.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Status flow on collection:
|
|
|
|
|
|
// - Always sets:
|
|
|
|
|
|
// * `item_status = "collected"` (operator-side)
|
|
|
|
|
|
// * `drv_status = "collected"` (driver-side status machine)
|
|
|
|
|
|
// * `drv_collected_at = now` (timestamp of physical pickup)
|
|
|
|
|
|
// * `qty_collected = receivedQty` (audit: actual qty picked up)
|
|
|
|
|
|
// - Always sets `collection_photo` if a file was supplied.
|
|
|
|
|
|
// - Quantity-mismatch behaviour:
|
|
|
|
|
|
// * Full (received === expected) → no extra flags. Clean collection.
|
|
|
|
|
|
// * Partial (0 < received < expected) → `problem_flag = true`,
|
|
|
|
|
|
// `problem_type = "stock"`, and a stock-shortage note in
|
|
|
|
|
|
// `problem_notes`. The operator still sees the item as
|
|
|
|
|
|
// collected but the problem flag surfaces it in the Problems
|
|
|
|
|
|
// tab so a follow-up can be planned.
|
|
|
|
|
|
// * Over (received > expected) → `problem_flag = true` and a
|
|
|
|
|
|
// `problem_notes` entry explaining the overage. No
|
|
|
|
|
|
// `problem_type` because it's not a stock issue — the supplier
|
|
|
|
|
|
// gave more than ordered, which can mean a packing error or a
|
|
|
|
|
|
// promo. We leave the type empty so the operator picks.
|
|
|
|
|
|
//
|
|
|
|
|
|
// The helper intentionally doesn't compute `expected` itself — the
|
|
|
|
|
|
// caller passes `expectedQty` so the source of truth for "what was
|
|
|
|
|
|
// ordered" stays at the call site (where the same number was just
|
|
|
|
|
|
// shown to the driver in the modal).
|
2026-05-24 19:51:29 +00:00
|
|
|
|
export const updateCollectionStatus = async (
|
|
|
|
|
|
id: string,
|
|
|
|
|
|
status: string,
|
2026-07-07 18:43:17 +00:00
|
|
|
|
data: {
|
|
|
|
|
|
qty_collected?: number;
|
|
|
|
|
|
notes?: string;
|
|
|
|
|
|
collected_date?: string;
|
|
|
|
|
|
photo?: File | null;
|
|
|
|
|
|
expectedQty?: number;
|
|
|
|
|
|
problem_type?: string;
|
|
|
|
|
|
problem_flag?: boolean;
|
|
|
|
|
|
}
|
2026-05-24 19:51:29 +00:00
|
|
|
|
) => {
|
2026-07-07 18:43:17 +00:00
|
|
|
|
const patch: Record<string, unknown> = {
|
2026-05-24 22:47:47 +00:00
|
|
|
|
item_status: status,
|
2026-07-07 18:43:17 +00:00
|
|
|
|
drv_status: 'collected',
|
|
|
|
|
|
drv_collected_at: new Date().toISOString(),
|
|
|
|
|
|
qty_collected: data.qty_collected,
|
|
|
|
|
|
collected_date: data.collected_date ?? new Date().toISOString(),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Only forward the optional photo if a file was actually attached.
|
|
|
|
|
|
// Passing `null`/undefined to a PB file field would clear an existing
|
|
|
|
|
|
// attachment on subsequent updates, which we don't want for a
|
|
|
|
|
|
// re-collect after the first one.
|
|
|
|
|
|
if (data.photo) patch.collection_photo = data.photo;
|
|
|
|
|
|
|
|
|
|
|
|
// Notes field keeps the driver's free-text commentary.
|
|
|
|
|
|
if (data.notes !== undefined) patch.notes = data.notes;
|
|
|
|
|
|
|
|
|
|
|
|
// Quantity-mismatch handling — see the doc-comment above.
|
|
|
|
|
|
const received = typeof data.qty_collected === 'number' ? data.qty_collected : 0;
|
|
|
|
|
|
const expected = typeof data.expectedQty === 'number' ? data.expectedQty : 0;
|
|
|
|
|
|
if (expected > 0 && received > 0 && received < expected) {
|
|
|
|
|
|
// Partial — short pickup. Flag so it surfaces in Problems.
|
|
|
|
|
|
patch.problem_flag = true;
|
|
|
|
|
|
patch.problem_type = 'stock';
|
|
|
|
|
|
const userNote = (data.notes ?? '').trim();
|
|
|
|
|
|
const autoNote = `Partial collection: ${received} of ${expected} units`;
|
|
|
|
|
|
patch.problem_notes = userNote ? `${autoNote} — ${userNote}` : autoNote;
|
|
|
|
|
|
} else if (expected > 0 && received > expected) {
|
|
|
|
|
|
// Over-collection — supplier gave more than ordered. Flag without
|
|
|
|
|
|
// a stock type so the operator picks the right follow-up.
|
|
|
|
|
|
patch.problem_flag = true;
|
|
|
|
|
|
patch.problem_type = '';
|
|
|
|
|
|
const userNote = (data.notes ?? '').trim();
|
|
|
|
|
|
const autoNote = `Over collection: ${received} units received against ${expected} ordered`;
|
|
|
|
|
|
patch.problem_notes = userNote ? `${autoNote} — ${userNote}` : autoNote;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return await pb.collection('order_items').update(id, patch);
|
2026-05-24 19:51:29 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-07 23:35:26 +00:00
|
|
|
|
// ---------- Attachments ----------
|
|
|
|
|
|
// Upload a file (typically a pickup photo) as an OrderItemAttachment
|
|
|
|
|
|
// row in PB's `order_item_attachments` collection. Mirrors the
|
|
|
|
|
|
// quote_item_attachments flow: parent via the `order_item` relation,
|
|
|
|
|
|
// file carried in the `file` field, plus content_type / size_bytes
|
|
|
|
|
|
// for the operator side to surface in their list view.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Returns the new attachment record. Throws on PB errors so the
|
|
|
|
|
|
// caller can roll back the status change (otherwise the driver
|
|
|
|
|
|
// sees a "Collected" toast while the photo is lost).
|
|
|
|
|
|
export const uploadOrderItemAttachment = async (params: {
|
|
|
|
|
|
orderItemId: string;
|
|
|
|
|
|
file: File;
|
|
|
|
|
|
source?: AttachmentSource;
|
|
|
|
|
|
notes?: string;
|
|
|
|
|
|
}): Promise<OrderItemAttachment> => {
|
|
|
|
|
|
const { orderItemId, file, source = 'driver_proof', notes } = params;
|
|
|
|
|
|
|
|
|
|
|
|
// Build a FormData payload. PB's create() accepts a plain object
|
|
|
|
|
|
// with the file property, which it then serializes as multipart.
|
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
|
formData.append('order_item', orderItemId);
|
|
|
|
|
|
formData.append('filename', file.name);
|
|
|
|
|
|
formData.append('file', file);
|
|
|
|
|
|
formData.append('content_type', file.type || '');
|
|
|
|
|
|
if (typeof file.size === 'number') {
|
|
|
|
|
|
formData.append('size_bytes', String(file.size));
|
|
|
|
|
|
}
|
|
|
|
|
|
formData.append('source', source);
|
|
|
|
|
|
if (notes) formData.append('notes', notes);
|
|
|
|
|
|
|
|
|
|
|
|
const created = await pb.collection('order_item_attachments').create(formData);
|
|
|
|
|
|
return created as unknown as OrderItemAttachment;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// List attachments for a single order item. Newest first.
|
|
|
|
|
|
export const listOrderItemAttachments = async (
|
|
|
|
|
|
orderItemId: string
|
|
|
|
|
|
): Promise<OrderItemAttachment[]> => {
|
|
|
|
|
|
const records = await pb.collection('order_item_attachments').getFullList({
|
|
|
|
|
|
filter: `order_item = "${orderItemId}"`,
|
|
|
|
|
|
sort: '-uploaded_at',
|
|
|
|
|
|
});
|
|
|
|
|
|
return records as unknown as OrderItemAttachment[];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-07 15:15:42 +00:00
|
|
|
|
// ---------- 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(),
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-24 22:47:47 +00:00
|
|
|
|
// Report a problem on an order item
|
2026-05-24 19:51:29 +00:00
|
|
|
|
export const reportProblem = async (
|
2026-05-24 22:47:47 +00:00
|
|
|
|
orderItemId: string,
|
2026-05-24 19:51:29 +00:00
|
|
|
|
problemType: string,
|
|
|
|
|
|
description: string
|
|
|
|
|
|
) => {
|
2026-05-24 22:47:47 +00:00
|
|
|
|
return await pb.collection('order_items').update(orderItemId, {
|
|
|
|
|
|
item_status: 'problem',
|
2026-05-24 19:51:29 +00:00
|
|
|
|
problem_type: problemType,
|
2026-05-24 22:47:47 +00:00
|
|
|
|
problem_flag: true,
|
|
|
|
|
|
problem_notes: description,
|
2026-05-24 19:51:29 +00:00
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-07 23:35:26 +00:00
|
|
|
|
// Location tracking - tries to save, fails silently if table doesn't exist.
|
|
|
|
|
|
// IMPORTANT: PB schema (verified live 2026-07-08) stores the driver
|
|
|
|
|
|
// relation under `relation`, not `driver`. Writing to `driver` 400s and
|
|
|
|
|
|
// the row is never created — that's why the map polyline has been empty.
|
|
|
|
|
|
// If we ever migrate PB to rename the field back to `driver` (it should
|
|
|
|
|
|
// be — `relation` is a confusing name), update this + getDriverTrail.
|
2026-05-24 19:51:29 +00:00
|
|
|
|
export const saveDriverLocation = async (location: Omit<DriverLocation, 'id'>) => {
|
2026-05-24 20:31:35 +00:00
|
|
|
|
try {
|
2026-05-24 22:47:47 +00:00
|
|
|
|
return await pb.collection('driver_locations').create(location);
|
2026-07-07 23:35:26 +00:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// Log now (was silent) — silent catches are how the `driver`-field
|
|
|
|
|
|
// bug stayed hidden for months.
|
|
|
|
|
|
console.warn('[saveDriverLocation] failed:', e instanceof Error ? e.message : e);
|
2026-05-24 20:31:35 +00:00
|
|
|
|
}
|
2026-05-24 19:51:29 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-07 23:35:26 +00:00
|
|
|
|
// Get driver's location trail for today (for map polyline).
|
2026-05-24 23:33:54 +00:00
|
|
|
|
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({
|
2026-07-07 23:35:26 +00:00
|
|
|
|
filter: `relation = "${driverId}" && timestamp >= "${today.toISOString()}"`,
|
2026-05-24 23:33:54 +00:00
|
|
|
|
sort: 'timestamp',
|
|
|
|
|
|
});
|
|
|
|
|
|
return records as unknown as DriverLocation[];
|
2026-07-07 23:35:26 +00:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn('[getDriverTrail] failed:', e instanceof Error ? e.message : e);
|
2026-05-24 23:33:54 +00:00
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-24 19:51:29 +00:00
|
|
|
|
// 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<AppSettings>) => {
|
|
|
|
|
|
if (typeof window === 'undefined') return;
|
|
|
|
|
|
const current = getSettings();
|
|
|
|
|
|
const updated = { ...current, ...settings };
|
|
|
|
|
|
localStorage.setItem(SETTINGS_KEY, JSON.stringify(updated));
|
|
|
|
|
|
return updated;
|
Add PWA support and UI redesign
Add full PWA support (icons, manifest, service worker, pwa lib and bridge) and introduce install/offline UI. Generate app icons via a new scripts/generate-icons.js and include multiple icon sizes in public/icons; update manifest and layout head meta/preconnect. Revamp global styles (globals.css) with new utilities, animations, spacing for bottom nav, card shadows, skeletons and accessibility tweaks. Polish major screens (login, collections, map, settings) with improved headers, loading states, touch/press feedback, and layout adjustments; add new components (InstallPrompt, OfflineIndicator, PWABridge) and update various components to match new visual/UX patterns. Update Dockerfile to generate icons and use Next.js static export output (/out) for nginx, and bump package setup to support icon generation.
2026-05-26 22:34:50 +00:00
|
|
|
|
};
|