From 742646268cd292c78e2eca5b9e248c8c8174abac Mon Sep 17 00:00:00 2001 From: suryacp23 Date: Tue, 28 Jul 2026 16:02:09 +0530 Subject: [PATCH] google maps added --- package-lock.json | 91 +++++++++- package.json | 1 + src/api/config.ts | 28 +-- src/components/dv/DailyLogDetail.tsx | 1 + src/components/maps/DailyLogMap.tsx | 254 +++++++++++++++++++++++++++ src/screens/DailyLogsPage.tsx | 17 +- src/screens/MyDailyLogsPage.tsx | 17 +- src/screens/StoresPage.tsx | 17 +- 8 files changed, 378 insertions(+), 48 deletions(-) create mode 100644 src/components/maps/DailyLogMap.tsx diff --git a/package-lock.json b/package-lock.json index a1ac328..62e7f18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 295173d..b939f34 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/api/config.ts b/src/api/config.ts index 8fd9172..c39be4e 100644 --- a/src/api/config.ts +++ b/src/api/config.ts @@ -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,15 +65,15 @@ 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: { ORDERS: '0804c6c3-6cf9-4050-94bb-fa48dd5de87d', @@ -212,3 +212,11 @@ export const ROUTE_WISE_DISTRIBUTORS: Record = { 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' + } +}; diff --git a/src/components/dv/DailyLogDetail.tsx b/src/components/dv/DailyLogDetail.tsx index d8ebc78..7eb06b6 100644 --- a/src/components/dv/DailyLogDetail.tsx +++ b/src/components/dv/DailyLogDetail.tsx @@ -380,6 +380,7 @@ export function DailyLogDetail({ instanceId, refreshKey, onPunchOut, onBack }: D + ); } diff --git a/src/components/maps/DailyLogMap.tsx b/src/components/maps/DailyLogMap.tsx new file mode 100644 index 0000000..34e324c --- /dev/null +++ b/src/components/maps/DailyLogMap.tsx @@ -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([]); + const [selectedStore, setSelectedStore] = useState(null); + const [error, setError] = useState(null); + const mapRef = useRef(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( + '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 ( +
+ +
+ ); + } + + return ( +
+ {error && ( +
+ {error} +
+ )} + +
+ + {/* User's Current Location Marker */} + {userLocation && ( + + )} + + {/* 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 ( + setSelectedStore(store)} + title={store.business_name} + /> + ); + })} + + {/* Info Window for Selected Store */} + {selectedStore && selectedStore.location && !isNaN(Number(selectedStore.location.latitude)) && ( + setSelectedStore(null)} + > +
+

{selectedStore.business_name}

+

{selectedStore.area}

+
+ + {selectedStore.route_name} + + + {selectedStore.distance_km} km + +
+
+
+ )} +
+
+ + {showStoreList && ( +
+

+ Nearby Stores +

+
+ {stores.length > 0 ? ( + stores.map((store) => ( +
setSelectedStore(store)}> + {store.business_name} + {store.store_code} + {store.distance_km != null && ( + {store.distance_km.toFixed(2)} km away + )} +
+ )) + ) : ( + No nearby stores found. + )} +
+
+ )} +
+ ); +} diff --git a/src/screens/DailyLogsPage.tsx b/src/screens/DailyLogsPage.tsx index b27e8f9..31734dd 100644 --- a/src/screens/DailyLogsPage.tsx +++ b/src/screens/DailyLogsPage.tsx @@ -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 = ( -
-
+
+
-

Activity Locations

+

Activity Locations

- +
); diff --git a/src/screens/MyDailyLogsPage.tsx b/src/screens/MyDailyLogsPage.tsx index c4cb24a..546723e 100644 --- a/src/screens/MyDailyLogsPage.tsx +++ b/src/screens/MyDailyLogsPage.tsx @@ -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 = ( -
-
+
+
-

Activity Locations

+

Activity Locations

- +
); diff --git a/src/screens/StoresPage.tsx b/src/screens/StoresPage.tsx index ca04b05..ba12a33 100644 --- a/src/screens/StoresPage.tsx +++ b/src/screens/StoresPage.tsx @@ -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 = ( -
-
+
+
-

Location Spread

+

Location Spread

- +
);