From 7d6f1a9021f1215e14e21193e22b179ed36ba427 Mon Sep 17 00:00:00 2001 From: suryacp23 Date: Tue, 28 Jul 2026 11:46:02 +0530 Subject: [PATCH] added map --- .gitignore | 1 + package-lock.json | 91 ++++++- package.json | 1 + src/api/client.ts | 11 +- src/api/config.ts | 8 + src/components/buttons/Button.tsx | 2 +- src/components/cards/CallCard.tsx | 10 +- src/components/cards/OrderCard.tsx | 22 +- src/components/cards/StoreCard.tsx | 22 +- src/components/dv/CallDetail.tsx | 150 +++++------ src/components/dv/OrderDetail.tsx | 48 ++-- src/components/dv/PotentialMiningTable.tsx | 30 +-- src/components/dv/StoreDetail.tsx | 70 +++--- src/components/forms/DynamicForm.tsx | 72 ++++++ .../forms/fields/GeolocationInput.tsx | 151 ++++++++++- .../forms/fields/SmartGridField.tsx | 17 +- src/components/maps/NearestStoresMap.tsx | 238 ++++++++++++++++++ src/components/reusable/Modal.tsx | 6 +- src/components/rv/RecordView.tsx | 6 +- src/screens/AllDailyLogsPage.tsx | 2 +- src/screens/CallsPage.tsx | 2 +- src/screens/DailyLogsPage.tsx | 13 +- src/screens/DailySalesReportPage.tsx | 25 +- src/screens/OrdersPage.tsx | 2 +- src/screens/StoresPage.tsx | 30 +-- src/styles/styles.css | 11 + 26 files changed, 768 insertions(+), 273 deletions(-) create mode 100644 src/components/maps/NearestStoresMap.tsx diff --git a/.gitignore b/.gitignore index 376c8af..253fcba 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ dist-ssr # vite-plugin-pwa dev output dev-dist +.env diff --git a/package-lock.json b/package-lock.json index 8da4268..a457f95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "krishna-sales-pwa", "version": "0.0.0", "dependencies": { + "@react-google-maps/api": "^2.20.8", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.8", "lucide-react": "^0.400.0", @@ -1745,6 +1746,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", @@ -2304,6 +2321,36 @@ "url": "https://github.com/sponsors/sxzz" } }, + "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/@reduxjs/toolkit": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", @@ -3463,6 +3510,12 @@ "dev": true, "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", @@ -5120,7 +5173,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": { @@ -5743,6 +5795,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", @@ -6256,7 +6317,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": { @@ -6355,6 +6415,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", @@ -6680,6 +6746,18 @@ "dev": true, "license": "MIT" }, + "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", @@ -7988,6 +8066,15 @@ "node": ">=10" } }, + "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/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", diff --git a/package.json b/package.json index 3f50f8e..5ad9b78 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "generate-pwa-assets": "pwa-assets-generator" }, "dependencies": { + "@react-google-maps/api": "^2.20.8", "jspdf": "^4.2.1", "jspdf-autotable": "^5.0.8", "lucide-react": "^0.400.0", diff --git a/src/api/client.ts b/src/api/client.ts index 8323094..308d894 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -9,7 +9,7 @@ import type { RecordViewResponse, User, } from './types'; -import { APP_ID } from './config'; +import { APP_ID, PIPELINE } from './config'; const TOKEN_KEY = 'krishna_sales_token'; @@ -222,6 +222,15 @@ export class ZinoClient { }); } + // --- Pipeline APIs --- + + dailySalesReport(data: Record): Promise { + return this.request('POST', PIPELINE.endpoints.dailySalesReport, data, { + 'TemplateID': '157', + 'sec-ch-ua-platform': '"Android"', + }); + } + // --- RDBMS lookup records (owner-agent picker etc.) --- /** Search an RDBMS lookup template. Returns the matching rows. `formData` diff --git a/src/api/config.ts b/src/api/config.ts index baf0831..d8a2772 100644 --- a/src/api/config.ts +++ b/src/api/config.ts @@ -204,3 +204,11 @@ export const SALES_REPORT_DISTRIBUTORS = [ 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/buttons/Button.tsx b/src/components/buttons/Button.tsx index ec2746f..553be79 100644 --- a/src/components/buttons/Button.tsx +++ b/src/components/buttons/Button.tsx @@ -20,7 +20,7 @@ const SIZES: Record = { sm: 'h-[34px] px-3.5 text-[13px] rounded-sm', md: 'h-[42px] px-[18px] text-base rounded-md', lg: 'h-[50px] px-6 text-md rounded-md', - fab: 'h-[56px] w-[56px] text-sm rounded-full !bg-[#1F4D36] text-white shadow-lg', + fab: 'h-[56px] w-[56px] text-sm rounded-full !bg-primary text-white shadow-lg', }; const VARIANTS: Record = { diff --git a/src/components/cards/CallCard.tsx b/src/components/cards/CallCard.tsx index cf41366..dbabfdc 100644 --- a/src/components/cards/CallCard.tsx +++ b/src/components/cards/CallCard.tsx @@ -232,7 +232,7 @@ export function CallCard({ row, isDetailView = false }: { row: Record 0 && (
-

Order Products

+

Order Products

{totalOrders} {Number(totalOrders) === 1 ? 'Item' : 'Items'} @@ -246,9 +246,9 @@ export function CallCard({ row, isDetailView = false }: { row: Record 0 ? skuNum * bagsNum : Number(prod.row_kgs_3 || prod.row_kgs || prod.kgs_3 || prod.weight_3 || 0); const kgsStr = kgsNum > 0 ? ` (${kgsNum} kg)` : ''; return ( -
- {productName} - {bagsStr} Bags{kgsStr} +
+ {productName} + {bagsStr} Bags{kgsStr}
); })} @@ -273,7 +273,7 @@ export function CallCard({ row, isDetailView = false }: { row: Record -

Distributor Details

+

Distributor Details

{distName !== '—' && (
diff --git a/src/components/cards/OrderCard.tsx b/src/components/cards/OrderCard.tsx index 05402b5..fdfade0 100644 --- a/src/components/cards/OrderCard.tsx +++ b/src/components/cards/OrderCard.tsx @@ -43,7 +43,7 @@ export function OrderCard({ row, fields, isDetailView = false }: { row: Record f.data_type === 'grid' || String(f.field_key).includes('order_details')); const gridValRaw = gridField ? row[gridField.field_key] : (row.order_details || row.order_details_2 || row.order_details_3); const gridVal = Array.isArray(gridValRaw) ? gridValRaw : []; - const totalItems = gridVal.length; +// const totalItems = gridVal.length; // Totals let totalKgsVal = Number(row.total_kgs || row.total_kgs_2 || row.total_kgs_3 || row.total_weight || row.weight || 0); @@ -61,7 +61,7 @@ export function OrderCard({ row, fields, isDetailView = false }: { row: Record 0 ? (
3 ? formatValue(skuCodeRaw) : ''; - const category = formatValue(getVal('product_category') || ''); - const brCode = formatValue(getVal('br_code') || ''); - const subtitleArr = [skuCode, category, brCode].filter(v => v && v !== '—'); - const subtitle = subtitleArr.join(' . ').toUpperCase(); const bagsStr = formatValue(getVal('bags') || getVal('total_bags') || getVal('quantity') || 0); const bagsNum = Number(getVal('bags') || getVal('total_bags') || getVal('quantity') || 0); @@ -107,12 +101,12 @@ export function OrderCard({ row, fields, isDetailView = false }: { row: Record -
- {productName} +
+ {productName}
-
- {bagsStr} Bags - ({kgsStr}) +
+ {bagsStr} Bags + ({kgsStr})
); @@ -130,7 +124,7 @@ export function OrderCard({ row, fields, isDetailView = false }: { row: RecordTOTAL
{totalBags} Bags - ({totalKgs} kg) + ({totalKgs} kg)
diff --git a/src/components/cards/StoreCard.tsx b/src/components/cards/StoreCard.tsx index 2ed8c8b..15dce8f 100644 --- a/src/components/cards/StoreCard.tsx +++ b/src/components/cards/StoreCard.tsx @@ -73,27 +73,27 @@ export function StoreCard({ row, isDetailView = false, onEdit }: { row: Record
-

+

{storeName}

{ownerName !== '—' && ( <> -
+
Owner
-
{ownerName}
+
{ownerName}
)} {areaName !== '—' && ( <> -
+
Area
-
{areaName}
+
{areaName}
)}
@@ -135,22 +135,22 @@ export function StoreCard({ row, isDetailView = false, onEdit }: { row: Record
-
+
- Phone - {phone} + Phone + {phone}
-
+
- Email - {email} + Email + {email}
diff --git a/src/components/dv/CallDetail.tsx b/src/components/dv/CallDetail.tsx index 4de4b7d..d6434bd 100644 --- a/src/components/dv/CallDetail.tsx +++ b/src/components/dv/CallDetail.tsx @@ -406,21 +406,21 @@ export function CallDetail({ {/* Top Right KPI Grid */}
-
-
TOTAL BAGS
-
{totalBags.toLocaleString()}
+
+
TOTAL BAGS
+
{totalBags.toLocaleString()}
-
-
TOTAL KGS
-
{totalKgs.toLocaleString()}
+
+
TOTAL KGS
+
{totalKgs.toLocaleString()}
-
-
LINE ITEMS
-
{lineItemsCount}
+
+
LINE ITEMS
+
{lineItemsCount}
-
-
SALES OFFICER
-
{salesOfficer}
+
+
SALES OFFICER
+
{salesOfficer}
@@ -520,8 +520,8 @@ export function CallDetail({
-
- +
+

Order Details

@@ -541,44 +541,36 @@ export function CallDetail({ const bagsStr = item.bags; const kgsStr = item.totalKgs > 0 ? `${item.totalKgs.toLocaleString()} kg` : '0 kg'; return ( -
-
- {item.name} +
+
+ {item.name}
-
- {bagsStr} Bags - ({kgsStr}) +
+ {bagsStr} Bags + ({kgsStr})
); })}
-
- Total Order +
+ Total Order
- Bags + Bags {totalBags}
- Total Kgs + Total Kgs {totalKgs.toLocaleString()}
) : ( -
- - Order not placed +
+ + Order not placed
)}
@@ -604,8 +596,8 @@ export function CallDetail({
-
- +
+

Potential vs Ordered

@@ -619,42 +611,34 @@ export function CallDetail({ const renderTable = (items: any[]) => (
{items.map((item, idx) => ( -
+
- {item.name} + {item.name}
-
+
- Potential - {item.actualPotential} + Potential + {item.actualPotential}
- Ordered - {item.totalOrdered} + Ordered + {item.totalOrdered}
- Diff - = 0 ? '#059669' : '#ef4444' }}> + Diff + = 0 ? 'text-emerald-600' : 'text-red-500'}`}> {item.difference > 0 ? '+' : ''}{item.difference}
{item.reason && ( -
- Reason - {item.reason} +
+ Reason + {item.reason}
)}
@@ -747,9 +731,9 @@ export function CallDetail({ })}
) : ( -
- - No proof image uploaded +
+ + No proof image uploaded
)}
@@ -775,11 +759,11 @@ export function CallDetail({ />
) : ( -
-
- +
+
+
- {storeName} + {storeName}
)}
@@ -788,8 +772,8 @@ export function CallDetail({
-
- +
+

Store

@@ -840,11 +824,11 @@ export function CallDetail({
{/* Notes Container */} -
-
- NOTES +
+
+ NOTES
-

+

{storeNotes}

@@ -855,8 +839,8 @@ export function CallDetail({
-
- +
+

Store Potential

@@ -865,20 +849,12 @@ export function CallDetail({
{storePotentialList.map((item, idx) => ( -
-
- {item.name} +
+
+ {item.name}
-
- {item.actualPotential} KG Potential +
+ {item.actualPotential} KG Potential
))} @@ -889,8 +865,8 @@ export function CallDetail({ {/* Distributor Card */}
-
- +
+

Distributor

diff --git a/src/components/dv/OrderDetail.tsx b/src/components/dv/OrderDetail.tsx index 0f1b044..e20b4a4 100644 --- a/src/components/dv/OrderDetail.tsx +++ b/src/components/dv/OrderDetail.tsx @@ -4,7 +4,7 @@ import { ORDER_BOOKING } from '../../api/config'; import { Card } from '../reusable/Card'; import { Spinner } from '../reusable/Spinner'; import { EmptyState } from '../reusable/EmptyState'; -import { Store, User, Package, Calendar, Hash, Mail, Weight, List, MapPin, ArrowLeft } from 'lucide-react'; +import { Store, User, Package, Calendar, Hash, Mail, MapPin, ArrowLeft } from 'lucide-react'; export interface WiredDetailViewProps { instanceId: number | string; @@ -157,16 +157,16 @@ export function OrderDetail({ instanceId, onDataLoad, onBack }: WiredDetailViewP {/* Middle Stats Section */}
-
TOTAL BAGS
-
{String(totalBags)}
+
TOTAL BAGS
+
{String(totalBags)}
-
TOTAL KGS
-
{Number(totalKgs).toLocaleString()}
+
TOTAL KGS
+
{Number(totalKgs).toLocaleString()}
-
LINE ITEMS
-
{itemsArray.length}
+
LINE ITEMS
+
{itemsArray.length}
@@ -186,13 +186,13 @@ export function OrderDetail({ instanceId, onDataLoad, onBack }: WiredDetailViewP
{itemsArray.map((item: any, idx: number) => { const productName = String(item.product_name_3 || item.product_name || 'Unknown Product'); - const skuCodeRaw = item.sku_code_3 || item.sku_code || ''; - const skuCode = isNaN(Number(skuCodeRaw)) || String(skuCodeRaw).length > 3 ? String(skuCodeRaw) : ''; - const category = String(item.product_category_3 || item.product_category || ''); - const brCode = String(item.br_code_3 || item.br_code || ''); +// const skuCodeRaw = item.sku_code_3 || item.sku_code || ''; +// const skuCode = isNaN(Number(skuCodeRaw)) || String(skuCodeRaw).length > 3 ? String(skuCodeRaw) : ''; +// const category = String(item.product_category_3 || item.product_category || ''); +// const brCode = String(item.br_code_3 || item.br_code || ''); - const subtitleArr = [skuCode, category, brCode].filter(v => v && v !== '-' && v !== '—'); - const subtitle = subtitleArr.join(' . ').toUpperCase(); +// const subtitleArr = [skuCode, category, brCode].filter(v => v && v !== '-' && v !== '—'); +// const subtitle = subtitleArr.join(' . ').toUpperCase(); const bagsNum = Number(item.bags_3 || item.bags || 0); const bagsStr = String(bagsNum); @@ -203,21 +203,13 @@ export function OrderDetail({ instanceId, onDataLoad, onBack }: WiredDetailViewP const kgsStr = kgsNum > 0 ? `${kgsNum} kg` : '0 kg'; return ( -
-
- {productName} +
+
+ {productName}
-
- {bagsStr} Bags - ({kgsStr}) +
+ {bagsStr} Bags + ({kgsStr})
); @@ -236,7 +228,7 @@ export function OrderDetail({ instanceId, onDataLoad, onBack }: WiredDetailViewP TOTAL
{totalBags} Bags - ({Number(totalKgs).toLocaleString()} kg) + ({Number(totalKgs).toLocaleString()} kg)
)} diff --git a/src/components/dv/PotentialMiningTable.tsx b/src/components/dv/PotentialMiningTable.tsx index 0ad88d1..63751d7 100644 --- a/src/components/dv/PotentialMiningTable.tsx +++ b/src/components/dv/PotentialMiningTable.tsx @@ -29,18 +29,10 @@ export function PotentialMiningTable({ data, onEdit, onDelete, hideOrderDetails const absDiff = Math.abs(difference); return ( -
+
- {item.productName} + {item.productName} {item.badge && (
-
+
- Potential - {item.potentialKgs} kg + Potential + {item.potentialKgs} kg
{!hideOrderDetails && ( <>
- Ordered - {item.orderedKgs} kg + Ordered + {item.orderedKgs} kg
- Diff - + Diff + {isSurplus ? '+' : '-'}{absDiff}
@@ -83,7 +75,7 @@ export function PotentialMiningTable({ data, onEdit, onDelete, hideOrderDetails @@ -92,7 +84,7 @@ export function PotentialMiningTable({ data, onEdit, onDelete, hideOrderDetails diff --git a/src/components/dv/StoreDetail.tsx b/src/components/dv/StoreDetail.tsx index a8d72c0..1341591 100644 --- a/src/components/dv/StoreDetail.tsx +++ b/src/components/dv/StoreDetail.tsx @@ -190,8 +190,8 @@ export function StoreDetail({ instanceId, onBack, onEdit }: StoreDetailProps) { {String(businessName)}
) : ( -
- +
+
)} @@ -301,19 +301,19 @@ export function StoreDetail({ instanceId, onBack, onEdit }: StoreDetailProps) {

Route Assignment

-
-
Route
-
{routeName ? String(routeName) : '-'}
-
Code {routeCode ? String(routeCode) : '-'}
+
+
Route
+
{routeName ? String(routeName) : '-'}
+
Code {routeCode ? String(routeCode) : '-'}
-
-
Sub Route
-
{subRoute ? String(subRoute) : '-'}
+
+
Sub Route
+
{subRoute ? String(subRoute) : '-'}
-
-
Area
-
{area ? String(area) : '-'}
-
PIN {pinCode ? String(pinCode) : '-'}
+
+
Area
+
{area ? String(area) : '-'}
+
PIN {pinCode ? String(pinCode) : '-'}
@@ -330,27 +330,19 @@ export function StoreDetail({ instanceId, onBack, onEdit }: StoreDetailProps) { {potential.map((p: any, idx: number) => { if (!p.product_category && !p.quantity) return null; return ( -
-
- {p.product_category ? String(p.product_category) : '-'} +
+
+ {p.product_category ? String(p.product_category) : '-'}
-
- {p.quantity ? String(p.quantity) : '0'} Potential +
+ {p.quantity ? String(p.quantity) : '0'} Potential
); })}
-
- Total Potential +
+ Total Potential {totalPotential}
@@ -364,27 +356,27 @@ export function StoreDetail({ instanceId, onBack, onEdit }: StoreDetailProps) {

Distributor

-
-
{distributorName ? String(distributorName) : '-'}
-
Owner · {distributorOwnerName ? String(distributorOwnerName) : '-'}
+
+
{distributorName ? String(distributorName) : '-'}
+
Owner · {distributorOwnerName ? String(distributorOwnerName) : '-'}
-
- +
+
-
Phone
-
{String(distributorPhone)}
+
Phone
+
{String(distributorPhone)}
-
- +
+
-
Email
-
{distributorEmail ? String(distributorEmail) : '-'}
+
Email
+
{distributorEmail ? String(distributorEmail) : '-'}
diff --git a/src/components/forms/DynamicForm.tsx b/src/components/forms/DynamicForm.tsx index 8ed77da..e278f88 100644 --- a/src/components/forms/DynamicForm.tsx +++ b/src/components/forms/DynamicForm.tsx @@ -204,6 +204,77 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: // Auto-calculate order_details totals const getBaseIdForField = (id: string) => id.replace(/_\d+$/, ''); const fieldDef = schema?.fields.find(f => f.id === fieldId); + + // Auto-fill dataset keys from _raw for select/multiselect fields + if (fieldDef && (fieldDef.data_type === 'select' || fieldDef.data_type === 'multiselect') && fieldDef.properties?.options) { + const option = fieldDef.properties.options.find(o => String(o.value) === String(newVal)); + if (option && option._raw) { + const rawData = option._raw; + const getBaseId = (id: string) => id.replace(/_\d+$/, ''); + const baseFieldId = getBaseId(fieldId); + const prefixMatch = baseFieldId.match(/^([a-z]+)_/); + const prefix = prefixMatch ? prefixMatch[1] + '_' : ''; + + for (const key of Object.keys(rawData)) { + const lowerKey = key.toLowerCase(); + let targetCol = schema?.fields.find(c => + c.id.toLowerCase() === lowerKey || + (c.mapped_workflow_field || '').toLowerCase() === lowerKey || + getBaseId(c.id).toLowerCase() === lowerKey || + c.name.toLowerCase().replace(/\s+/g, '') === lowerKey.replace(/_/g, '') + ); + + if (fieldId.startsWith('route_name') && targetCol) { + const baseTargetId = getBaseId(targetCol.id).toLowerCase(); + if (baseTargetId === 'owner_name' || baseTargetId === 'phone' || baseTargetId === 'phone_number' || baseTargetId === 'email') { + targetCol = undefined; + } + } + + if (!targetCol && (fieldId === 'route_name_2' || fieldId.startsWith('route_name'))) { + targetCol = schema?.fields.find(c => { + const mappedName = (c.mapped_workflow_field || '').toLowerCase(); + const nameFallback = (c.name || '').toLowerCase().replace(/\s+/g, '_'); + const fieldName = mappedName || nameFallback; + + return fieldName === `distributor_${lowerKey}` || + (lowerKey === 'phone' && fieldName === 'distributor_phone_number') || + (lowerKey === 'email' && fieldName === 'distributor_email'); + }); + } + + if (!targetCol) { + const pKey = prefix + lowerKey; + targetCol = schema?.fields.find(c => + c.id.toLowerCase() === pKey || + (c.mapped_workflow_field || '').toLowerCase() === pKey || + getBaseId(c.id).toLowerCase() === pKey || + c.name.toLowerCase().replace(/\s+/g, '') === pKey.replace(/_/g, '') + ); + } + + if (targetCol && targetCol.id !== fieldId) { + let fillVal = rawData[key]; + + if ((targetCol.data_type === 'select' || targetCol.data_type === 'multiselect') && targetCol.properties?.options) { + const matchedOpt = targetCol.properties.options.find( + (o: any) => String(o.value).toLowerCase() === String(fillVal).toLowerCase() + ); + if (matchedOpt) { + fillVal = matchedOpt.value; + } + } + + console.log(`[Auto-fill] Mapping _raw key "${key}" to form field "${targetCol.id}" with value:`, fillVal); + next[targetCol.id] = fillVal; + } else if (!targetCol && key !== fieldId) { + console.log(`[Auto-fill] Mapping _raw key "${key}" directly to form state with value:`, rawData[key]); + next[key] = rawData[key]; + } + } + } + } + const isOrderDetails = fieldId === 'order_details' || getBaseIdForField(fieldId) === 'order_details' || fieldDef?.mapped_workflow_field === 'order_details'; if (isOrderDetails && Array.isArray(newVal)) { @@ -485,6 +556,7 @@ export function DynamicForm({ client, activityId: initialActivityId, instanceId: columns={f.columns || []} value={(val as Record[]) || []} onChange={(newVal) => handleFieldChange(f.id, newVal)} + isActivityPotentialMining={currentActivityId === ORDER_BOOKING.activities.POTENTIAL_MINING.uid} /> ); } diff --git a/src/components/forms/fields/GeolocationInput.tsx b/src/components/forms/fields/GeolocationInput.tsx index 17e9150..cc81063 100644 --- a/src/components/forms/fields/GeolocationInput.tsx +++ b/src/components/forms/fields/GeolocationInput.tsx @@ -1,5 +1,19 @@ -import { useState } from 'react'; +import { useState, useEffect, useCallback } from 'react'; +import { Modal } from '../../reusable/Modal'; import { Button } from '../../buttons/Button'; +import { useJsApiLoader, GoogleMap, Marker } from '@react-google-maps/api'; +import { X } from "lucide-react"; + +const mapContainerStyle = { + width: '100%', + height: '400px', + borderRadius: '8px' +}; + +const defaultCenter = { + lat: 20.5937, + lng: 78.9629 +}; export function GeolocationInput({ label, @@ -9,11 +23,29 @@ export function GeolocationInput({ }: { label: string; required?: boolean; - value: { latitude: number; longitude: number } | null; - onChange: (val: { latitude: number; longitude: number } | null) => void; + value: { latitude: number; longitude: number; accuracy?: number } | null; + onChange: (val: { latitude: number; longitude: number; accuracy?: number } | null) => void; }) { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + + const [isMapModalOpen, setIsMapModalOpen] = useState(false); + const [mapMarkerPos, setMapMarkerPos] = useState<{lat: number, lng: number} | 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 || '') + }); + + // Auto-fetch location on first mount if no value is present + useEffect(() => { + if (!value) { + fetchLocation(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const fetchLocation = () => { if (!navigator.geolocation) { @@ -27,6 +59,7 @@ export function GeolocationInput({ onChange({ latitude: position.coords.latitude, longitude: position.coords.longitude, + accuracy: position.coords.accuracy, }); setLoading(false); }, @@ -38,24 +71,120 @@ export function GeolocationInput({ ); }; + const handleOpenMap = () => { + if (value) { + setMapMarkerPos({ lat: value.latitude, lng: value.longitude }); + } else { + setMapMarkerPos(null); + } + setIsMapModalOpen(true); + }; + + const handleMapClick = useCallback((e: google.maps.MapMouseEvent) => { + if (e.latLng) { + setMapMarkerPos({ + lat: e.latLng.lat(), + lng: e.latLng.lng() + }); + } + }, []); + + const confirmMapSelection = () => { + if (mapMarkerPos) { + onChange({ + latitude: mapMarkerPos.lat, + longitude: mapMarkerPos.lng + }); + setIsMapModalOpen(false); + } + }; + + const clear = () => onChange(null); + const hasValue = !!value; + return ( -
+
{label} {required && *} -
- - {value && ( - + +
+
+ + +
+ + {hasValue ? ( +
+ {value.latitude.toFixed(5)}, {value.longitude.toFixed(5)} + +
+ ) : ( + + No location set )}
- {error && {error}} + + {error && {error}} {required && !value && } + + {isMapModalOpen && ( + setIsMapModalOpen(false)} + title="Select Location" + width="lg" + > +
+

Click on the map to place a pin at your desired location.

+ +
+ {!isLoaded ? ( + Loading Map... + ) : ( + + {mapMarkerPos && ( + + )} + + )} +
+ +
+ + +
+
+
+ )}
); } diff --git a/src/components/forms/fields/SmartGridField.tsx b/src/components/forms/fields/SmartGridField.tsx index 463785f..eb39499 100644 --- a/src/components/forms/fields/SmartGridField.tsx +++ b/src/components/forms/fields/SmartGridField.tsx @@ -10,13 +10,15 @@ export function SmartGridField({ columns, value = [], onChange, + isActivityPotentialMining = false, }: { label: string; columns: FormScreenField[]; value: Record[]; onChange: (val: Record[]) => void; + isActivityPotentialMining?: boolean; }) { - const isPotentialMining = label.toLowerCase().includes('potential'); + const isPotentialMining = isActivityPotentialMining; const formRef = useRef(null); const [newRow, setNewRow] = useState>({}); const [editingIdx, setEditingIdx] = useState(null); @@ -258,7 +260,7 @@ export function SmartGridField({ }; return ( -
+
{label} {value.length === 0 ? ( @@ -325,7 +327,7 @@ export function SmartGridField({

Edit Row

-
+
{productName}
{(!isPotentialMining || hasBagsCol) && ( @@ -354,7 +356,7 @@ export function SmartGridField({ } return ( -
+
{productName}
@@ -402,7 +404,7 @@ export function SmartGridField({ }`} >
-
+

Add New Item

@@ -411,11 +413,8 @@ export function SmartGridField({
-
diff --git a/src/components/maps/NearestStoresMap.tsx b/src/components/maps/NearestStoresMap.tsx new file mode 100644 index 0000000..687cb71 --- /dev/null +++ b/src/components/maps/NearestStoresMap.tsx @@ -0,0 +1,238 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useJsApiLoader, GoogleMap, Marker, InfoWindow } from '@react-google-maps/api'; +import { PIPELINE, BASE_URL } from '../../api/config'; +import { Loader2, MapPin } from 'lucide-react'; + +// Interfaces for the API response +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%', + minHeight: '400px', + borderRadius: '16px' +}; + +const defaultCenter = { lat: 12.9716, lng: 77.5946 }; + +export function NearestStoresMap() { + const [userLocation, setUserLocation] = useState<{ lat: number, lng: number } | null>(null); + const [stores, setStores] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [selectedStore, setSelectedStore] = useState(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) => { + setLoading(true); + setError(null); + try { + const response = await fetch(`${BASE_URL}${PIPELINE.endpoints.nearestStores}`, { + method: 'POST', + headers: { + 'accept': 'application/json, text/plain, */*', + 'content-type': 'application/json', + 'groupid': '25', + 'orgid': '57', + 'templateid': '189', + 'x-pipeline-version': 'draft' + }, + body: JSON.stringify({ + latitude: lat, + longitude: lng + }) + }); + + if (!response.ok) { + throw new Error(`Failed to fetch stores: ${response.statusText}`); + } + + const data = await response.json() as NearestStoresResponse; + if (data.response && data.response.stores) { + setStores(data.response.stores); + } + } catch (err: any) { + setError(err.message || 'An error occurred while fetching stores.'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (!navigator.geolocation) { + setError('Geolocation is not supported by your browser.'); + return; + } + + setLoading(true); + 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'); + setLoading(false); + }, + { enableHighAccuracy: true } + ); + }, []); + + const onLoad = useCallback(function callback(map: google.maps.Map) { + if (userLocation) { + const bounds = new window.google.maps.LatLngBounds(); + bounds.extend(userLocation); + stores.forEach(store => { + if (store.location) { + bounds.extend({ lat: store.location.latitude, lng: store.location.longitude }); + } + }); + if (stores.length > 0) { + map.fitBounds(bounds); + } + } + }, [userLocation, stores]); + + if (!isLoaded) { + return ( +
+ +
+ ); + } + + return ( +
+ {error && ( +
+ {error} +
+ )} + + {loading && !userLocation && ( +
+ + Locating you... +
+ )} + +
+ + {/* User's Current Location Marker */} + {userLocation && ( + + )} + + {/* Stores Markers */} + {stores.map((store) => ( + store.location && ( + setSelectedStore(store)} + title={store.business_name} + /> + ) + ))} + + {/* Info Window for Selected Store */} + {selectedStore && selectedStore.location && ( + setSelectedStore(null)} + > +
+

{selectedStore.business_name}

+

{selectedStore.area}

+
+ + {selectedStore.route_name} + + + {selectedStore.distance_km} km + +
+
+
+ )} +
+
+ +
+

+ + Nearest Stores ({stores.length}) +

+ {loading && stores.length === 0 && ( +
+ Fetching nearby stores... +
+ )} + {!loading && stores.length === 0 && userLocation && ( +
+ No stores found nearby. +
+ )} +
+ {stores.map(store => ( +
setSelectedStore(store)}> +
+ {store.business_name} +
+

{store.area}

+
+ {store.route_name} + {store.distance_km} km +
+
+ ))} +
+
+
+ ); +} diff --git a/src/components/reusable/Modal.tsx b/src/components/reusable/Modal.tsx index d843032..2060679 100644 --- a/src/components/reusable/Modal.tsx +++ b/src/components/reusable/Modal.tsx @@ -62,7 +62,7 @@ export function Modal({ open, onClose, title, subtitle, width = 'md', maxHeight
-
+
{title &&

{title}

} {subtitle &&
{subtitle}
} @@ -72,9 +72,9 @@ export function Modal({ open, onClose, title, subtitle, width = 'md', maxHeight
diff --git a/src/components/rv/RecordView.tsx b/src/components/rv/RecordView.tsx index 3e964f4..275f2e0 100644 --- a/src/components/rv/RecordView.tsx +++ b/src/components/rv/RecordView.tsx @@ -174,13 +174,13 @@ export function RecordView({ setShowFilters(true); }} className={cn( - "relative flex items-center justify-center w-11 h-11 rounded-lg transition-colors shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-[#1F4D36]", - "bg-[#1F4D36] text-white border border-[#1F4D36] hover:opacity-90" + "relative flex items-center justify-center w-11 h-11 rounded-lg transition-colors shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-primary", + "bg-primary text-white border border-primary hover:opacity-90" )} > {Object.values(activeFilters).filter(Boolean).length > 0 && ( - + {Object.values(activeFilters).filter(Boolean).length} )} diff --git a/src/screens/AllDailyLogsPage.tsx b/src/screens/AllDailyLogsPage.tsx index 0afbf32..38ec833 100644 --- a/src/screens/AllDailyLogsPage.tsx +++ b/src/screens/AllDailyLogsPage.tsx @@ -38,7 +38,7 @@ export function AllDailyLogsPage() { {/* Global Floating Action Button for Punch In */} diff --git a/src/screens/CallsPage.tsx b/src/screens/CallsPage.tsx index 7fc8061..d0c4a85 100644 --- a/src/screens/CallsPage.tsx +++ b/src/screens/CallsPage.tsx @@ -168,7 +168,7 @@ export function CallsPage() { {/* Global Floating Action Button for Add Call */} diff --git a/src/screens/DailyLogsPage.tsx b/src/screens/DailyLogsPage.tsx index 035a086..497b690 100644 --- a/src/screens/DailyLogsPage.tsx +++ b/src/screens/DailyLogsPage.tsx @@ -8,6 +8,7 @@ import { ClipboardList } from 'lucide-react'; import { DynamicForm } from '../components/forms/DynamicForm'; import { DAILY_REPORTS } from '../api/config'; import { dailyReportsClient } from '../api/clients'; +import { NearestStoresMap } from '../components/maps/NearestStoresMap'; export function DailyLogsPage() { const params = useParams(); @@ -20,8 +21,11 @@ export function DailyLogsPage() { const [refreshKey, setRefreshKey] = useState(0); return ( - <> - +
+ + + { @@ -33,11 +37,12 @@ export function DailyLogsPage() { setPunchOutTitle("Punch Out"); }} /> +
{/* Global Floating Action Button for Punch In */} @@ -89,6 +94,6 @@ export function DailyLogsPage() { > {instanceId != null && } - +
); } diff --git a/src/screens/DailySalesReportPage.tsx b/src/screens/DailySalesReportPage.tsx index 3a69a8f..e675110 100644 --- a/src/screens/DailySalesReportPage.tsx +++ b/src/screens/DailySalesReportPage.tsx @@ -9,6 +9,7 @@ import { import { Download, Printer } from 'lucide-react'; import jsPDF from 'jspdf'; import autoTable from 'jspdf-autotable'; +import { dailyReportsClient } from '../api/clients'; export function DailySalesReportPage() { const [date, setDate] = useState(''); @@ -134,27 +135,13 @@ export function DailySalesReportPage() { setError(null); setReportData(null); try { - const res = await fetch('https://sandbox.getzino.in/api/papi2/daily-sales-report', { - method: 'POST', - headers: { - 'TemplateID': '157', - 'Accept': 'application/json, text/plain, */*', - 'Content-Type': 'application/json', - 'sec-ch-ua-platform': '"Android"', - }, - body: JSON.stringify({ - date, - route, - distributor, - so_name: soName, - }) + const data = await dailyReportsClient.dailySalesReport({ + date, + route, + distributor, + so_name: soName, }); - if (!res.ok) { - throw new Error(`Error: ${res.status} ${res.statusText}`); - } - - const data = await res.json(); setReportData(data); } catch (err) { setError((err as Error).message ?? 'Failed to fetch report'); diff --git a/src/screens/OrdersPage.tsx b/src/screens/OrdersPage.tsx index aa7c2c3..0e66b3a 100644 --- a/src/screens/OrdersPage.tsx +++ b/src/screens/OrdersPage.tsx @@ -169,7 +169,7 @@ export function OrdersPage() { diff --git a/src/screens/StoresPage.tsx b/src/screens/StoresPage.tsx index a625d99..66d4018 100644 --- a/src/screens/StoresPage.tsx +++ b/src/screens/StoresPage.tsx @@ -52,23 +52,25 @@ export function StoresPage() { } return ( - <> - { - const id = row.instance_id as number | string | undefined; - if (id != null) navigate(`/stores/${id}`); - }} - onEditRow={(row) => { - setEditingInstanceId(row.instance_id as string | number); - setEditTitle("Edit Store"); - }} - /> +
+
+ { + const id = row.instance_id as number | string | undefined; + if (id != null) navigate(`/stores/${id}`); + }} + onEditRow={(row) => { + setEditingInstanceId(row.instance_id as string | number); + setEditTitle("Edit Store"); + }} + /> +
{/* Global Floating Action Button for Create Store */} @@ -111,6 +113,6 @@ export function StoresPage() { /> )} - +
); } diff --git a/src/styles/styles.css b/src/styles/styles.css index e9c617a..aca973e 100644 --- a/src/styles/styles.css +++ b/src/styles/styles.css @@ -115,6 +115,17 @@ --text-4xl: var(--text-4xl); --text-5xl: var(--text-5xl); --text-6xl: var(--text-6xl); + + /* ---- Custom App Colors ---- */ + --color-primary: var(--primary-color); + --color-primary-light-bg: #e8fbf0; + --color-primary-light-border: #bcecd0; + + /* Custom Mint and Ink for Location UI */ + --color-mint: #dcfce7; + --color-mint-foreground: #065f46; + --teal-ink: #064e3b; + --violet-ink: #6d28d9; } @layer base {