From 0dcf807d02ba283031be5556e91adb25719a63aa Mon Sep 17 00:00:00 2001 From: Manvi Date: Fri, 10 Jul 2026 10:22:42 +0530 Subject: [PATCH 1/4] Feature:Implement village search on CC-usage page --- src/pages/PlansPage.jsx | 66 ++++++++++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/src/pages/PlansPage.jsx b/src/pages/PlansPage.jsx index 111071d8..2d3aa820 100644 --- a/src/pages/PlansPage.jsx +++ b/src/pages/PlansPage.jsx @@ -17,6 +17,7 @@ import LandingNavbar from "../components/landing_navbar.jsx"; import FilterListIcon from "@mui/icons-material/FilterList"; import SelectReact from "react-select"; import StewardDetailPage from "../components/steward_detailPage.jsx"; +import { useMap, useMapsLibrary } from "@vis.gl/react-google-maps"; const P = { base: "oklch(49.6% 0.265 301.924)", @@ -437,6 +438,8 @@ const PlansPage = () => { const [planDotsVisible, setPlanDotsVisible] = useState(false); const [dprStatus, setDprStatus] = useState(null); const [statusTracking, setStatusTracking] = useState(null); + const [selectedVillage, setSelectedVillage] = useState(null); + const [villageOptions, setVillageOptions] = useState([]); // ── MAP INIT ──────────────────────────────────────────────── useEffect(() => { @@ -1244,6 +1247,18 @@ const PlansPage = () => { return match?.organization ?? null; }; + const handleVillageChange = (selected) => { + setSelectedVillage(selected); + if (!selected) return; + + const village = selected.value; + const plan = statePlansRef.current?.find((p) => p.village === village); + if (!plan) return; + + const orgId = getStewardOrgId(plan.facilitator_name); + handleOrgChange({ value: orgId, label: plan.facilitator_name }); + }; + const filteredOrgOptions = isStateView ? organizationOptions : (metaStats?.organization_breakdown ?? []).map(o => ({ @@ -1316,22 +1331,45 @@ const PlansPage = () => { )} -
-
-
- -
- +
+ {/* Organization Filter */} +
+
+ +
+ + +
+ + {/* Village Filter */} +
+
+
+ +
+
{/* PLAN STATUS LEGEND */} {planDotsVisible && ( From 2a61d4767f9ef1f34fb17ff4b426bbc4ec63181a Mon Sep 17 00:00:00 2001 From: Manvi Date: Fri, 10 Jul 2026 12:50:35 +0530 Subject: [PATCH 2/4] Feature:Implement village search on CC-usage page --- src/pages/PlansPage.jsx | 134 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 126 insertions(+), 8 deletions(-) diff --git a/src/pages/PlansPage.jsx b/src/pages/PlansPage.jsx index 2d3aa820..caf78390 100644 --- a/src/pages/PlansPage.jsx +++ b/src/pages/PlansPage.jsx @@ -441,6 +441,99 @@ const PlansPage = () => { const [selectedVillage, setSelectedVillage] = useState(null); const [villageOptions, setVillageOptions] = useState([]); + // NEW — village search via Google Places Autocomplete + const placesLib = useMapsLibrary("places"); + const autocompleteServiceRef = useRef(null); + const placesServiceRef = useRef(null); + + const [villageSearchText, setVillageSearchText] = useState(""); + const [villageSuggestions, setVillageSuggestions] = useState([]); + + useEffect(() => { + if (!placesLib) return; + autocompleteServiceRef.current = new placesLib.AutocompleteService(); + // PlacesService needs a map or a div — a detached div works fine, nothing renders + placesServiceRef.current = new placesLib.PlacesService(document.createElement("div")); + }, [placesLib]); + + const handleVillageInputChange = (text) => { + setVillageSearchText(text); + if (!text.trim() || !autocompleteServiceRef.current) { + setVillageSuggestions([]); + return; + } + autocompleteServiceRef.current.getPlacePredictions( + { input: text, componentRestrictions: { country: "in" } }, + (predictions) => setVillageSuggestions(predictions || []) + ); + }; + + const handleVillageSuggestionSelect = (placeId, description) => { + setVillageSearchText(description); + setVillageSuggestions([]); + placesServiceRef.current?.getDetails( + { placeId, fields: ["geometry", "address_components", "name"] }, + async (place) => { + const loc = place?.geometry?.location; + if (loc) { + mapRef.current?.getView().animate({ + center: [loc.lng(), loc.lat()], + zoom: 14, + duration: 700, + }); + } + + const components = place?.address_components ?? []; + const stateName = components.find((c) => c.types.includes("administrative_area_level_1"))?.long_name; + const districtName = components.find((c) => c.types.includes("administrative_area_level_2"))?.long_name; + const searchTerm = (place?.name || description.split(",")[0] || "").toLowerCase(); + + // 👇 NEW — clear state bubbles and district pins, we're drilling in + if (bubbleLayerRef.current) { + mapRef.current.removeLayer(bubbleLayerRef.current); + bubbleLayerRef.current = null; + } + if (districtLayerRef.current) { + mapRef.current.removeLayer(districtLayerRef.current); + districtLayerRef.current = null; + } + setIsStateView(false); + + try { + // 1️⃣ Try district-level match first (more specific) + if (districtName) { + const nameToId = Object.fromEntries( + Object.entries(districtLookupRef.current).map(([id, name]) => [name.toLowerCase(), Number(id)]) + ); + const districtId = nameToId[districtName.toLowerCase()]; + if (districtId) { + const plans = await fetchPlansByDistrict(districtId, orgRef.current?.value ?? null); + addPlanDots(plans); + return; + } + } + + // 2️⃣ Fall back to state-level, filtered by village name + if (stateName && metaStatsRef.current?.state_breakdown) { + const matchedState = metaStatsRef.current.state_breakdown.find( + (s) => s.state_name?.toLowerCase() === stateName.toLowerCase() + ); + if (matchedState) { + const plans = await fetchPlansByState(matchedState.state_id, orgRef.current?.value ?? null); + const matchedPlans = plans.filter((p) => + p.village_name?.toLowerCase().includes(searchTerm) || + p.village?.toLowerCase().includes(searchTerm) + ); + addPlanDots(matchedPlans.length ? matchedPlans : plans); + } + } + } catch (err) { + console.error("Village plans fetch failed:", err); + } + } + ); + }; + // ── MAP INIT ──────────────────────────────────────────────── useEffect(() => { const map = new Map({ @@ -1351,7 +1444,7 @@ const PlansPage = () => { />
- {/* Village Filter */} + {/* Village Search */}
{
- handleVillageInputChange(e.target.value)} + placeholder="Search village name..." + className="w-full pl-9 pr-3 rounded-xl text-sm outline-none" + style={{ + minHeight: "42px", + border: "1px solid rgba(220, 200, 240, 0.8)", + boxShadow: "0 8px 24px rgba(0,0,0,0.10)", + backgroundColor: "rgba(255,255,255,0.97)", + backdropFilter: "blur(6px)", + color: P.text, + }} /> + + {villageSuggestions.length > 0 && ( +
+ {villageSuggestions.map((s) => ( +
handleVillageSuggestionSelect(s.place_id, s.description)} + className="px-3 py-2 text-sm cursor-pointer hover:bg-gray-50" + style={{ color: P.text, borderBottom: `1px solid ${P.border}` }} + > + {s.description} +
+ ))} +
+ )}
From 75600d965749a8f677c0baea16a0c54aedb20054 Mon Sep 17 00:00:00 2001 From: Manvi Date: Fri, 10 Jul 2026 16:23:09 +0530 Subject: [PATCH 3/4] Feature:Village search based on google api --- src/pages/PlansPage.jsx | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/pages/PlansPage.jsx b/src/pages/PlansPage.jsx index caf78390..5c56c9ed 100644 --- a/src/pages/PlansPage.jsx +++ b/src/pages/PlansPage.jsx @@ -468,7 +468,7 @@ const PlansPage = () => { ); }; - const handleVillageSuggestionSelect = (placeId, description) => { +const handleVillageSuggestionSelect = (placeId, description) => { setVillageSearchText(description); setVillageSuggestions([]); placesServiceRef.current?.getDetails( @@ -488,7 +488,6 @@ const PlansPage = () => { const districtName = components.find((c) => c.types.includes("administrative_area_level_2"))?.long_name; const searchTerm = (place?.name || description.split(",")[0] || "").toLowerCase(); - // 👇 NEW — clear state bubbles and district pins, we're drilling in if (bubbleLayerRef.current) { mapRef.current.removeLayer(bubbleLayerRef.current); bubbleLayerRef.current = null; @@ -498,6 +497,7 @@ const PlansPage = () => { districtLayerRef.current = null; } setIsStateView(false); + setMapLoading(true); try { // 1️⃣ Try district-level match first (more specific) @@ -507,8 +507,17 @@ const PlansPage = () => { ); const districtId = nameToId[districtName.toLowerCase()]; if (districtId) { - const plans = await fetchPlansByDistrict(districtId, orgRef.current?.value ?? null); + currentDistrictRef.current = { district_id: districtId, district_name: districtName }; + const [plans, districtStats, dprData, trackingData] = await Promise.all([ + fetchPlansByDistrict(districtId, orgRef.current?.value ?? null), + fetchMetaStats(orgRef.current?.value ?? null, null, districtId), + fetchDprReportStatus({ organizationId: orgRef.current?.value ?? null, districtId }), + fetchStatusTracking({ organizationId: orgRef.current?.value ?? null, districtId }), + ]); addPlanDots(plans); + setMetaStats(districtStats); + setDprStatus(dprData); + setStatusTracking(trackingData); return; } } @@ -519,16 +528,27 @@ const PlansPage = () => { (s) => s.state_name?.toLowerCase() === stateName.toLowerCase() ); if (matchedState) { - const plans = await fetchPlansByState(matchedState.state_id, orgRef.current?.value ?? null); + currentStateRef.current = { state_id: matchedState.state_id, state_name: matchedState.state_name }; + const [plans, stateStats, dprData, trackingData] = await Promise.all([ + fetchPlansByState(matchedState.state_id, orgRef.current?.value ?? null), + fetchMetaStats(orgRef.current?.value ?? null, matchedState.state_id), + fetchDprReportStatus({ organizationId: orgRef.current?.value ?? null, stateId: matchedState.state_id }), + fetchStatusTracking({ organizationId: orgRef.current?.value ?? null, stateId: matchedState.state_id }), + ]); const matchedPlans = plans.filter((p) => p.village_name?.toLowerCase().includes(searchTerm) || p.village?.toLowerCase().includes(searchTerm) ); addPlanDots(matchedPlans.length ? matchedPlans : plans); + setMetaStats(stateStats); + setDprStatus(dprData); + setStatusTracking(trackingData); } } } catch (err) { console.error("Village plans fetch failed:", err); + } finally { + setMapLoading(false); } } ); From 9ae9685ff1ed77b2f2b69fb368db10dc8c335d3a Mon Sep 17 00:00:00 2001 From: Manvi Date: Fri, 17 Jul 2026 15:58:26 +0530 Subject: [PATCH 4/4] Fix:Village search issue, wrong data --- package.json | 1 + src/pages/PlansPage.jsx | 38 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 76b8dc14..bdbba20d 100755 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "chartjs-plugin-annotation": "^3.1.0", "date-fns": "^4.1.0", "dotenv": "^16.4.7", + "fastest-levenshtein": "^1.0.16", "file-saver": "^2.0.5", "html2canvas": "^1.4.1", "jspdf": "^4.2.0", diff --git a/src/pages/PlansPage.jsx b/src/pages/PlansPage.jsx index 5c56c9ed..f95c66d4 100644 --- a/src/pages/PlansPage.jsx +++ b/src/pages/PlansPage.jsx @@ -18,6 +18,7 @@ import FilterListIcon from "@mui/icons-material/FilterList"; import SelectReact from "react-select"; import StewardDetailPage from "../components/steward_detailPage.jsx"; import { useMap, useMapsLibrary } from "@vis.gl/react-google-maps"; +import { distance } from "fastest-levenshtein"; const P = { base: "oklch(49.6% 0.265 301.924)", @@ -468,6 +469,28 @@ const PlansPage = () => { ); }; + const findClosestDistrict = (districtName, nameToId) => { + const search = districtName.toLowerCase().trim(); + + let bestKey = null; + let bestDistance = Infinity; + + Object.keys(nameToId).forEach((key) => { + const d = distance(search, key.toLowerCase()); + + if (d < bestDistance) { + bestDistance = d; + bestKey = key; + } + }); + + console.log( + `Closest match: ${districtName} → ${bestKey} (distance: ${bestDistance})` + ); + + return bestDistance <= 2 ? nameToId[bestKey] : null; +}; + const handleVillageSuggestionSelect = (placeId, description) => { setVillageSearchText(description); setVillageSuggestions([]); @@ -485,8 +508,14 @@ const handleVillageSuggestionSelect = (placeId, description) => { const components = place?.address_components ?? []; const stateName = components.find((c) => c.types.includes("administrative_area_level_1"))?.long_name; - const districtName = components.find((c) => c.types.includes("administrative_area_level_2"))?.long_name; + const districtName = components.find((c) => c.types.includes("administrative_area_level_3"))?.long_name || + components.find((c) => c.types.includes("administrative_area_level_2"))?.long_name; + const tehsilName = components.find((c) => c.types.includes("locality"))?.long_name || place?.name; const searchTerm = (place?.name || description.split(",")[0] || "").toLowerCase(); + console.log(components); + console.log("District Name:", districtName); + // console.log("District Lookup:", nameToId[districtName?.toLowerCase()]); + console.log("Tehsil Name:", tehsilName); if (bubbleLayerRef.current) { mapRef.current.removeLayer(bubbleLayerRef.current); @@ -505,7 +534,10 @@ const handleVillageSuggestionSelect = (placeId, description) => { const nameToId = Object.fromEntries( Object.entries(districtLookupRef.current).map(([id, name]) => [name.toLowerCase(), Number(id)]) ); - const districtId = nameToId[districtName.toLowerCase()]; + let districtId = nameToId[districtName.toLowerCase()]; + if (!districtId) { + districtId = findClosestDistrict(districtName, nameToId); + } if (districtId) { currentDistrictRef.current = { district_id: districtId, district_name: districtName }; const [plans, districtStats, dprData, trackingData] = await Promise.all([ @@ -514,6 +546,8 @@ const handleVillageSuggestionSelect = (placeId, description) => { fetchDprReportStatus({ organizationId: orgRef.current?.value ?? null, districtId }), fetchStatusTracking({ organizationId: orgRef.current?.value ?? null, districtId }), ]); + console.log("District plans count:", plans.length); + addPlanDots(plans); setMetaStats(districtStats); setDprStatus(dprData);