krishna_sales/src/components/maps/DailyLogMap.tsx
2026-07-31 12:33:24 +05:30

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>
);
}