253 lines
8.3 KiB
TypeScript
253 lines
8.3 KiB
TypeScript
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
import { useJsApiLoader, GoogleMap, Marker, InfoWindow } from '@react-google-maps/api';
|
|
import { Loader2, MapPin } from 'lucide-react';
|
|
import { PIPELINE } from '../../api/config';
|
|
import { dailyReportsClient } from '../../api/clients';
|
|
|
|
interface StoreLocation {
|
|
latitude: number;
|
|
longitude: number;
|
|
}
|
|
|
|
interface Store {
|
|
area: string;
|
|
business_name: string;
|
|
distance_km: number;
|
|
location: StoreLocation;
|
|
route_name: string;
|
|
store_code: string;
|
|
}
|
|
|
|
interface NearestStoresResponse {
|
|
message: string;
|
|
response: {
|
|
count: number;
|
|
stores: Store[];
|
|
success: boolean;
|
|
};
|
|
}
|
|
|
|
const mapContainerStyle = {
|
|
width: '100%',
|
|
height: '100%',
|
|
borderRadius: '0px'
|
|
};
|
|
|
|
const defaultCenter = { lat: 12.9716, lng: 77.5946 };
|
|
|
|
export function DailyLogMap({
|
|
latitude,
|
|
longitude,
|
|
showStoreList = false
|
|
}: {
|
|
latitude?: number | string | null;
|
|
longitude?: number | string | null;
|
|
showStoreList?: boolean;
|
|
}) {
|
|
const [userLocation, setUserLocation] = useState<{ lat: number, lng: number } | null>(null);
|
|
const [stores, setStores] = useState<Store[]>([]);
|
|
const [selectedStore, setSelectedStore] = useState<Store | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const mapRef = useRef<google.maps.Map | null>(null);
|
|
|
|
const { isLoaded } = useJsApiLoader({
|
|
id: 'google-map-script',
|
|
googleMapsApiKey: import.meta.env.VITE_GOOGLE_MAPS_API_KEY || ''
|
|
});
|
|
|
|
const fetchNearestStores = async (lat: number, lng: number) => {
|
|
try {
|
|
const data = await dailyReportsClient.request<NearestStoresResponse>(
|
|
'POST',
|
|
PIPELINE.endpoints.nearestStores,
|
|
{
|
|
latitude: lat,
|
|
longitude: lng
|
|
},
|
|
{
|
|
'accept': 'application/json, text/plain, */*',
|
|
'groupid': '25',
|
|
'orgid': '57',
|
|
'templateid': '189',
|
|
'x-pipeline-version': 'draft'
|
|
}
|
|
);
|
|
|
|
if (data.response && data.response.stores) {
|
|
setStores(data.response.stores);
|
|
}
|
|
} catch (err: any) {
|
|
console.error('An error occurred while fetching stores.', err);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
// If coords provided in props, use them
|
|
if (latitude && longitude && String(latitude).trim() !== '—' && String(longitude).trim() !== '—') {
|
|
const lat = Number(latitude);
|
|
const lng = Number(longitude);
|
|
if (!isNaN(lat) && !isNaN(lng)) {
|
|
setUserLocation({ lat, lng });
|
|
fetchNearestStores(lat, lng);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Otherwise fallback to whatever tactic used for mobile
|
|
if (!navigator.geolocation) {
|
|
setError('Geolocation is not supported by your browser.');
|
|
return;
|
|
}
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
(position) => {
|
|
const lat = position.coords.latitude;
|
|
const lng = position.coords.longitude;
|
|
setUserLocation({ lat, lng });
|
|
fetchNearestStores(lat, lng);
|
|
},
|
|
(err) => {
|
|
setError(err.message || 'Failed to get location');
|
|
},
|
|
{ enableHighAccuracy: true }
|
|
);
|
|
}, [latitude, longitude]);
|
|
|
|
const onLoad = useCallback(function callback(map: google.maps.Map) {
|
|
mapRef.current = map;
|
|
if (userLocation) {
|
|
const bounds = new window.google.maps.LatLngBounds();
|
|
bounds.extend(userLocation);
|
|
map.fitBounds(bounds);
|
|
|
|
const listener = window.google.maps.event.addListener(map, 'idle', () => {
|
|
if (map.getZoom()! > 15) {
|
|
map.setZoom(15);
|
|
}
|
|
window.google.maps.event.removeListener(listener);
|
|
});
|
|
}
|
|
}, [userLocation]);
|
|
|
|
useEffect(() => {
|
|
if (mapRef.current && userLocation && stores.length > 0) {
|
|
const bounds = new window.google.maps.LatLngBounds();
|
|
bounds.extend(userLocation);
|
|
stores.forEach(store => {
|
|
if (store.location) {
|
|
const lat = Number(store.location.latitude);
|
|
const lng = Number(store.location.longitude);
|
|
if (!isNaN(lat) && !isNaN(lng)) {
|
|
bounds.extend({ lat, lng });
|
|
}
|
|
}
|
|
});
|
|
mapRef.current.fitBounds(bounds);
|
|
}
|
|
}, [stores, userLocation]);
|
|
|
|
if (!isLoaded) {
|
|
return (
|
|
<div className="w-full h-[320px] flex items-center justify-center bg-slate-50 rounded-xl border border-slate-200">
|
|
<Loader2 className="w-8 h-8 text-slate-400 animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="w-full h-full flex flex-col bg-transparent min-h-[320px]">
|
|
{error && (
|
|
<div className="bg-red-50 text-red-600 p-3 mb-4 rounded-lg text-sm border border-red-100">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="w-full min-h-[320px] flex-1 relative rounded-none border border-slate-200 rounded-xl overflow-hidden">
|
|
<GoogleMap
|
|
mapContainerStyle={mapContainerStyle}
|
|
center={userLocation || defaultCenter}
|
|
zoom={userLocation ? 13 : 5}
|
|
onLoad={onLoad}
|
|
options={{
|
|
streetViewControl: false,
|
|
mapTypeControl: false,
|
|
fullscreenControl: false
|
|
}}
|
|
>
|
|
{/* User's Current Location Marker */}
|
|
{userLocation && (
|
|
<Marker
|
|
position={userLocation}
|
|
icon={{
|
|
url: 'http://maps.google.com/mapfiles/ms/icons/blue-dot.png'
|
|
}}
|
|
title="Location"
|
|
/>
|
|
)}
|
|
|
|
{/* Stores Markers */}
|
|
{stores.map((store) => {
|
|
if (!store.location) return null;
|
|
const lat = Number(store.location.latitude);
|
|
const lng = Number(store.location.longitude);
|
|
if (isNaN(lat) || isNaN(lng)) return null;
|
|
|
|
return (
|
|
<Marker
|
|
key={store.store_code}
|
|
position={{ lat, lng }}
|
|
onClick={() => setSelectedStore(store)}
|
|
title={store.business_name}
|
|
/>
|
|
);
|
|
})}
|
|
|
|
{/* Info Window for Selected Store */}
|
|
{selectedStore && selectedStore.location && !isNaN(Number(selectedStore.location.latitude)) && (
|
|
<InfoWindow
|
|
position={{ lat: Number(selectedStore.location.latitude), lng: Number(selectedStore.location.longitude) }}
|
|
onCloseClick={() => setSelectedStore(null)}
|
|
>
|
|
<div className="p-1 max-w-[200px]">
|
|
<h3 className="font-semibold text-sm text-slate-800">{selectedStore.business_name}</h3>
|
|
<p className="text-xs text-slate-500 mt-1">{selectedStore.area}</p>
|
|
<div className="flex justify-between items-center mt-2 pt-2 border-t border-slate-100">
|
|
<span className="text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded border border-blue-100">
|
|
{selectedStore.route_name}
|
|
</span>
|
|
<span className="text-[10px] text-slate-400 font-medium">
|
|
{selectedStore.distance_km} km
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</InfoWindow>
|
|
)}
|
|
</GoogleMap>
|
|
</div>
|
|
|
|
{showStoreList && (
|
|
<div className="mt-4 p-5 flex flex-col gap-4 bg-[var(--tiles-card-bg)] rounded-xl border border-slate-200">
|
|
<h3 className="font-bold text-slate-800 text-sm m-0 flex items-center gap-2 uppercase tracking-wider">
|
|
<MapPin size={16} className="text-slate-500" /> Nearby Stores
|
|
</h3>
|
|
<div className="flex flex-col gap-3">
|
|
{stores.length > 0 ? (
|
|
stores.map((store) => (
|
|
<div key={store.store_code} className="flex flex-col p-3 rounded-lg border border-slate-100 bg-slate-50 hover:bg-slate-100 transition-colors cursor-pointer" onClick={() => setSelectedStore(store)}>
|
|
<span className="font-bold text-slate-800 text-sm">{store.business_name}</span>
|
|
<span className="text-xs text-slate-500">{store.store_code}</span>
|
|
{store.distance_km != null && (
|
|
<span className="text-xs text-[#1b37a5] font-semibold mt-1">{store.distance_km.toFixed(2)} km away</span>
|
|
)}
|
|
</div>
|
|
))
|
|
) : (
|
|
<span className="text-sm text-slate-400 italic">No nearby stores found.</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|