feat: update collection and supplier handling across components
This commit is contained in:
parent
0363ebeb75
commit
27b5b9fc78
9 changed files with 163 additions and 143 deletions
14
app/page.tsx
14
app/page.tsx
|
|
@ -85,9 +85,9 @@ export default function CollectionsPage() {
|
|||
filtered = filtered.filter((c) => {
|
||||
const desc = c.is_manual
|
||||
? c.description
|
||||
: c.expand?.order_item?.description || '';
|
||||
const supplier = c.expand?.supplier?.suppliername || '';
|
||||
const orderNum = c.expand?.order_item?.expand?.order?.order_number || '';
|
||||
: c.expand?.orders_orderitem?.description || '';
|
||||
const supplier = c.expand?.wfdash_suppliers?.suppliername || '';
|
||||
const orderNum = c.expand?.orders_orderitem?.expand?.orders_order?.order_number || '';
|
||||
return (
|
||||
desc.toLowerCase().includes(query) ||
|
||||
supplier.toLowerCase().includes(query) ||
|
||||
|
|
@ -180,7 +180,7 @@ export default function CollectionsPage() {
|
|||
|
||||
// Group by supplier
|
||||
const groupedBySupplier = filteredCollections.reduce((acc, collection) => {
|
||||
const supplier = collection.expand?.supplier;
|
||||
const supplier = collection.expand?.wfdash_suppliers;
|
||||
if (!supplier) return acc;
|
||||
|
||||
const key = supplier.id;
|
||||
|
|
@ -189,7 +189,7 @@ export default function CollectionsPage() {
|
|||
}
|
||||
acc[key].items.push(collection);
|
||||
return acc;
|
||||
}, {} as Record<string, { supplier: NonNullable<Collection['expand']>['supplier']; items: Collection[] }>);
|
||||
}, {} as Record<string, { supplier: NonNullable<Collection['expand']>['wfdash_suppliers']; items: Collection[] }>);
|
||||
|
||||
const groupedArray = Object.values(groupedBySupplier).sort((a, b) =>
|
||||
(a.supplier?.suppliername || '').localeCompare(b.supplier?.suppliername || '')
|
||||
|
|
@ -358,9 +358,9 @@ export default function CollectionsPage() {
|
|||
<span className="bg-orange-100 text-orange-700 text-[10px] px-2 py-0.5 rounded-full font-medium">
|
||||
{items.length}
|
||||
</span>
|
||||
{supplier!.address && (
|
||||
{supplier!.supplieraddress && (
|
||||
<span className="text-xs text-gray-400 truncate flex-1 text-right">
|
||||
{supplier!.address}
|
||||
{supplier!.supplieraddress}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export default function CollectModal({
|
|||
|
||||
if (!isOpen || !collection) return null;
|
||||
|
||||
const expectedQty = collection.quantity || collection.expand?.order_item?.quantity || 0;
|
||||
const expectedQty = collection.quantity || collection.expand?.orders_orderitem?.quantity || 0;
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -68,14 +68,14 @@ export default function CollectModal({
|
|||
<p className="text-sm font-medium truncate">
|
||||
{collection.is_manual
|
||||
? collection.description
|
||||
: collection.expand?.order_item?.description || 'Unknown item'}
|
||||
: collection.expand?.orders_orderitem?.description || 'Unknown item'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Expected: {expectedQty} {collection.unit || collection.expand?.order_item?.unit || 'units'}
|
||||
Expected: {expectedQty} {collection.unit || collection.expand?.orders_orderitem?.unit || 'units'}
|
||||
</p>
|
||||
{collection.expand?.order_item?.selling_price && (
|
||||
{collection.expand?.orders_orderitem?.selling_price && (
|
||||
<p className="text-xs text-green-600 mt-1">
|
||||
Unit price: {formatCurrency(collection.expand.order_item.selling_price)}
|
||||
Unit price: {formatCurrency(collection.expand.orders_orderitem.selling_price)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useState } from 'react';
|
||||
import { Collection } from '@/lib/types';
|
||||
import { cn, formatDate, formatCurrency } from '@/lib/utils';
|
||||
import { cn, formatDate, formatCurrency, parseCoordinates } from '@/lib/utils';
|
||||
import {
|
||||
Check,
|
||||
AlertTriangle,
|
||||
|
|
@ -38,8 +38,8 @@ function getItemType(collection: Collection): {
|
|||
};
|
||||
}
|
||||
|
||||
const orderNumber = collection.expand?.order_item?.expand?.order?.order_number || '';
|
||||
const desc = collection.description || collection.expand?.order_item?.description || '';
|
||||
const orderNumber = collection.expand?.orders_orderitem?.expand?.orders_order?.order_number || '';
|
||||
const desc = collection.description || collection.expand?.orders_orderitem?.description || '';
|
||||
|
||||
if (orderNumber.toLowerCase().includes('breakdown')) {
|
||||
return {
|
||||
|
|
@ -83,13 +83,14 @@ export default function CollectionItemCard({
|
|||
onNavigate,
|
||||
}: CollectionItemProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [showActions, setShowActions] = useState(false);
|
||||
|
||||
const supplier = collection.expand?.supplier;
|
||||
const orderItem = collection.expand?.order_item;
|
||||
const supplier = collection.expand?.wfdash_suppliers;
|
||||
const orderItem = collection.expand?.orders_orderitem;
|
||||
const itemType = getItemType(collection);
|
||||
|
||||
const hasCoords = supplier?.latitude && supplier?.longitude;
|
||||
// Parse coordinates from text field
|
||||
const coords = parseCoordinates(supplier?.coordinates);
|
||||
const hasCoords = coords !== null;
|
||||
|
||||
const description = collection.is_manual
|
||||
? collection.description
|
||||
|
|
@ -98,8 +99,8 @@ export default function CollectionItemCard({
|
|||
const quantity = collection.quantity || orderItem?.quantity || 0;
|
||||
const unit = collection.unit || orderItem?.unit || 'units';
|
||||
|
||||
const orderNumber = orderItem?.expand?.order?.order_number || '';
|
||||
const poNumber = orderItem?.expand?.purchase_order?.po_number || '';
|
||||
const orderNumber = orderItem?.expand?.orders_order?.order_number || '';
|
||||
const poNumber = orderItem?.expand?.orders_purchaseorder?.po_number || '';
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -178,9 +179,9 @@ export default function CollectionItemCard({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{supplier?.address && (
|
||||
{supplier?.supplieraddress && (
|
||||
<p className="text-xs text-gray-500 mb-3">
|
||||
<strong>Address:</strong> {supplier.address}
|
||||
<strong>Address:</strong> {supplier.supplieraddress}
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
|
@ -216,11 +217,11 @@ export default function CollectionItemCard({
|
|||
<span>Problem</span>
|
||||
</button>
|
||||
|
||||
{hasCoords && supplier && (
|
||||
{hasCoords && coords && supplier && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onNavigate(supplier.latitude!, supplier.longitude!, supplier.suppliername);
|
||||
onNavigate(coords.latitude, coords.longitude, supplier.suppliername);
|
||||
}}
|
||||
className={cn(
|
||||
'flex items-center justify-center gap-1.5 py-2.5 px-3 rounded-lg text-sm font-medium',
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
|
||||
import { Collection, Supplier } from '@/lib/types';
|
||||
import { formatCurrency, calculateDistance } from '@/lib/utils';
|
||||
import { Navigation, Check, AlertTriangle } from 'lucide-react';
|
||||
import { formatCurrency, calculateDistance, parseCoordinates } from '@/lib/utils';
|
||||
import { Navigation, AlertTriangle } from 'lucide-react';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
|
||||
|
|
@ -45,8 +45,11 @@ function MapBounds({ collections }: { collections: Collection[] }) {
|
|||
|
||||
useEffect(() => {
|
||||
const validPoints = collections
|
||||
.filter((c) => c.expand?.supplier?.latitude && c.expand?.supplier?.longitude)
|
||||
.map((c) => [c.expand!.supplier!.latitude!, c.expand!.supplier!.longitude!] as [number, number]);
|
||||
.map((c) => {
|
||||
const coords = parseCoordinates(c.expand?.wfdash_suppliers?.coordinates);
|
||||
return coords ? [coords.latitude, coords.longitude] as [number, number] : null;
|
||||
})
|
||||
.filter((point): point is [number, number] => point !== null);
|
||||
|
||||
if (validPoints.length > 0) {
|
||||
const bounds = L.latLngBounds(validPoints);
|
||||
|
|
@ -59,7 +62,6 @@ function MapBounds({ collections }: { collections: Collection[] }) {
|
|||
|
||||
export default function CollectionMap({ collections, onNavigate }: MapViewProps) {
|
||||
const [userLocation, setUserLocation] = useState<[number, number] | null>(null);
|
||||
const [selectedSupplier, setSelectedSupplier] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if ('geolocation' in navigator) {
|
||||
|
|
@ -67,34 +69,27 @@ export default function CollectionMap({ collections, onNavigate }: MapViewProps)
|
|||
(pos) => {
|
||||
setUserLocation([pos.coords.latitude, pos.coords.longitude]);
|
||||
},
|
||||
(error) => console.error('Could not get location:', error),
|
||||
{ enableHighAccuracy: true, timeout: 10000 }
|
||||
);
|
||||
|
||||
const watchId = navigator.geolocation.watchPosition(
|
||||
(pos) => {
|
||||
setUserLocation([pos.coords.latitude, pos.coords.longitude]);
|
||||
() => {
|
||||
// Silently fail - desktop may not have GPS
|
||||
},
|
||||
(error) => console.error('Watch error:', error),
|
||||
{ enableHighAccuracy: true }
|
||||
{ enableHighAccuracy: false, timeout: 30000 }
|
||||
);
|
||||
|
||||
return () => navigator.geolocation.clearWatch(watchId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Group collections by supplier
|
||||
const suppliers = useMemo(() => {
|
||||
const map = new Map<string, { supplier: Supplier; collections: Collection[] }>();
|
||||
const map = new Map<string, { supplier: Supplier; collections: Collection[]; coords: { latitude: number; longitude: number } }>();
|
||||
|
||||
collections.forEach((collection) => {
|
||||
if (!collection.expand?.supplier) return;
|
||||
const supplier = collection.expand.supplier;
|
||||
if (!supplier.latitude || !supplier.longitude) return;
|
||||
if (!collection.expand?.wfdash_suppliers) return;
|
||||
const supplier = collection.expand.wfdash_suppliers;
|
||||
const coords = parseCoordinates(supplier.coordinates);
|
||||
if (!coords) return;
|
||||
|
||||
const key = supplier.id;
|
||||
if (!map.has(key)) {
|
||||
map.set(key, { supplier, collections: [] });
|
||||
map.set(key, { supplier, collections: [], coords });
|
||||
}
|
||||
map.get(key)!.collections.push(collection);
|
||||
});
|
||||
|
|
@ -105,13 +100,13 @@ export default function CollectionMap({ collections, onNavigate }: MapViewProps)
|
|||
const defaultCenter: [number, number] = [-33.9249, 18.4241]; // Cape Town default
|
||||
const center = userLocation || defaultCenter;
|
||||
|
||||
const getSupplierDistance = (supplier: Supplier) => {
|
||||
if (!userLocation || !supplier.latitude || !supplier.longitude) return null;
|
||||
const getSupplierDistance = (coords: { latitude: number; longitude: number }) => {
|
||||
if (!userLocation) return null;
|
||||
return calculateDistance(
|
||||
userLocation[0],
|
||||
userLocation[1],
|
||||
supplier.latitude,
|
||||
supplier.longitude
|
||||
coords.latitude,
|
||||
coords.longitude
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -138,23 +133,20 @@ export default function CollectionMap({ collections, onNavigate }: MapViewProps)
|
|||
</Marker>
|
||||
)}
|
||||
|
||||
{suppliers.map(({ supplier, collections: supplierCollections }) => {
|
||||
const distance = getSupplierDistance(supplier);
|
||||
{suppliers.map(({ supplier, collections: supplierCollections, coords }) => {
|
||||
const distance = getSupplierDistance(coords);
|
||||
const hasProblem = supplierCollections.some((c) => c.status === 'problem');
|
||||
|
||||
return (
|
||||
<Marker
|
||||
key={supplier.id}
|
||||
position={[supplier.latitude!, supplier.longitude!]}
|
||||
position={[coords.latitude, coords.longitude]}
|
||||
icon={defaultIcon}
|
||||
eventHandlers={{
|
||||
click: () => setSelectedSupplier(supplier.id),
|
||||
}}
|
||||
>
|
||||
<Popup>
|
||||
<div className="min-w-[200px] max-w-[250px]">
|
||||
<h3 className="font-bold text-sm mb-1">{supplier.suppliername}</h3>
|
||||
<p className="text-xs text-gray-500 mb-2">{supplier.address}</p>
|
||||
<p className="text-xs text-gray-500 mb-2">{supplier.supplieraddress}</p>
|
||||
|
||||
{distance && (
|
||||
<p className="text-xs text-orange-600 mb-2 font-medium">
|
||||
|
|
@ -175,11 +167,11 @@ export default function CollectionMap({ collections, onNavigate }: MapViewProps)
|
|||
<span className="font-medium truncate">
|
||||
{c.is_manual
|
||||
? c.description
|
||||
: c.expand?.order_item?.description || 'Unknown'}
|
||||
: c.expand?.orders_orderitem?.description || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-gray-400">
|
||||
Qty: {c.quantity} {c.unit || c.expand?.order_item?.unit || 'units'}
|
||||
Qty: {c.quantity} {c.unit || c.expand?.orders_orderitem?.unit || 'units'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -187,9 +179,9 @@ export default function CollectionMap({ collections, onNavigate }: MapViewProps)
|
|||
|
||||
<button
|
||||
onClick={() =>
|
||||
onNavigate(supplier.latitude!, supplier.longitude!, supplier.suppliername)
|
||||
onNavigate(coords.latitude, coords.longitude, supplier.suppliername)
|
||||
}
|
||||
className="w-full flex items-center justify-center gap-1.5 py-2 px-3 bg-blue-500 text-white rounded-lg text-sm font-medium active:bg-orange-600 transition-colors"
|
||||
className="w-full flex items-center justify-center gap-1.5 py-2 px-3 bg-orange-500 text-white rounded-lg text-sm font-medium active:bg-orange-600 transition-colors"
|
||||
>
|
||||
<Navigation size={14} />
|
||||
Navigate
|
||||
|
|
@ -206,12 +198,12 @@ export default function CollectionMap({ collections, onNavigate }: MapViewProps)
|
|||
<p className="text-xs font-medium text-gray-700 mb-2">Legend</p>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-blue-500 border-2 border-white shadow-sm" />
|
||||
<div className="w-3 h-3 rounded-full bg-orange-500 border-2 border-white shadow-sm" />
|
||||
<span className="text-[10px] text-gray-600">Your Location</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 relative">
|
||||
<div className="absolute inset-0 bg-blue-500 rounded-sm rotate-45 transform origin-bottom" />
|
||||
<div className="absolute inset-0 bg-orange-500 rounded-sm rotate-45 transform origin-bottom" />
|
||||
</div>
|
||||
<span className="text-[10px] text-gray-600">Collection Stop</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,19 +5,26 @@ import { getSettings, saveDriverLocation, addToQueue, isAuthenticated } from '@/
|
|||
|
||||
export default function LocationTracker() {
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const watchRef = useRef<number | null>(null);
|
||||
const lastSentRef = useRef<number>(0);
|
||||
const hasPermissionRef = useRef<boolean | null>(null);
|
||||
|
||||
const sendLocation = useCallback(async (position: GeolocationPosition) => {
|
||||
if (!isAuthenticated()) return;
|
||||
|
||||
const user = JSON.parse(localStorage.getItem('pocketbase_auth') || '{}')?.model;
|
||||
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,
|
||||
accuracy: position.coords.accuracy || undefined,
|
||||
altitude: position.coords.altitude || undefined,
|
||||
speed: position.coords.speed || undefined,
|
||||
heading: position.coords.heading || undefined,
|
||||
|
|
@ -26,8 +33,8 @@ export default function LocationTracker() {
|
|||
|
||||
try {
|
||||
await saveDriverLocation(locationData);
|
||||
lastSentRef.current = Date.now();
|
||||
} catch (error) {
|
||||
console.error('Failed to save location:', error);
|
||||
// Queue for sync when back online
|
||||
addToQueue({
|
||||
type: 'status_update',
|
||||
|
|
@ -38,74 +45,56 @@ export default function LocationTracker() {
|
|||
|
||||
useEffect(() => {
|
||||
if (!('geolocation' in navigator)) {
|
||||
console.warn('Geolocation not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = getSettings();
|
||||
const intervalMs = Math.max(10000, settings.locationInterval * 1000);
|
||||
const intervalMs = Math.max(30000, settings.locationInterval * 1000);
|
||||
|
||||
const startTracking = () => {
|
||||
// Use watchPosition for more frequent updates
|
||||
watchRef.current = navigator.geolocation.watchPosition(
|
||||
sendLocation,
|
||||
const trackLocation = () => {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
hasPermissionRef.current = true;
|
||||
sendLocation(position);
|
||||
},
|
||||
(error) => {
|
||||
console.error('Geolocation error:', 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: 10000,
|
||||
timeout: 30000,
|
||||
}
|
||||
);
|
||||
|
||||
// Also send on interval as backup
|
||||
intervalRef.current = setInterval(() => {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
sendLocation,
|
||||
(error) => console.error('Interval geolocation error:', error),
|
||||
{
|
||||
enableHighAccuracy: settings.enableHighAccuracy,
|
||||
timeout: 10000,
|
||||
}
|
||||
);
|
||||
}, intervalMs);
|
||||
};
|
||||
|
||||
// Start tracking if authenticated
|
||||
if (isAuthenticated()) {
|
||||
startTracking();
|
||||
}
|
||||
// Send immediately on mount
|
||||
trackLocation();
|
||||
|
||||
// Listen for auth changes
|
||||
const handleAuthChange = () => {
|
||||
if (isAuthenticated()) {
|
||||
startTracking();
|
||||
} else {
|
||||
stopTracking();
|
||||
}
|
||||
};
|
||||
// Then on interval
|
||||
intervalRef.current = setInterval(trackLocation, intervalMs);
|
||||
|
||||
// Check periodically for auth state
|
||||
const authCheckInterval = setInterval(handleAuthChange, 5000);
|
||||
|
||||
const stopTracking = () => {
|
||||
if (watchRef.current !== null) {
|
||||
navigator.geolocation.clearWatch(watchRef.current);
|
||||
watchRef.current = null;
|
||||
}
|
||||
return () => {
|
||||
if (intervalRef.current !== null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
stopTracking();
|
||||
clearInterval(authCheckInterval);
|
||||
};
|
||||
}, [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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,10 +64,10 @@ export default function ProblemModal({
|
|||
<p className="text-sm font-medium truncate">
|
||||
{collection.is_manual
|
||||
? collection.description
|
||||
: collection.expand?.order_item?.description || 'Unknown item'}
|
||||
: collection.expand?.orders_orderitem?.description || 'Unknown item'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{collection.expand?.supplier?.suppliername || 'Unknown Supplier'}
|
||||
{collection.expand?.wfdash_suppliers?.suppliername || 'Unknown Supplier'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ const POCKETBASE_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || 'https://pocket
|
|||
|
||||
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;
|
||||
|
||||
|
|
@ -18,7 +23,12 @@ export const logout = () => {
|
|||
};
|
||||
|
||||
export const login = async (email: string, password: string) => {
|
||||
return await pb.collection('users').authWithPassword(email, password);
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
|
||||
// Collection data helpers
|
||||
|
|
@ -31,20 +41,20 @@ export const getAssignedCollections = async () => {
|
|||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(tomorrow.getDate() + 2);
|
||||
|
||||
const records = await pb.collection('collections').getFullList({
|
||||
filter: `status = "assigned" && driver = "${user.id}" && planned_date <= "${tomorrow.toISOString()}"`,
|
||||
expand: 'order_item,supplier',
|
||||
sort: 'supplier.suppliername',
|
||||
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',
|
||||
});
|
||||
|
||||
return records as unknown as Collection[];
|
||||
};
|
||||
|
||||
export const getPendingCollections = async () => {
|
||||
const records = await pb.collection('collections').getFullList({
|
||||
const records = await pb.collection(COLLECTIONS_TABLE).getFullList({
|
||||
filter: 'status = "pending" || status = "problem"',
|
||||
expand: 'order_item,supplier',
|
||||
sort: 'supplier.suppliername',
|
||||
expand: 'orders_orderitem,wfdash_suppliers',
|
||||
sort: 'wfdash_suppliers.suppliername',
|
||||
});
|
||||
|
||||
return records as unknown as Collection[];
|
||||
|
|
@ -55,7 +65,7 @@ export const updateCollectionStatus = async (
|
|||
status: string,
|
||||
data: Partial<Collection>
|
||||
) => {
|
||||
return await pb.collection('collections').update(id, {
|
||||
return await pb.collection(COLLECTIONS_TABLE).update(id, {
|
||||
status,
|
||||
...data,
|
||||
});
|
||||
|
|
@ -67,22 +77,28 @@ export const reportProblem = async (
|
|||
description: string
|
||||
) => {
|
||||
// Update collection status
|
||||
await pb.collection('collections').update(collectionId, {
|
||||
await pb.collection(COLLECTIONS_TABLE).update(collectionId, {
|
||||
status: 'problem',
|
||||
notes: description,
|
||||
});
|
||||
|
||||
// Create problem record
|
||||
return await pb.collection('collection_problems').create({
|
||||
collection: collectionId,
|
||||
return await pb.collection(PROBLEMS_TABLE).create({
|
||||
driver_list_collection: collectionId,
|
||||
problem_type: problemType,
|
||||
description,
|
||||
reported_at: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
// Location tracking
|
||||
export const saveDriverLocation = async (location: Omit<DriverLocation, 'id'>) => {
|
||||
return await pb.collection('driver_locations').create(location);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// Settings helpers
|
||||
|
|
|
|||
30
lib/types.ts
30
lib/types.ts
|
|
@ -1,10 +1,10 @@
|
|||
export interface Supplier {
|
||||
id: string;
|
||||
suppliername: string;
|
||||
address: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
collection?: string;
|
||||
supplieraddress: string;
|
||||
coordinates?: string; // Raw coordinates text (e.g. "-33.9249, 18.4241")
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
}
|
||||
|
||||
export interface OrderItem {
|
||||
|
|
@ -14,20 +14,18 @@ export interface OrderItem {
|
|||
order_qty: number;
|
||||
unit: string;
|
||||
selling_price?: number;
|
||||
order?: string;
|
||||
purchase_order?: string;
|
||||
item_status: string;
|
||||
expand?: {
|
||||
order?: { order_number: string };
|
||||
purchase_order?: { po_number: string };
|
||||
orders_order?: { order_number: string };
|
||||
orders_purchaseorder?: { po_number: string };
|
||||
};
|
||||
}
|
||||
|
||||
export interface Collection {
|
||||
id: string;
|
||||
order_item: string;
|
||||
supplier: string;
|
||||
driver: string;
|
||||
orders_orderitem: string;
|
||||
wfdash_suppliers: string;
|
||||
auth_user: string;
|
||||
status: 'pending' | 'assigned' | 'collected' | 'problem';
|
||||
quantity: number;
|
||||
planned_date: string;
|
||||
|
|
@ -39,17 +37,17 @@ export interface Collection {
|
|||
is_manual: boolean;
|
||||
description: string;
|
||||
unit: string;
|
||||
// Expanded fields
|
||||
// Expanded fields use the relation field names from PocketBase
|
||||
expand?: {
|
||||
order_item?: OrderItem;
|
||||
supplier?: Supplier;
|
||||
driver?: Driver;
|
||||
orders_orderitem?: OrderItem;
|
||||
wfdash_suppliers?: Supplier;
|
||||
auth_user?: Driver;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CollectionProblem {
|
||||
id: string;
|
||||
collection: string;
|
||||
driver_list_collection: string;
|
||||
problem_type: 'out_of_stock' | 'partial_stock' | 'quality_issue' | 'wrong_item' | 'other';
|
||||
description: string;
|
||||
photo?: string;
|
||||
|
|
|
|||
24
lib/utils.ts
24
lib/utils.ts
|
|
@ -84,3 +84,27 @@ export function calculateDistance(
|
|||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return R * c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse coordinates from a text field.
|
||||
* Supports formats like:
|
||||
* "-33.9249, 18.4241"
|
||||
* "-33.9249 18.4241"
|
||||
* "lat: -33.9249, lng: 18.4241"
|
||||
* Returns null if parsing fails.
|
||||
*/
|
||||
export function parseCoordinates(coordString: string | undefined): { latitude: number; longitude: number } | null {
|
||||
if (!coordString) return null;
|
||||
|
||||
// Try to extract two numbers that look like lat/lng
|
||||
const matches = coordString.match(/-?\d+\.?\d*/g);
|
||||
if (matches && matches.length >= 2) {
|
||||
const lat = parseFloat(matches[0]);
|
||||
const lng = parseFloat(matches[1]);
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
return { latitude: lat, longitude: lng };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue