WFDASHDRIVER-APP/components/CollectionMap.tsx

214 lines
7.5 KiB
TypeScript

'use client';
import { useEffect, useMemo, useState } from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
import { Collection, Supplier } from '@/lib/types';
import { formatCurrency, calculateDistance, parseCoordinates } from '@/lib/utils';
import { Navigation, AlertTriangle } from 'lucide-react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
// Fix Leaflet default markers in Next.js
const defaultIcon = L.icon({
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41],
});
const currentLocationIcon = L.divIcon({
className: 'custom-div-icon',
html: `
<div style="
width: 20px;
height: 20px;
background: #ea580c;
border: 3px solid white;
border-radius: 50%;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
"></div>
`,
iconSize: [20, 20],
iconAnchor: [10, 10],
});
interface MapViewProps {
collections: Collection[];
onNavigate: (lat: number, lng: number, name: string) => void;
}
function MapBounds({ collections }: { collections: Collection[] }) {
const map = useMap();
useEffect(() => {
const validPoints = collections
.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);
map.fitBounds(bounds, { padding: [50, 50], maxZoom: 15 });
}
}, [map, collections]);
return null;
}
export default function CollectionMap({ collections, onNavigate }: MapViewProps) {
const [userLocation, setUserLocation] = useState<[number, number] | null>(null);
useEffect(() => {
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition(
(pos) => {
setUserLocation([pos.coords.latitude, pos.coords.longitude]);
},
() => {
// Silently fail - desktop may not have GPS
},
{ enableHighAccuracy: false, timeout: 30000 }
);
}
}, []);
// Group collections by supplier
const suppliers = useMemo(() => {
const map = new Map<string, { supplier: Supplier; collections: Collection[]; coords: { latitude: number; longitude: number } }>();
collections.forEach((collection) => {
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: [], coords });
}
map.get(key)!.collections.push(collection);
});
return Array.from(map.values());
}, [collections]);
const defaultCenter: [number, number] = [-33.9249, 18.4241]; // Cape Town default
const center = userLocation || defaultCenter;
const getSupplierDistance = (coords: { latitude: number; longitude: number }) => {
if (!userLocation) return null;
return calculateDistance(
userLocation[0],
userLocation[1],
coords.latitude,
coords.longitude
);
};
return (
<div className="h-[calc(100vh-4rem)] w-full relative">
<MapContainer
center={center}
zoom={12}
className="h-full w-full"
zoomControl={false}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapBounds collections={collections} />
{userLocation && (
<Marker position={userLocation} icon={currentLocationIcon}>
<Popup>
<div className="text-sm font-medium">Your Location</div>
</Popup>
</Marker>
)}
{suppliers.map(({ supplier, collections: supplierCollections, coords }) => {
const distance = getSupplierDistance(coords);
const hasProblem = supplierCollections.some((c) => c.status === 'problem');
return (
<Marker
key={supplier.id}
position={[coords.latitude, coords.longitude]}
icon={defaultIcon}
>
<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.supplieraddress}</p>
{distance && (
<p className="text-xs text-orange-600 mb-2 font-medium">
{distance.toFixed(1)} km away
</p>
)}
<div className="space-y-1.5 mb-3">
{supplierCollections.map((c) => (
<div
key={c.id}
className="text-xs p-1.5 bg-gray-50 rounded"
>
<div className="flex items-center gap-1">
{c.status === 'problem' && (
<AlertTriangle size={12} className="text-red-500" />
)}
<span className="font-medium truncate">
{c.is_manual
? c.description
: c.expand?.orders_orderitem?.description || 'Unknown'}
</span>
</div>
<span className="text-gray-400">
Qty: {c.quantity} {c.unit || c.expand?.orders_orderitem?.unit || 'units'}
</span>
</div>
))}
</div>
<button
onClick={() =>
onNavigate(coords.latitude, coords.longitude, supplier.suppliername)
}
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
</button>
</div>
</Popup>
</Marker>
);
})}
</MapContainer>
{/* Legend overlay */}
<div className="absolute top-4 right-4 bg-white/90 backdrop-blur-sm rounded-xl p-3 shadow-lg z-[400]">
<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-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-orange-500 rounded-sm rotate-45 transform origin-bottom" />
</div>
<span className="text-[10px] text-gray-600">Collection Stop</span>
</div>
</div>
</div>
</div>
);
}