diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..f7f15cd Binary files /dev/null and b/.DS_Store differ diff --git a/app/.DS_Store b/app/.DS_Store new file mode 100644 index 0000000..37c505d Binary files /dev/null and b/app/.DS_Store differ diff --git a/app/map/page.tsx b/app/map/page.tsx index 35aed69..84b91a8 100644 --- a/app/map/page.tsx +++ b/app/map/page.tsx @@ -3,15 +3,14 @@ import { useState, useEffect, useCallback, Suspense } from 'react'; import { useRouter } from 'next/navigation'; import dynamic from 'next/dynamic'; -import { Collection } from '@/lib/types'; +import { OrderItem } from '@/lib/types'; import { useAuth } from '@/components/AuthProvider'; import BottomNav from '@/components/BottomNav'; import LocationTracker from '@/components/LocationTracker'; import { getAssignedCollections, isAuthenticated } from '@/lib/pocketbase'; -import { cn } from '@/lib/utils'; +import { cn, parseCoordinates } from '@/lib/utils'; import { RefreshCw, MapPin, AlertCircle } from 'lucide-react'; -// Dynamic import for Leaflet to avoid SSR issues const CollectionMap = dynamic(() => import('@/components/CollectionMap'), { ssr: false, loading: () => ( @@ -24,25 +23,22 @@ const CollectionMap = dynamic(() => import('@/components/CollectionMap'), { export default function MapPage() { const router = useRouter(); const { isLoggedIn, isLoading: authLoading } = useAuth(); - const [collections, setCollections] = useState([]); + const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [error, setError] = useState(''); useEffect(() => { - if (!authLoading && !isLoggedIn) { - router.push('/login/'); - } + if (!authLoading && !isLoggedIn) router.push('/login/'); }, [isLoggedIn, authLoading, router]); - const loadCollections = useCallback(async (showLoading = true) => { + const loadItems = useCallback(async (showLoading = true) => { if (!isAuthenticated()) return; if (showLoading) setIsLoading(true); setError(''); - try { const data = await getAssignedCollections(); - setCollections(data); + setItems(data); } catch (err) { console.error('Failed to load collections:', err); setError('Failed to load collections'); @@ -52,20 +48,13 @@ export default function MapPage() { } }, []); - useEffect(() => { - loadCollections(); - }, [loadCollections]); + useEffect(() => { loadItems(); }, [loadItems]); const handleNavigate = (lat: number, lng: number, name: string) => { const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); - let url: string; - - if (isIOS) { - url = `maps://maps.apple.com/?q=${encodeURIComponent(name)}&ll=${lat},${lng}`; - } else { - url = `https://www.google.com/maps?q=${lat},${lng}`; - } - + const url = isIOS + ? `maps://maps.apple.com/?q=${encodeURIComponent(name)}&ll=${lat},${lng}` + : `https://www.google.com/maps?q=${lat},${lng}`; window.open(url, '_blank'); }; @@ -77,15 +66,12 @@ export default function MapPage() { ); } - if (!isLoggedIn) { - return null; - } + if (!isLoggedIn) return null; return (
- {/* Header */}
@@ -93,27 +79,18 @@ export default function MapPage() {

Collection Map

- - {collections.length} stops - + {items.length} stops
- {/* Map */}
{isLoading ? (
@@ -123,31 +100,21 @@ export default function MapPage() {

{error}

- +
- ) : collections.length === 0 ? ( + ) : items.length === 0 ? (

No collections to show

All collections completed!

) : ( - -
-
- } - > - + +
+
+ }> +
)}
diff --git a/app/page.tsx b/app/page.tsx index de2931c..1686a2f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useRouter } from 'next/navigation'; -import { Collection } from '@/lib/types'; +import { OrderItem } from '@/lib/types'; import { useAuth } from '@/components/AuthProvider'; import BottomNav from '@/components/BottomNav'; import LocationTracker from '@/components/LocationTracker'; @@ -16,7 +16,7 @@ import { isAuthenticated, logout, } from '@/lib/pocketbase'; -import { cn, formatDate } from '@/lib/utils'; +import { cn } from '@/lib/utils'; import { RefreshCw, LogOut, @@ -27,41 +27,38 @@ import { Package, Search, X, - Filter, } from 'lucide-react'; export default function CollectionsPage() { const router = useRouter(); const { isLoggedIn, user, isLoading: authLoading } = useAuth(); - const [collections, setCollections] = useState([]); - const [filteredCollections, setFilteredCollections] = useState([]); + const [items, setItems] = useState([]); + const [filteredItems, setFilteredItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [error, setError] = useState(''); const [searchQuery, setSearchQuery] = useState(''); - const [selectedCollection, setSelectedCollection] = useState(null); + const [selectedItem, setSelectedItem] = useState(null); const [showCollectModal, setShowCollectModal] = useState(false); const [showProblemModal, setShowProblemModal] = useState(false); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null); - const [filter, setFilter] = useState<'all' | 'today' | 'tomorrow'>('all'); + const [filter, setFilter] = useState<'all' | 'today' | 'all_date'>('all'); - // Redirect if not authenticated useEffect(() => { if (!authLoading && !isLoggedIn) { router.push('/login/'); } }, [isLoggedIn, authLoading, router]); - const loadCollections = useCallback(async (showLoading = true) => { + const loadItems = useCallback(async (showLoading = true) => { if (!isAuthenticated()) return; - if (showLoading) setIsLoading(true); setError(''); try { const data = await getAssignedCollections(); - setCollections(data); - setFilteredCollections(data); + setItems(data); + setFilteredItems(data); } catch (err) { console.error('Failed to load collections:', err); setError('Failed to load collections. Please try again.'); @@ -72,22 +69,18 @@ export default function CollectionsPage() { }, []); useEffect(() => { - loadCollections(); - }, [loadCollections]); + loadItems(); + }, [loadItems]); - // Filter collections useEffect(() => { - let filtered = collections; + let filtered = items; - // Search filter if (searchQuery) { const query = searchQuery.toLowerCase(); - filtered = filtered.filter((c) => { - const desc = c.is_manual - ? c.description - : c.expand?.orders_orderitem?.description || ''; - const supplier = c.expand?.wfdash_suppliers?.suppliername || ''; - const orderNum = c.expand?.orders_orderitem?.expand?.orders_order?.order_number || ''; + filtered = filtered.filter((i) => { + const desc = i.description || ''; + const supplier = i.expand?.supplier?.suppliername || ''; + const orderNum = i.expand?.order?.order_number || ''; return ( desc.toLowerCase().includes(query) || supplier.toLowerCase().includes(query) || @@ -96,33 +89,12 @@ export default function CollectionsPage() { }); } - // Date filter - if (filter !== 'all') { - const today = new Date(); - today.setHours(0, 0, 0, 0); - const tomorrow = new Date(today); - tomorrow.setDate(tomorrow.getDate() + 1); - - filtered = filtered.filter((c) => { - if (!c.planned_date) return filter === 'today'; - const planned = new Date(c.planned_date); - planned.setHours(0, 0, 0, 0); - - if (filter === 'today') { - return planned.getTime() === today.getTime(); - } else if (filter === 'tomorrow') { - return planned.getTime() === tomorrow.getTime(); - } - return true; - }); - } - - setFilteredCollections(filtered); - }, [collections, searchQuery, filter]); + setFilteredItems(filtered); + }, [items, searchQuery]); const handleRefresh = async () => { setIsRefreshing(true); - await loadCollections(false); + await loadItems(false); }; const showToast = (message: string, type: 'success' | 'error' | 'warning' = 'success') => { @@ -130,16 +102,15 @@ export default function CollectionsPage() { setTimeout(() => setToast(null), 3000); }; - const handleCollect = async (collection: Collection, receivedQty: number, notes: string) => { + const handleCollect = async (item: OrderItem, receivedQty: number, notes: string) => { try { - await updateCollectionStatus(collection.id, 'collected', { - received_qty: receivedQty, + await updateCollectionStatus(item.id, 'collected', { + qty_collected: receivedQty, notes, - actual_date: new Date().toISOString(), + collected_date: new Date().toISOString(), }); - // Remove from list - setCollections((prev) => prev.filter((c) => c.id !== collection.id)); + setItems((prev) => prev.filter((i) => i.id !== item.id)); setShowCollectModal(false); showToast('Collection completed successfully'); } catch (err) { @@ -149,14 +120,14 @@ export default function CollectionsPage() { }; const handleProblem = async ( - collection: Collection, + item: OrderItem, problemType: string, description: string, photo?: File ) => { try { - await reportProblem(collection.id, problemType, description); - setCollections((prev) => prev.filter((c) => c.id !== collection.id)); + await reportProblem(item.id, problemType, description); + setItems((prev) => prev.filter((i) => i.id !== item.id)); setShowProblemModal(false); showToast('Problem reported successfully', 'warning'); } catch (err) { @@ -167,29 +138,23 @@ export default function CollectionsPage() { const handleNavigate = (lat: number, lng: number, name: string) => { const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); - let url: string; - - if (isIOS) { - url = `maps://maps.apple.com/?q=${encodeURIComponent(name)}&ll=${lat},${lng}`; - } else { - url = `https://www.google.com/maps?q=${lat},${lng}`; - } - + const url = isIOS + ? `maps://maps.apple.com/?q=${encodeURIComponent(name)}&ll=${lat},${lng}` + : `https://www.google.com/maps?q=${lat},${lng}`; window.open(url, '_blank'); }; // Group by supplier - const groupedBySupplier = filteredCollections.reduce((acc, collection) => { - const supplier = collection.expand?.wfdash_suppliers; + const groupedBySupplier = filteredItems.reduce((acc, item) => { + const supplier = item.expand?.supplier; if (!supplier) return acc; - const key = supplier.id; if (!acc[key]) { acc[key] = { supplier, items: [] }; } - acc[key].items.push(collection); + acc[key].items.push(item); return acc; - }, {} as Record['wfdash_suppliers']; items: Collection[] }>); + }, {} as Record['supplier']; items: OrderItem[] }>); const groupedArray = Object.values(groupedBySupplier).sort((a, b) => (a.supplier?.suppliername || '').localeCompare(b.supplier?.suppliername || '') @@ -203,48 +168,30 @@ export default function CollectionsPage() { ); } - if (!isLoggedIn) { - return null; // Will redirect - } + if (!isLoggedIn) return null; return (
- {/* Header */}

Collections

-

- {user?.name || user?.email} -

+

{user?.name || user?.email}

- -
- {/* Search Bar */}
@@ -256,63 +203,33 @@ export default function CollectionsPage() { className="w-full pl-9 pr-9 py-2 bg-white/10 border border-white/20 rounded-xl text-sm text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/30" /> {searchQuery && ( - )}
- {/* Filter Tabs */}
{[ { key: 'all' as const, label: 'All', icon: Package }, - { key: 'today' as const, label: 'Today', icon: Clock }, - { key: 'tomorrow' as const, label: 'Tomorrow', icon: Clock }, ].map(({ key, label, icon: Icon }) => ( ))}
- {/* Content */}
{isLoading ? (
@@ -328,10 +245,7 @@ export default function CollectionsPage() {

{error}

-
@@ -340,45 +254,28 @@ export default function CollectionsPage() {

All caught up!

- {searchQuery - ? 'No collections match your search' - : 'No assigned collections for this period'} + {searchQuery ? 'No collections match your search' : 'No assigned collections'}

) : (
{groupedArray.map(({ supplier, items }) => (
- {/* Supplier Header */}
-

- {supplier!.suppliername} -

- - {items.length} - +

{supplier!.suppliername}

+ {items.length} {supplier!.supplieraddress && ( - - {supplier!.supplieraddress} - + {supplier!.supplieraddress} )}
- - {/* Items */}
- {items.map((collection) => ( + {items.map((item) => ( { - setSelectedCollection(c); - setShowCollectModal(true); - }} - onProblem={(c) => { - setSelectedCollection(c); - setShowProblemModal(true); - }} + key={item.id} + item={item} + onCollect={(i) => { setSelectedItem(i); setShowCollectModal(true); }} + onProblem={(i) => { setSelectedItem(i); setShowProblemModal(true); }} onNavigate={handleNavigate} /> ))} @@ -389,38 +286,28 @@ export default function CollectionsPage() { )}
- {/* Modals */} { - setShowCollectModal(false); - setSelectedCollection(null); - }} + onClose={() => { setShowCollectModal(false); setSelectedItem(null); }} onConfirm={handleCollect} /> { - setShowProblemModal(false); - setSelectedCollection(null); - }} + onClose={() => { setShowProblemModal(false); setSelectedItem(null); }} onConfirm={handleProblem} /> - {/* Toast */} {toast && (
-
+
{toast.message}
diff --git a/components/CollectModal.tsx b/components/CollectModal.tsx index 4bfc7d4..a43d619 100644 --- a/components/CollectModal.tsx +++ b/components/CollectModal.tsx @@ -1,19 +1,19 @@ 'use client'; import { useState } from 'react'; -import { Collection } from '@/lib/types'; +import { OrderItem } from '@/lib/types'; import { cn, formatCurrency } from '@/lib/utils'; import { X, Minus, Plus } from 'lucide-react'; interface CollectModalProps { - collection: Collection | null; + item: OrderItem | null; isOpen: boolean; onClose: () => void; - onConfirm: (collection: Collection, receivedQty: number, notes: string) => void; + onConfirm: (item: OrderItem, receivedQty: number, notes: string) => void; } export default function CollectModal({ - collection, + item, isOpen, onClose, onConfirm, @@ -22,9 +22,9 @@ export default function CollectModal({ const [notes, setNotes] = useState(''); const [error, setError] = useState(''); - if (!isOpen || !collection) return null; + if (!isOpen || !item) return null; - const expectedQty = collection.quantity || collection.expand?.orders_orderitem?.quantity || 0; + const expectedQty = item.order_qty || item.quantity || 0; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -36,7 +36,7 @@ export default function CollectModal({ return; } - onConfirm(collection, qty, notes); + onConfirm(item, qty, notes); setReceivedQty(''); setNotes(''); setError(''); @@ -65,17 +65,18 @@ export default function CollectModal({
-

- {collection.is_manual - ? collection.description - : collection.expand?.orders_orderitem?.description || 'Unknown item'} -

+

{item.description}

- Expected: {expectedQty} {collection.unit || collection.expand?.orders_orderitem?.unit || 'units'} + Expected: {expectedQty} units

- {collection.expand?.orders_orderitem?.selling_price && ( + {item.expand?.order && ( +

+ Order: {item.expand.order.order_number} +

+ )} + {item.selling_price && (

- Unit price: {formatCurrency(collection.expand.orders_orderitem.selling_price)} + Unit price: {formatCurrency(item.selling_price)}

)}
diff --git a/components/CollectionItemCard.tsx b/components/CollectionItemCard.tsx index 1d13c23..7398841 100644 --- a/components/CollectionItemCard.tsx +++ b/components/CollectionItemCard.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState } from 'react'; -import { Collection } from '@/lib/types'; +import { OrderItem } from '@/lib/types'; import { cn, formatDate, formatCurrency, parseCoordinates } from '@/lib/utils'; import { Check, @@ -17,19 +17,19 @@ import { } from 'lucide-react'; interface CollectionItemProps { - collection: Collection; - onCollect: (collection: Collection) => void; - onProblem: (collection: Collection) => void; + item: OrderItem; + onCollect: (item: OrderItem) => void; + onProblem: (item: OrderItem) => void; onNavigate: (lat: number, lng: number, name: string) => void; } -function getItemType(collection: Collection): { +function getItemType(item: OrderItem): { type: string; icon: React.ReactNode; color: string; label: string; } { - if (collection.status === 'problem') { + if (item.item_status === 'problem' || item.problem_flag) { return { type: 'problem', icon: , @@ -38,8 +38,7 @@ function getItemType(collection: Collection): { }; } - const orderNumber = collection.expand?.orders_orderitem?.expand?.orders_order?.order_number || ''; - const desc = collection.description || collection.expand?.orders_orderitem?.description || ''; + const orderNumber = item.expand?.order?.order_number || ''; if (orderNumber.toLowerCase().includes('breakdown')) { return { @@ -59,12 +58,12 @@ function getItemType(collection: Collection): { }; } - if (collection.is_manual) { + if (item.is_stock_item) { return { - type: 'manual', + type: 'stock', icon: , color: 'text-purple-500', - label: 'Manual', + label: 'Stock', }; } @@ -77,38 +76,29 @@ function getItemType(collection: Collection): { } export default function CollectionItemCard({ - collection, + item, onCollect, onProblem, onNavigate, }: CollectionItemProps) { const [expanded, setExpanded] = useState(false); - const supplier = collection.expand?.wfdash_suppliers; - const orderItem = collection.expand?.orders_orderitem; - const itemType = getItemType(collection); + const supplier = item.expand?.supplier; + const itemType = getItemType(item); - // Parse coordinates from text field const coords = parseCoordinates(supplier?.coordinates); const hasCoords = coords !== null; - const description = collection.is_manual - ? collection.description - : orderItem?.description || 'Unknown item'; - - const quantity = collection.quantity || orderItem?.quantity || 0; - const unit = collection.unit || orderItem?.unit || 'units'; - - const orderNumber = orderItem?.expand?.orders_order?.order_number || ''; - const poNumber = orderItem?.expand?.orders_purchaseorder?.po_number || ''; + const description = item.description || 'Unknown item'; + const expectedQty = item.order_qty || item.quantity || 0; return (
{/* Header */} @@ -131,7 +121,7 @@ export default function CollectionItemCard({ itemType.type === 'problem' && 'bg-red-100 text-red-700', itemType.type === 'breakdown' && 'bg-orange-100 text-orange-700', itemType.type === 'repair' && 'bg-amber-100 text-amber-700', - itemType.type === 'manual' && 'bg-purple-100 text-purple-700', + itemType.type === 'stock' && 'bg-purple-100 text-purple-700', itemType.type === 'normal' && 'bg-orange-100 text-orange-700' )} > @@ -143,15 +133,15 @@ export default function CollectionItemCard({

{supplier?.suppliername || 'Unknown Supplier'}

-

Expected: {quantity} {unit}

- {orderNumber &&

Order: {orderNumber}

} - {poNumber &&

PO: {poNumber}

} - {collection.planned_date && ( -

Planned: {formatDate(collection.planned_date)}

+

Expected: {expectedQty} units

+ {item.expand?.order?.order_number &&

Order: {item.expand.order.order_number}

} + {item.expand?.purchase_order?.po_number &&

PO: {item.expand.purchase_order.po_number}

} + {item.collection_date && ( +

Planned: {formatDate(item.collection_date)}

)} - {orderItem?.selling_price && ( + {item.selling_price && (

- {formatCurrency(orderItem.selling_price * quantity)} + {formatCurrency(item.selling_price * expectedQty)}

)}
@@ -170,11 +160,20 @@ export default function CollectionItemCard({ {/* Expanded Details */} {expanded && (
- {collection.notes && ( + {item.notes && (

- {collection.notes} + {item.notes} +

+
+ )} + + {item.problem_notes && ( +
+

+ + {item.problem_notes}

)} @@ -190,7 +189,7 @@ export default function CollectionItemCard({
@@ -193,7 +139,6 @@ export default function CollectionMap({ collections, onNavigate }: MapViewProps) })} - {/* Legend overlay */}

Legend

diff --git a/components/LocationTracker.tsx b/components/LocationTracker.tsx index 71fd740..d7d5f32 100644 --- a/components/LocationTracker.tsx +++ b/components/LocationTracker.tsx @@ -1,81 +1,45 @@ 'use client'; -import { useEffect, useRef, useCallback } from 'react'; -import { getSettings, saveDriverLocation, addToQueue, isAuthenticated } from '@/lib/pocketbase'; +import { useEffect, useRef } from 'react'; +import { getSettings, saveDriverLocation, isAuthenticated } from '@/lib/pocketbase'; export default function LocationTracker() { const intervalRef = useRef | null>(null); - const lastSentRef = useRef(0); - const hasPermissionRef = useRef(null); - - const sendLocation = useCallback(async (position: GeolocationPosition) => { - if (!isAuthenticated()) return; - - const user = getCurrentUser(); - if (!user) return; - - // Skip if accuracy is too poor (based on settings) - const settings = getSettings(); - if (position.coords.accuracy && position.coords.accuracy > settings.minimumAccuracy) { - return; - } - - const locationData = { - driver: user.id, - latitude: position.coords.latitude, - longitude: position.coords.longitude, - accuracy: position.coords.accuracy || undefined, - altitude: position.coords.altitude || undefined, - speed: position.coords.speed || undefined, - heading: position.coords.heading || undefined, - timestamp: new Date().toISOString(), - }; - - try { - await saveDriverLocation(locationData); - lastSentRef.current = Date.now(); - } catch (error) { - // Queue for sync when back online - addToQueue({ - type: 'status_update', - data: locationData, - }); - } - }, []); useEffect(() => { - if (!('geolocation' in navigator)) { - return; - } + if (!('geolocation' in navigator)) return; const settings = getSettings(); const intervalMs = Math.max(30000, settings.locationInterval * 1000); const trackLocation = () => { + if (!isAuthenticated()) return; + + const auth = JSON.parse(localStorage.getItem('pocketbase_auth') || '{}'); + const user = auth?.model; + if (!user) return; + navigator.geolocation.getCurrentPosition( (position) => { - hasPermissionRef.current = true; - sendLocation(position); + if (position.coords.accuracy > settings.minimumAccuracy) return; + + saveDriverLocation({ + driver: user.id, + latitude: position.coords.latitude, + longitude: position.coords.longitude, + accuracy: position.coords.accuracy || undefined, + altitude: position.coords.altitude || undefined, + speed: position.coords.speed || undefined, + heading: position.coords.heading || undefined, + timestamp: new Date().toISOString(), + }); }, - (error) => { - // Silently handle errors - don't spam console - if (error.code === error.PERMISSION_DENIED) { - hasPermissionRef.current = false; - } - // TIMEOUT and POSITION_UNAVAILABLE are expected on desktop - }, - { - enableHighAccuracy: settings.enableHighAccuracy, - maximumAge: intervalMs, - timeout: 30000, - } + () => {}, + { enableHighAccuracy: settings.enableHighAccuracy, maximumAge: intervalMs, timeout: 30000 } ); }; - // Send immediately on mount trackLocation(); - - // Then on interval intervalRef.current = setInterval(trackLocation, intervalMs); return () => { @@ -84,17 +48,7 @@ export default function LocationTracker() { intervalRef.current = null; } }; - }, [sendLocation]); + }, []); - // This component doesn't render anything return null; } - -function getCurrentUser() { - try { - const auth = JSON.parse(localStorage.getItem('pocketbase_auth') || '{}'); - return auth?.model; - } catch { - return null; - } -} diff --git a/components/ProblemModal.tsx b/components/ProblemModal.tsx index 5534316..98ab68c 100644 --- a/components/ProblemModal.tsx +++ b/components/ProblemModal.tsx @@ -1,19 +1,19 @@ 'use client'; import { useState } from 'react'; -import { Collection, PROBLEM_TYPES } from '@/lib/types'; +import { OrderItem, PROBLEM_TYPES } from '@/lib/types'; import { cn } from '@/lib/utils'; import { X, Camera } from 'lucide-react'; interface ProblemModalProps { - collection: Collection | null; + item: OrderItem | null; isOpen: boolean; onClose: () => void; - onConfirm: (collection: Collection, problemType: string, description: string, photo?: File) => void; + onConfirm: (item: OrderItem, problemType: string, description: string, photo?: File) => void; } export default function ProblemModal({ - collection, + item, isOpen, onClose, onConfirm, @@ -23,7 +23,7 @@ export default function ProblemModal({ const [photo, setPhoto] = useState(null); const [error, setError] = useState(''); - if (!isOpen || !collection) return null; + if (!isOpen || !item) return null; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -39,7 +39,7 @@ export default function ProblemModal({ return; } - onConfirm(collection, problemType, description, photo || undefined); + onConfirm(item, problemType, description, photo || undefined); setProblemType(''); setDescription(''); setPhoto(null); @@ -61,13 +61,9 @@ export default function ProblemModal({
-

- {collection.is_manual - ? collection.description - : collection.expand?.orders_orderitem?.description || 'Unknown item'} -

+

{item.description}

- {collection.expand?.wfdash_suppliers?.suppliername || 'Unknown Supplier'} + {item.expand?.supplier?.suppliername || 'Unknown Supplier'}

diff --git a/import_suppliers.py b/import_suppliers.py new file mode 100644 index 0000000..8e2e87a --- /dev/null +++ b/import_suppliers.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Import suppliers from NocoDB into PocketBase""" +import requests, json, time, sys + +PB_URL = "https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io" +NOCODB_API = "https://nocodb.apphub.co.za/api/v3/data/putj028icfsplze/md9msyn5qco5ttu/records" +TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfcGJfdXNlcnNfYXV0aF8iLCJleHAiOjE3ODAyNjMwODcsImlkIjoiazA0NXlmZXplOTJja3g5IiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.4tUpls9uCAgcvwTvbWBrcl8_NLxgS__oaWZNRAEGsDo" + +HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"} + +# Get existing supplier names +try: + existing = requests.get(f"{PB_URL}/api/collections/suppliers/records?perPage=500", headers=HEADERS).json() + existing_names = {r.get("suppliername", "").strip() for r in existing.get("items", []) if r.get("suppliername")} +except: + existing_names = set() + +print(f"Existing suppliers in PocketBase: {len(existing_names)}") + +created = 0 +skipped = 0 + +# Use NocoDB MCP-style approach - manually construct records from known data +# We'll fetch via direct API with the auth header +page = 1 +while True: + try: + resp = requests.get( + f"{NOCODB_API}?pageSize=100&page={page}&fields=suppliername,email,suppliernumber,supplieraddress,coordinates,closingtime,supply_tags", + headers={"xc-token": "PUT_TOKEN_HERE"} # NocoDB needs its own token + ) + if resp.status_code != 200: + print(f"Page {page}: HTTP {resp.status_code} — stopping") + break + + data = resp.json() + records = data.get("records", []) + if not records: + break + + for r in records: + f = r["fields"] + name = (f.get("suppliername") or "").strip() + if not name or name in existing_names: + skipped += 1 + continue + + payload = {"suppliername": name} + for k in ["supplieraddress", "coordinates", "email", "suppliernumber", "closingtime", "supply_tags"]: + v = f.get(k) + if v and str(v).strip(): + payload[k] = str(v).strip() + + if len(payload) <= 1: + skipped += 1 + continue + + create = requests.post(f"{PB_URL}/api/collections/suppliers/records", headers=HEADERS, json=payload) + if create.status_code == 200: + created += 1 + existing_names.add(name) + else: + skipped += 1 + + time.sleep(0.05) + + print(f"Page {page}: processed {len(records)} records ({created} created, {skipped} skipped so far)") + page += 1 + + if not data.get("next"): + break + + except Exception as e: + print(f"Error: {e}") + break + +print(f"\nDone! Created: {created}, Skipped (existing/no data): {skipped}") diff --git a/lib/pocketbase.ts b/lib/pocketbase.ts index 8066be2..236044c 100644 --- a/lib/pocketbase.ts +++ b/lib/pocketbase.ts @@ -1,15 +1,10 @@ import PocketBase from 'pocketbase'; -import { AppSettings, DEFAULT_SETTINGS, Collection, DriverLocation } from './types'; +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); -// Collection names in PocketBase (matching Django table names) -const COLLECTIONS_TABLE = 'driver_list_collection'; -const PROBLEMS_TABLE = 'driver_list_collectionproblem'; -const LOCATIONS_TABLE = 'driver_locations'; - // Auth helpers export const isAuthenticated = () => pb.authStore.isValid; @@ -23,81 +18,55 @@ export const logout = () => { }; export const login = async (email: string, password: string) => { - // Try 'users' first (PocketBase default), fallback to 'auth_user' - try { - return await pb.collection('users').authWithPassword(email, password); - } catch { - return await pb.collection('auth_user').authWithPassword(email, password); - } + return await pb.collection('users').authWithPassword(email, password); }; -// Collection data helpers +// Get assigned collections (order_items assigned to current driver) export const getAssignedCollections = async () => { const user = getCurrentUser(); if (!user) return []; - const today = new Date(); - today.setHours(0, 0, 0, 0); - const tomorrow = new Date(today); - tomorrow.setDate(tomorrow.getDate() + 2); - - const records = await pb.collection(COLLECTIONS_TABLE).getFullList({ - filter: `status = "assigned" && auth_user = "${user.id}" && planned_date <= "${tomorrow.toISOString()}"`, - expand: 'orders_orderitem,wfdash_suppliers', - sort: 'wfdash_suppliers.suppliername', + const records = await pb.collection('order_items').getFullList({ + filter: `driver = "${user.id}" && (item_status = "assigned" || item_status = "driver_pool")`, + expand: 'supplier,order,purchase_order', + sort: 'supplier.suppliername', }); - return records as unknown as Collection[]; -}; - -export const getPendingCollections = async () => { - const records = await pb.collection(COLLECTIONS_TABLE).getFullList({ - filter: 'status = "pending" || status = "problem"', - expand: 'orders_orderitem,wfdash_suppliers', - sort: 'wfdash_suppliers.suppliername', - }); - - return records as unknown as Collection[]; + return records as unknown as OrderItem[]; }; +// Update order item status when collected export const updateCollectionStatus = async ( id: string, status: string, - data: Partial + data: { qty_collected?: number; notes?: string; collected_date?: string; problem_type?: string; problem_flag?: boolean } ) => { - return await pb.collection(COLLECTIONS_TABLE).update(id, { - status, + return await pb.collection('order_items').update(id, { + item_status: status, ...data, }); }; +// Report a problem on an order item export const reportProblem = async ( - collectionId: string, + orderItemId: string, problemType: string, description: string ) => { - // Update collection status - await pb.collection(COLLECTIONS_TABLE).update(collectionId, { - status: 'problem', - notes: description, - }); - - // Create problem record - return await pb.collection(PROBLEMS_TABLE).create({ - driver_list_collection: collectionId, + return await pb.collection('order_items').update(orderItemId, { + item_status: 'problem', problem_type: problemType, - description, - reported_at: new Date().toISOString(), + problem_flag: true, + problem_notes: description, }); }; -// Location tracking +// Location tracking - tries to save, fails silently if table doesn't exist export const saveDriverLocation = async (location: Omit) => { try { - return await pb.collection(LOCATIONS_TABLE).create(location); - } catch (err) { - // If driver_locations table doesn't exist yet, silently fail - console.warn('Could not save location - table may not exist:', err); + return await pb.collection('driver_locations').create(location); + } catch { + // Silently fail - table may not exist yet } }; @@ -124,42 +93,3 @@ export const saveSettings = (settings: Partial) => { localStorage.setItem(SETTINGS_KEY, JSON.stringify(updated)); return updated; }; - -// Offline queue helpers -const QUEUE_KEY = '1driver_offline_queue'; - -export interface QueueItem { - id: string; - type: 'status_update' | 'problem_report'; - data: Record; - timestamp: string; -} - -export const addToQueue = (item: Omit) => { - if (typeof window === 'undefined') return; - const queue = getQueue(); - const newItem: QueueItem = { - ...item, - id: crypto.randomUUID(), - timestamp: new Date().toISOString(), - }; - queue.push(newItem); - localStorage.setItem(QUEUE_KEY, JSON.stringify(queue)); -}; - -export const getQueue = (): QueueItem[] => { - if (typeof window === 'undefined') return []; - const saved = localStorage.getItem(QUEUE_KEY); - return saved ? JSON.parse(saved) : []; -}; - -export const removeFromQueue = (id: string) => { - if (typeof window === 'undefined') return; - const queue = getQueue().filter((item) => item.id !== id); - localStorage.setItem(QUEUE_KEY, JSON.stringify(queue)); -}; - -export const clearQueue = () => { - if (typeof window === 'undefined') return; - localStorage.removeItem(QUEUE_KEY); -}; diff --git a/lib/types.ts b/lib/types.ts index 0e9f438..317ba47 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,66 +1,57 @@ export interface Supplier { id: string; suppliername: string; + email: string; + suppliernumber: string; supplieraddress: string; - coordinates?: string; // Raw coordinates text (e.g. "-33.9249, 18.4241") - latitude?: number; - longitude?: number; + 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; } export interface OrderItem { id: string; + order: string; + supplier: string; + driver: string; description: string; + po_description: string; quantity: number; order_qty: number; - unit: string; - selling_price?: number; + selling_price: number; + cost_price: number; item_status: string; - expand?: { - orders_order?: { order_number: string }; - orders_purchaseorder?: { po_number: string }; - }; -} - -export interface Collection { - id: string; - orders_orderitem: string; - wfdash_suppliers: string; - auth_user: string; - status: 'pending' | 'assigned' | 'collected' | 'problem'; - quantity: number; - planned_date: string; - actual_date: string; - received_qty: number; + status: string; + driver_status: string; notes: string; - created_at: string; - updated_at: string; - is_manual: boolean; - description: string; - unit: string; - // Expanded fields use the relation field names from PocketBase + problem_type: string; + problem_flag: boolean; + problem_notes: string; + qty_collected: number; + collected_date: string; + collection_date: string; + is_stock_item: boolean; + // Expanded fields expand?: { - orders_orderitem?: OrderItem; - wfdash_suppliers?: Supplier; - auth_user?: Driver; + supplier?: Supplier; + order?: Order; + purchase_order?: PurchaseOrder; }; } -export interface CollectionProblem { - id: string; - driver_list_collection: string; - problem_type: 'out_of_stock' | 'partial_stock' | 'quality_issue' | 'wrong_item' | 'other'; - description: string; - photo?: string; - reported_at: string; -} - -export interface Driver { - id: string; - email: string; - name: string; - username: string; -} - export interface DriverLocation { id: string; driver: string; @@ -71,12 +62,11 @@ export interface DriverLocation { speed?: number; heading?: number; timestamp: string; - battery_level?: number; } export interface AppSettings { - locationInterval: number; // in seconds - minimumAccuracy: number; // in meters + locationInterval: number; + minimumAccuracy: number; enableHighAccuracy: boolean; darkMode: boolean; fontSize: 'small' | 'normal' | 'large';