From 81ba7edc2e2a187ff338b0e7c153606f2fe31b44 Mon Sep 17 00:00:00 2001 From: Manvi Date: Wed, 1 Jul 2026 16:04:10 +0530 Subject: [PATCH 1/6] Feature:linking plans from kyl to dpr page --- src/components/kyl_rightSidebar.jsx | 291 +++++++++++++++++++++++++++- 1 file changed, 287 insertions(+), 4 deletions(-) diff --git a/src/components/kyl_rightSidebar.jsx b/src/components/kyl_rightSidebar.jsx index 35f87863..198f91fd 100644 --- a/src/components/kyl_rightSidebar.jsx +++ b/src/components/kyl_rightSidebar.jsx @@ -1,8 +1,12 @@ // src/components/kyl_rightSidebar.jsx import KML from 'ol/format/KML'; import GeoJSON from 'ol/format/GeoJSON'; -import { Style, Stroke, Fill } from 'ol/style'; - +import { Style, Stroke, Fill, Circle as CircleStyle } from "ol/style"; +import Feature from "ol/Feature"; +import Point from "ol/geom/Point"; +import VectorLayer from "ol/layer/Vector"; +import VectorSource from "ol/source/Vector"; +import { useNavigate } from "react-router-dom"; import React from "react"; import SelectButton from "./buttons/select_button"; import filtersDetails from "../components/data/Filters.json"; @@ -13,6 +17,7 @@ import jsPDF from 'jspdf'; import autoTable from 'jspdf-autotable'; import XLSX from 'xlsx-js-style'; + const KYLRightSidebar = ({ state, district, @@ -59,8 +64,13 @@ const KYLRightSidebar = ({ const [isDownloading, setIsDownloading] = React.useState(false); const [isExportingGeo, setIsExportingGeo] = React.useState(false); const [geoExportFormat, setGeoExportFormat] = React.useState(null); + const [plans, setPlans] = React.useState([]); + const [showPlans, setShowPlans] = React.useState(false); + const [plansLoading, setPlansLoading] = React.useState(false); + const plansLayerRef = React.useRef(null); const showBothPanels = selectedMWSProfile && selectedWaterbodyProfile; + const navigate = useNavigate(); const transformName = (name) => { if (!name) return name; @@ -376,6 +386,230 @@ const KYLRightSidebar = ({ if (!mwsArrowLayerRef?.current) { console.warn("Arrow layer not ready"); return; } setShowConnectivity(prev => !prev); }; + + const fetchPlansByTehsil = async (block) => { + const res = await fetch( + `${process.env.REACT_APP_API_URL}/watershed/plans/?tehsil=${block}&filter_test_plan=true`, + { + headers: { + "Content-Type": "application/json", + "ngrok-skip-browser-warning": "1", + "X-API-Key": process.env.REACT_APP_API_KEY, + }, + } + ); + + if (!res.ok) { + throw new Error(`API Error ${res.status}`); + } + + return res.json(); +}; + + const handlePlansClick = async () => { + try { + // Hide plans if already visible + if (showPlans) { + if (plansLayerRef.current) { + mapRef.current.removeLayer(plansLayerRef.current); + } + setShowPlans(false); + return; + } + + // Plans already fetched, just show them again + if (plans.length > 0) { + showPlansOnMap(plans); + setShowPlans(true); + return; + } + + // Fetch plans first time + setPlansLoading(true); + + const response = await fetchPlansByTehsil(block.block_id); + + setPlans(response); + + showPlansOnMap(response); + + setShowPlans(true); + } catch (err) { + console.error("Error fetching plans:", err); + } finally { + setPlansLoading(false); + } + }; + +const showPlansOnMap = (plansData) => { + if (!mapRef.current) return; + + // Remove previous plans layer + if (plansLayerRef.current) { + mapRef.current.removeLayer(plansLayerRef.current); + plansLayerRef.current = null; + } + + const features = plansData + .filter( + (plan) => + plan.latitude !== null && + plan.longitude !== null && + plan.latitude !== undefined && + plan.longitude !== undefined + ) + .map((plan) => { + const feature = new Feature({ + geometry: new Point([ + Number(plan.longitude), + Number(plan.latitude), + ]), + }); + + feature.set("plan", plan); + feature.set("planDetails", plan); + + const status = getPlanStatus(plan); + + feature.setStyle(DOT_DEFAULT(status)); + + return feature; + }); + + const layer = new VectorLayer({ + source: new VectorSource({ + features, + }), + zIndex: 9999, + }); + + plansLayerRef.current = layer; + + mapRef.current.addLayer(layer); + + if (features.length > 0) { + mapRef.current.getView().fit( + layer.getSource().getExtent(), + { + padding: [40, 40, 40, 40], + duration: 500, + maxZoom: 16, + } + ); + } +}; + +React.useEffect(() => { + if (!mapRef.current) return; + + let hoveredFeature = null; + + const handlePointerMove = (evt) => { + if (!showPlans || !plansLayerRef.current) return; + + const feature = mapRef.current.forEachFeatureAtPixel( + evt.pixel, + (feature, layer) => { + if (layer === plansLayerRef.current) { + return feature; + } + } + ); + + // Restore previous hovered feature + if (hoveredFeature && hoveredFeature !== feature) { + const status = getFeatureStatus(hoveredFeature); + hoveredFeature.setStyle(DOT_DEFAULT(status)); + hoveredFeature = null; + } + + // Apply hover style + if (feature && feature !== hoveredFeature) { + const status = getFeatureStatus(feature); + feature.setStyle(DOT_HOVERED(status)); + hoveredFeature = feature; + } + + mapRef.current.getTargetElement().style.cursor = feature ? "pointer" : ""; + }; + + const handlePlanClick = (evt) => { + if (!showPlans || !plansLayerRef.current) return; + + const feature = mapRef.current.forEachFeatureAtPixel( + evt.pixel, + (feature, layer) => { + if (layer === plansLayerRef.current) { + return feature; + } + } + ); + + if (!feature) return; + + const plan = feature.get("planDetails"); + + if (!plan) return; + + navigate( + `/landscape-stewardship/plan-view?id=${plan.id}&completed=${!!plan.is_completed}&dpr_reviewed=${!!plan.is_dpr_reviewed}&dpr_generated=${!!plan.is_dpr_generated}&dpr_approved=${!!plan.is_dpr_approved}`, + { + state: { + plan, + }, + } + ); + }; + + mapRef.current.on("pointermove", handlePointerMove); + mapRef.current.on("singleclick", handlePlanClick); + + return () => { + if (mapRef.current) { + mapRef.current.un("pointermove", handlePointerMove); + mapRef.current.un("singleclick", handlePlanClick); + } + }; +}, [showPlans, navigate]); + +// ── PLAN DOT STYLES ────────────────────────────────────────────────────────── + +const PLAN_STATUS_COLORS = { + in_progress: { fill: "#FF6FFF", stroke: "#ffffff" }, // magenta + dpr_completed: { fill: "#CCFF00", stroke: "#3E5800" }, // chartreuse +}; + +const getPlanStatus = (plan) => { + if (!plan) return "in_progress"; + if (plan.is_dpr_reviewed) return "dpr_completed"; + return "in_progress"; +}; + +const getFeatureStatus = (feature) => getPlanStatus(feature.get("planDetails")); + +const DOT_DEFAULT = (status = "in_progress") => new Style({ + image: new CircleStyle({ + radius: 9, + fill: new Fill({ color: PLAN_STATUS_COLORS[status].fill }), + stroke: new Stroke({ color: PLAN_STATUS_COLORS[status].stroke, width: 2 }), + }), +}); + +const DOT_HOVERED = (status = "in_progress") => new Style({ + image: new CircleStyle({ + radius: 12, + fill: new Fill({ color: PLAN_STATUS_COLORS[status].fill }), + stroke: new Stroke({ color: "#ffffff", width: 2.5 }), + }), +}); + +const DOT_SELECTED = (status = "in_progress") => new Style({ + image: new CircleStyle({ + radius: 12, + fill: new Fill({ color: "#ffffff" }), + stroke: new Stroke({ color: PLAN_STATUS_COLORS[status].fill, width: 3.5 }), + }), +}); const handleTehsilReport = () => { const reportURL = `${process.env.REACT_APP_API_URL}/generate_tehsil_report/?state=${transformName(state?.label)}&district=${transformName(district?.label)}&block=${transformName(block?.label)}`; @@ -1464,8 +1698,8 @@ const KYLRightSidebar = ({ {block && (
-
- +
+ + + +
); - console.log("selectedMWSProfile:", selectedMWSProfile); -console.log("manualSelectedMWS:", manualSelectedMWS); -console.log("selectionMode:", selectionMode); - return (
@@ -1688,8 +1686,84 @@ console.log("selectionMode:", selectionMode); )} - {!selectedMWSProfile && !selectedWaterbodyProfile && ( -
+ {selectedPlanProfile && ( +
+ + {/* Header */} +
+ + + +
+ + {/* + Plan Overview + + + | */} + +

+ {selectedPlanProfile.plan} +

+ +
+ +
+ + {/* Information Card */} +
+ +
+ Village + + {selectedPlanProfile.village_name || "--"} + +
+ +
+ Steward + + {selectedPlanProfile.facilitator_name || "--"} + +
+ +
+ Organization + + {selectedPlanProfile.organization_name || "--"} + +
+ +
+ + {/* View DPR Button */} + + +
+ )} + + {!selectedMWSProfile && !selectedWaterbodyProfile && !selectedPlanProfile && ( +
{/* ── Hint banner ── */}
From 7fe526663e757e57352d89e904eb13e8861d1bf7 Mon Sep 17 00:00:00 2001 From: Manvi Date: Thu, 2 Jul 2026 13:03:27 +0530 Subject: [PATCH 3/6] Fix:Back button was not working properly --- src/components/kyl_mapContainer.jsx | 46 ++++++++- src/components/kyl_rightSidebar.jsx | 62 ++++++++---- src/pages/PlanViewPage.jsx | 32 +++++- src/pages/PlansPage.jsx | 150 ++++++++++++++++++++++------ src/pages/kyl_dashboard.jsx | 8 +- 5 files changed, 240 insertions(+), 58 deletions(-) diff --git a/src/components/kyl_mapContainer.jsx b/src/components/kyl_mapContainer.jsx index 000f523e..45c07283 100644 --- a/src/components/kyl_mapContainer.jsx +++ b/src/components/kyl_mapContainer.jsx @@ -161,12 +161,12 @@ const MapZoomControls = ({ mapRef }) => { }; // Updated MapLegend component -const MapLegend = ({ showMWS, showVillages, currentLayer, showConnectivity }) => { +const MapLegend = ({ showMWS, showVillages, currentLayer, showConnectivity,showPlans,setShowPlans }) => { // Add state for collapsed status const [isCollapsed, setIsCollapsed] = useState(false); // If no layers are shown, don't display legend - if (!showMWS && !showVillages && (!currentLayer || currentLayer.length === 0)) + if (!showMWS && !showVillages && !showPlans && (!currentLayer || currentLayer.length === 0)) return null; const activeWBLayer = currentLayer?.find((layer) => @@ -202,6 +202,19 @@ const MapLegend = ({ showMWS, showVillages, currentLayer, showConnectivity }) => }, ]; + const planLegendItems = [ + { + color: "#FF6FFF", + border: "#ffffff", + name: "In Progress", + }, + { + color: "#CCFF00", + border: "#3E5800", + name: "DPR Reviewed", + }, +]; + const lulcLegendItems = [ { color: "#A9A9A9", label: "Barren Lands" }, { color: "#F0F4A3", label: "Single Kharif" }, @@ -843,6 +856,33 @@ const MapLegend = ({ showMWS, showVillages, currentLayer, showConnectivity }) =>
)} + {/* Plans Legend Section */} +{showPlans && ( +
+

+ Plans +

+ + {planLegendItems.map((item, index) => ( +
+
+ + {item.name} + +
+ ))} +
+)} + {/* LULC Legend Section */} {isLulcLayerActive && (
@@ -2045,6 +2085,7 @@ const KYLMapContainer = ({ currentLayer, setSearchLatLong, showConnectivity, + showPlans, selectionMode, setSelectionMode, }) => { @@ -2092,6 +2133,7 @@ const KYLMapContainer = ({ showVillages={showVillages && areVillageLayersAvailable} currentLayer={currentLayer} showConnectivity={showConnectivity} + showPlans={showPlans} /> {/* Search Bar */} diff --git a/src/components/kyl_rightSidebar.jsx b/src/components/kyl_rightSidebar.jsx index cbb92871..5fd849c2 100644 --- a/src/components/kyl_rightSidebar.jsx +++ b/src/components/kyl_rightSidebar.jsx @@ -62,6 +62,8 @@ const KYLRightSidebar = ({ setSelectionMode, manualSelectedMWS, onResetMWSSelection, + showPlans, + setShowPlans, }) => { const [loadingWB, setLoadingWB] = React.useState(false); const [showSelectionPopup, setShowSelectionPopup] = React.useState(false); @@ -69,7 +71,7 @@ const KYLRightSidebar = ({ const [isExportingGeo, setIsExportingGeo] = React.useState(false); const [geoExportFormat, setGeoExportFormat] = React.useState(null); const [plans, setPlans] = React.useState([]); - const [showPlans, setShowPlans] = React.useState(false); + const [plansLoading, setPlansLoading] = React.useState(false); const [selectedPlanProfile, setSelectedPlanProfile] = React.useState(null); const plansLayerRef = React.useRef(null); @@ -557,14 +559,6 @@ React.useEffect(() => { if (!plan) return; setSelectedPlanProfile(plan); - // navigate( - // `/landscape-stewardship/plan-view?id=${plan.id}&completed=${!!plan.is_completed}&dpr_reviewed=${!!plan.is_dpr_reviewed}&dpr_generated=${!!plan.is_dpr_generated}&dpr_approved=${!!plan.is_dpr_approved}`, - // { - // state: { - // plan, - // }, - // } - // ); }; mapRef.current.on("pointermove", handlePointerMove); @@ -1743,20 +1737,48 @@ const DOT_SELECTED = (status = "in_progress") => new Style({ {/* View DPR Button */}
diff --git a/src/pages/PlanViewPage.jsx b/src/pages/PlanViewPage.jsx index 34c80b99..1b878040 100644 --- a/src/pages/PlanViewPage.jsx +++ b/src/pages/PlanViewPage.jsx @@ -515,12 +515,23 @@ const PlanViewPage = () => { const navigate = useNavigate(); const [searchParams] = useSearchParams(); - const [plan, setPlan] = useState(state?.plan ?? null); - const [deepLinkLoading, setDeepLinkLoading] = useState(!state?.plan && !!searchParams.get("id")); + // const [plan, setPlan] = useState(state?.plan ?? null); + // const [deepLinkLoading, setDeepLinkLoading] = useState(!state?.plan && !!searchParams.get("id")); + const storedState = JSON.parse( + sessionStorage.getItem("planNavigationData") || "null" + ); + const navigationState = state || storedState; + const [plan, setPlan] = useState(navigationState?.plan ?? null); + + const [deepLinkLoading, setDeepLinkLoading] = useState( + !navigationState?.plan && !!searchParams.get("id") + ); + // ── DEEP LINK FETCH ───────────────────────────────────── useEffect(() => { - if (state?.plan || !searchParams.get("id")) return; + // if (state?.plan || !searchParams.get("id")) return; + if (navigationState?.plan || !searchParams.get("id")) return; const planId = searchParams.get("id"); const load = async () => { @@ -535,6 +546,9 @@ const PlanViewPage = () => { plan: brief?.village_name ?? `Plan ${planId}`, village_name: brief?.village_name, gram_panchayat: brief?.gram_panchayat, + latitude: brief?.latitude, + longitude: brief?.longitude, + state: brief?.state, district: transformName(brief?.district ?? ""), block: transformName(brief?.tehsil ?? ""), organization_name: team?.organization, @@ -733,7 +747,9 @@ const PlanViewPage = () => { return mwsLayer; }, [districtNameSafe, blockNameSafe]); + const loadAdminBoundary = useCallback(async (map) => { + const lon = plan?.longitude ? parseFloat(plan.longitude) : null; const lat = plan?.latitude ? parseFloat(plan.latitude) : null; @@ -1066,9 +1082,15 @@ const PlanViewPage = () => {
diff --git a/src/pages/PlansPage.jsx b/src/pages/PlansPage.jsx index 111071d8..658c249b 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 { useSearchParams } from "react-router-dom"; const P = { base: "oklch(49.6% 0.265 301.924)", @@ -437,6 +438,7 @@ const PlansPage = () => { const [planDotsVisible, setPlanDotsVisible] = useState(false); const [dprStatus, setDprStatus] = useState(null); const [statusTracking, setStatusTracking] = useState(null); + const [searchParams, setSearchParams] = useSearchParams(); // ── MAP INIT ──────────────────────────────────────────────── useEffect(() => { @@ -468,6 +470,8 @@ const PlansPage = () => { return () => map.setTarget(null); }, []); + + const fetchProposedBlocks = async () => { const res = await fetch(`${process.env.REACT_APP_API_URL}/proposed_blocks/`, { method: "GET", @@ -533,25 +537,64 @@ const PlansPage = () => { } }, [mapLoading, statsLoading]); - useEffect(() => { - const ctx = location.state?.returnContext; - if (!ctx?.stateId) return; - - const tryRestore = setInterval(() => { - if (mapRef.current && metaStatsRef.current && !hasRestoredRef.current) { - clearInterval(tryRestore); - hasRestoredRef.current = true; - handleStatePinClick({ - state_id: ctx.stateId, - state_name: ctx.stateName, + // useEffect(() => { + // const ctx = location.state?.returnContext; + // if (!ctx?.stateId) return; + + // const tryRestore = setInterval(() => { + // if (mapRef.current && metaStatsRef.current && !hasRestoredRef.current) { + // clearInterval(tryRestore); + // hasRestoredRef.current = true; + // handleStatePinClick({ + // state_id: ctx.stateId, + // state_name: ctx.stateName, + // }).then(() => { + // if (ctx.districtId) { + // handleDistrictPinClick({ + // district_id: ctx.districtId, + // district_name: ctx.districtName, + // }); + // } + // }); + // } + // }, 100); + + // return () => clearInterval(tryRestore); + // }, []); + + // ── STATS ─────────────────────────────────────────────────── + + useEffect(() => { + const stateId = searchParams.get("state"); + const stateName = searchParams.get("stateName"); + const districtId = searchParams.get("district"); + const districtName = searchParams.get("districtName"); + + if (!stateId) return; + + const tryRestore = setInterval(() => { + if (mapRef.current && metaStatsRef.current && !hasRestoredRef.current) { + clearInterval(tryRestore); + hasRestoredRef.current = true; + handleStatePinClick({ + state_id: stateId, + state_name: stateName, + }).then(() => { + if (districtId) { + handleDistrictPinClick({ + district_id: districtId, + district_name: districtName, }); } - }, 100); + }); + } + }, 100); + + return () => clearInterval(tryRestore); +}, []); + - return () => clearInterval(tryRestore); - }, []); - // ── STATS ─────────────────────────────────────────────────── const loadStats = async (orgId = null, stateId = null) => { setStatsLoading(true); setStatsError(false); @@ -850,6 +893,10 @@ const PlansPage = () => { const handleStatePinClick = async (stateData) => { currentStateRef.current = stateData; setMapLoading(true); + setSearchParams({ + state: stateData.state_id, + stateName: stateData.state_name, + }, { replace: true }); if (bubbleLayerRef.current) { mapRef.current.removeLayer(bubbleLayerRef.current); @@ -898,6 +945,13 @@ const PlansPage = () => { const handleDistrictPinClick = async (districtData) => { if (!districtData) return; + setSearchParams((prev) => { + const params = new URLSearchParams(prev); + params.set("district", districtData.district_id); + params.set("districtName", districtData.district_name); + return params; + }, { replace: true }); + if (viewModeRef.current === "plans") { setMapLoading(true); currentDistrictRef.current = districtData; @@ -946,6 +1000,7 @@ const PlansPage = () => { const handleBackToStateView = async () => { const map = mapRef.current; if (!map) return; + setSearchParams({}, { replace: true }); if (planLayerRef.current) { map.removeLayer(planLayerRef.current); @@ -1014,6 +1069,12 @@ const PlansPage = () => { const map = mapRef.current; if (!map) return; + setSearchParams((prev) => { + const params = new URLSearchParams(prev); + params.delete("district"); + params.delete("districtName"); + return params; + }, { replace: true }); currentDistrictRef.current = null; if (viewModeRef.current === "plans") { @@ -1479,19 +1540,52 @@ const PlansPage = () => {
From 6829424a852eccf45f1545f4dd4f05c0909ab168 Mon Sep 17 00:00:00 2001 From: Manvi Date: Thu, 2 Jul 2026 13:11:57 +0530 Subject: [PATCH 4/6] Fix:Back button was not working properly --- src/components/kyl_rightSidebar.jsx | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/components/kyl_rightSidebar.jsx b/src/components/kyl_rightSidebar.jsx index 5fd849c2..d241f9cd 100644 --- a/src/components/kyl_rightSidebar.jsx +++ b/src/components/kyl_rightSidebar.jsx @@ -1739,19 +1739,17 @@ const DOT_SELECTED = (status = "in_progress") => new Style({ - - -
- ); -}; - -export default SelectionModeToggle; \ No newline at end of file diff --git a/src/components/kyl_MWSProfilePanel.jsx b/src/components/kyl_MWSProfilePanel.jsx index 3067f890..d6820645 100644 --- a/src/components/kyl_MWSProfilePanel.jsx +++ b/src/components/kyl_MWSProfilePanel.jsx @@ -3,9 +3,9 @@ import { stateAtom, districtAtom, blockAtom, dataJsonAtom } from '../store/locat import { useRecoilValue } from 'recoil'; import { useEffect, useState } from 'react'; import { trackEvent } from "../services/analytics.js"; -import { CheckCircle2, Layers3 } from "lucide-react"; +import { CheckCircle2, Layers3,Table } from "lucide-react"; -const KYLMWSProfilePanel = ({ mwsData, onBack, hideBackButton = false , selectionMode = "single", setSelectionMode, onResetMWS,onResetSelection, +const KYLMWSProfilePanel = ({ mwsData, onBack, hideBackButton = false, onResetMWS,onOpenSelection, selectedMWS = [],}) => { const state = useRecoilValue(stateAtom); const district = useRecoilValue(districtAtom); @@ -13,7 +13,6 @@ const KYLMWSProfilePanel = ({ mwsData, onBack, hideBackButton = false , selecti const dataJson = useRecoilValue(dataJsonAtom) const [dataString, setDataString] = useState("") - console.log("onResetMWS =>", onResetMWS); const transformName = (name) => { if (!name) return name; @@ -49,49 +48,7 @@ const KYLMWSProfilePanel = ({ mwsData, onBack, hideBackButton = false , selecti return (
-
-

- Selection Mode -

- -
- - - -
- -

- {selectionMode === "single" - ? "Only one Micro-Watershed can be selected." - : "Select multiple Micro-Watersheds by clicking on the map."} -

-
+ {!hideBackButton && (
+
))} @@ -211,6 +169,17 @@ const KYLMWSProfilePanel = ({ mwsData, onBack, hideBackButton = false , selecti
)} +{onOpenSelection && ( +
+