google maps added

This commit is contained in:
suryacp23 2026-07-28 16:02:09 +05:30
parent 8345e1fe69
commit 742646268c
8 changed files with 378 additions and 48 deletions

91
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "frontend",
"version": "0.0.0",
"dependencies": {
"@react-google-maps/api": "^2.20.8",
"@types/leaflet": "^1.9.21",
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.8",
@ -447,6 +448,22 @@
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@googlemaps/js-api-loader": {
"version": "1.16.8",
"resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.8.tgz",
"integrity": "sha512-CROqqwfKotdO6EBjZO/gQGVTbeDps5V7Mt9+8+5Q+jTg5CRMi3Ii/L9PmV3USROrt2uWxtGzJHORmByxyo9pSQ==",
"license": "Apache-2.0"
},
"node_modules/@googlemaps/markerclusterer": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.5.3.tgz",
"integrity": "sha512-x7lX0R5yYOoiNectr10wLgCBasNcXFHiADIBdmn7jQllF2B5ENQw5XtZK+hIw4xnV0Df0xhN4LN98XqA5jaiOw==",
"license": "Apache-2.0",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"supercluster": "^8.0.1"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@ -592,6 +609,36 @@
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@react-google-maps/api": {
"version": "2.20.8",
"resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-2.20.8.tgz",
"integrity": "sha512-wtLYFtCGXK3qbIz1H5to3JxbosPnKsvjDKhqGylXUb859EskhzR7OpuNt0LqdLarXUtZCJTKzPn3BNaekNIahg==",
"license": "MIT",
"dependencies": {
"@googlemaps/js-api-loader": "1.16.8",
"@googlemaps/markerclusterer": "2.5.3",
"@react-google-maps/infobox": "2.20.0",
"@react-google-maps/marker-clusterer": "2.20.0",
"@types/google.maps": "3.58.1",
"invariant": "2.2.4"
},
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19",
"react-dom": "^16.8 || ^17 || ^18 || ^19"
}
},
"node_modules/@react-google-maps/infobox": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-2.20.0.tgz",
"integrity": "sha512-03PJHjohhaVLkX6+NHhlr8CIlvUxWaXhryqDjyaZ8iIqqix/nV8GFdz9O3m5OsjtxtNho09F/15j14yV0nuyLQ==",
"license": "MIT"
},
"node_modules/@react-google-maps/marker-clusterer": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-2.20.0.tgz",
"integrity": "sha512-tieX9Va5w1yP88vMgfH1pHTacDQ9TgDTjox3tLlisKDXRQWdjw+QeVVghhf5XqqIxXHgPdcGwBvKY6UP+SIvLw==",
"license": "MIT"
},
"node_modules/@react-leaflet/core": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
@ -1271,6 +1318,12 @@
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"license": "MIT"
},
"node_modules/@types/google.maps": {
"version": "3.58.1",
"resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz",
"integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==",
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@ -2267,7 +2320,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-json-stable-stringify": {
@ -2498,6 +2550,15 @@
"node": ">=12"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
@ -2548,7 +2609,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/jsesc": {
@ -2624,6 +2684,12 @@
"jspdf": "^2 || ^3 || ^4"
}
},
"node_modules/kdbush": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz",
"integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
"license": "ISC"
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -2931,6 +2997,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -3461,6 +3539,15 @@
"node": ">=0.1.14"
}
},
"node_modules/supercluster": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
"license": "ISC",
"dependencies": {
"kdbush": "^4.0.2"
}
},
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",

View File

@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@react-google-maps/api": "^2.20.8",
"@types/leaflet": "^1.9.21",
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.8",

View File

@ -5,7 +5,7 @@
// docs under src/docs/api.
export const APP_ID = '434';
export const BASE_URL = 'https://sandbox.getzino.in';
export const BASE_URL = 'https://dev.getzino.in';
// --- Order Booking (src/docs/api/order_booking.md) ---
export const ORDER_BOOKING = {
@ -65,14 +65,14 @@ export const ORDER_BOOKING = {
recordViews: {
ORDERS: '63714ac2-c9fb-40e2-abba-d4081a70b768',
CALLS: '56b21b46-6d2c-4e10-b5a3-c63d058d0afa',
PRODUCT_WISE_ORDERS:'f1b372de-9e00-4cc6-81b5-1e1a98e7daef',
ROUTE_WISE_ORDERS:'bfb346b6-b512-4870-946b-a6f2e621870e',
SO_WISE_ORDERS:'c155d266-4eef-420d-9fb8-cd6ace43c569',
SO_WISE_VISITS:'08767452-6f0d-46cc-ae7f-ddf2cb480159',
YEARLY_ORDERS:'b8fd04da-f076-4e8d-b711-cf9282bf450a',
MONTHLY_ORDERS:'6d4ce86a-7a05-446b-8c81-cd0b98b6b244',
WEEKLY_ORDERS:'c6e2c57f-ed17-4c92-a2de-189c61c89422',
STORE_WISE_PRODUCTS:'f8d71b1d-dffd-450a-b9c9-3a63db8992c5'
PRODUCT_WISE_ORDERS: 'f1b372de-9e00-4cc6-81b5-1e1a98e7daef',
ROUTE_WISE_ORDERS: 'bfb346b6-b512-4870-946b-a6f2e621870e',
SO_WISE_ORDERS: 'c155d266-4eef-420d-9fb8-cd6ace43c569',
SO_WISE_VISITS: '08767452-6f0d-46cc-ae7f-ddf2cb480159',
YEARLY_ORDERS: 'b8fd04da-f076-4e8d-b711-cf9282bf450a',
MONTHLY_ORDERS: '6d4ce86a-7a05-446b-8c81-cd0b98b6b244',
WEEKLY_ORDERS: 'c6e2c57f-ed17-4c92-a2de-189c61c89422',
STORE_WISE_PRODUCTS: 'f8d71b1d-dffd-450a-b9c9-3a63db8992c5'
},
detailViews: {
@ -212,3 +212,11 @@ export const ROUTE_WISE_DISTRIBUTORS: Record<string, string[]> = {
export const SALES_REPORT_SO_NAMES = [
'Surya C'
];
// --- Pipeline Endpoints ---
export const PIPELINE = {
endpoints: {
nearestStores: '/api/papi2/nearest-stores',
dailySalesReport: '/api/papi2/daily-sales-report'
}
};

View File

@ -380,6 +380,7 @@ export function DailyLogDetail({ instanceId, refreshKey, onPunchOut, onBack }: D
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,254 @@
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 isLocalhost = typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
const { isLoaded } = useJsApiLoader({
id: 'google-map-script',
googleMapsApiKey: isLocalhost ? '' : (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>
);
}

View File

@ -2,6 +2,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { Modal } from '../components/reusable';
import { DailyLogsView } from '../components/rv';
import { DailyLogDetail } from '../components/dv';
import { DailyLogMap } from '../components/maps/DailyLogMap';
import { useState } from 'react';
import { Button } from '../components/buttons/Button';
import { Plus, MapPin } from 'lucide-react';
@ -20,21 +21,13 @@ export function DailyLogsPage() {
const [refreshKey, setRefreshKey] = useState(0);
const mapComponent = (
<div className="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden flex flex-col h-full min-h-[320px]">
<div className="p-4 border-b border-slate-100 flex items-center gap-2">
<div className="!bg-[var(--tiles-card-bg)] rounded-lg shadow-sm border border-gray-100 overflow-hidden flex flex-col h-full min-h-[320px]">
<div className="px-5 py-4 border-b border-[var(--z-block-border)] flex items-center gap-2">
<MapPin size={18} className="text-slate-400" />
<h3 className="text-slate-800 font-bold">Activity Locations</h3>
<h3 className="text-md font-semibold text-[var(--z-text-default)]">Activity Locations</h3>
</div>
<div className="flex-1 relative bg-slate-50">
<iframe
title="Logs Map"
width="100%"
height="100%"
style={{ border: 0, position: 'absolute', inset: 0 }}
loading="lazy"
allowFullScreen
src="https://maps.google.com/maps?q=activity&hl=en&z=10&output=embed"
></iframe>
<DailyLogMap />
</div>
</div>
);

View File

@ -2,6 +2,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { Modal } from '../components/reusable';
import { DailyLogsView } from '../components/rv';
import { DailyLogDetail } from '../components/dv';
import { DailyLogMap } from '../components/maps/DailyLogMap';
import { useState } from 'react';
import { Button } from '../components/buttons/Button';
import { Plus, MapPin } from 'lucide-react';
@ -23,21 +24,13 @@ export function MyDailyLogsPage() {
const { userEmail } = useAuth();
const mapComponent = (
<div className="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden flex flex-col h-full min-h-[320px]">
<div className="p-4 border-b border-slate-100 flex items-center gap-2">
<div className="!bg-[var(--tiles-card-bg)] rounded-lg shadow-sm border border-gray-100 overflow-hidden flex flex-col h-full min-h-[320px]">
<div className="px-5 py-4 border-b border-[var(--z-block-border)] flex items-center gap-2">
<MapPin size={18} className="text-slate-400" />
<h3 className="text-slate-800 font-bold">Activity Locations</h3>
<h3 className="text-md font-semibold text-[var(--z-text-default)]">Activity Locations</h3>
</div>
<div className="flex-1 relative bg-slate-50">
<iframe
title="Logs Map"
width="100%"
height="100%"
style={{ border: 0, position: 'absolute', inset: 0 }}
loading="lazy"
allowFullScreen
src="https://maps.google.com/maps?q=activity&hl=en&z=10&output=embed"
></iframe>
<DailyLogMap />
</div>
</div>
);

View File

@ -2,6 +2,7 @@ import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { StoresView } from '../components/rv';
import { StoreDetail } from '../components/dv';
import { DailyLogMap } from '../components/maps/DailyLogMap';
import { Button } from '../components/buttons/Button';
import { Plus, MapPin } from 'lucide-react';
import { DynamicForm } from '../components/forms/DynamicForm';
@ -56,21 +57,13 @@ export function StoresPage() {
}
const mapComponent = (
<div className="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden flex flex-col h-full min-h-[320px]">
<div className="p-4 border-b border-slate-100 flex items-center gap-2">
<div className="!bg-[var(--tiles-card-bg)] rounded-lg shadow-sm border border-gray-100 overflow-hidden flex flex-col h-full min-h-[320px]">
<div className="px-5 py-4 border-b border-[var(--z-block-border)] flex items-center gap-2">
<MapPin size={18} className="text-slate-400" />
<h3 className="text-slate-800 font-bold">Location Spread</h3>
<h3 className="text-md font-semibold text-[var(--z-text-default)]">Location Spread</h3>
</div>
<div className="flex-1 relative bg-slate-50">
<iframe
title="Stores Map"
width="100%"
height="100%"
style={{ border: 0, position: 'absolute', inset: 0 }}
loading="lazy"
allowFullScreen
src="https://maps.google.com/maps?q=stores&hl=en&z=10&output=embed"
></iframe>
<DailyLogMap />
</div>
</div>
);