You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
702 lines
28 KiB
702 lines
28 KiB
// src/runtime/widget.tsx
|
|
import React, { useEffect, useRef, useState } from "react";
|
|
import { AllWidgetProps } from "jimu-core";
|
|
import FeatureLayer from "@arcgis/core/layers/FeatureLayer";
|
|
import Graphic from "@arcgis/core/Graphic";
|
|
import MapView from "@arcgis/core/views/MapView";
|
|
|
|
type SlumAttrs = {
|
|
objectid?: number | string;
|
|
slumcode?: string;
|
|
slum_cat?: string;
|
|
slum_name?: string;
|
|
area_name?: string;
|
|
locality?: string;
|
|
zone?: string;
|
|
ward?: string;
|
|
road_id?: string;
|
|
road_name?: string;
|
|
[k: string]: any;
|
|
};
|
|
|
|
export default function SlumLinkWidget(props: AllWidgetProps) {
|
|
const SLUM_BOUNDARY_URL =
|
|
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/gccdeptfield/SlumBoundary/FeatureServer/0";
|
|
const SLUM_TABLE_URL =
|
|
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/gccdeptfield/SlumBoundary/FeatureServer/1";
|
|
const ROAD_LAYER_URL =
|
|
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/Transporations/FeatureServer/0";
|
|
|
|
const slumBoundaryRef = useRef<FeatureLayer | null>(null);
|
|
const slumTableRef = useRef<FeatureLayer | null>(null);
|
|
const roadLayerRef = useRef<FeatureLayer | null>(null);
|
|
|
|
const viewRef = useRef<MapView | null>(null);
|
|
const clickHandlerRef = useRef<any>(null);
|
|
|
|
const [selectedSlum, setSelectedSlum] = useState<SlumAttrs | null>(null);
|
|
const [roadsInSlum, setRoadsInSlum] = useState<Array<{ road_id?: string; road_name?: string; attrs?: any }>>(
|
|
[]
|
|
);
|
|
const [selectedRoadIndices, setSelectedRoadIndices] = useState<number[]>([]);
|
|
const [status, setStatus] = useState<{ type: "idle" | "loading" | "success" | "error"; message?: string }>({
|
|
type: "idle",
|
|
});
|
|
|
|
// init layers
|
|
useEffect(() => {
|
|
slumBoundaryRef.current = new FeatureLayer({ url: SLUM_BOUNDARY_URL });
|
|
slumTableRef.current = new FeatureLayer({ url: SLUM_TABLE_URL });
|
|
roadLayerRef.current = new FeatureLayer({ url: ROAD_LAYER_URL });
|
|
}, []);
|
|
|
|
//
|
|
// Helper: robustly parse applyEdits result across server versions
|
|
//
|
|
function parseApplyEditsResult(res: any) {
|
|
if (!res) return { added: 0, updated: 0, deleted: 0, errors: [] as any[] };
|
|
|
|
const addResults = res.addFeatureResults || res.addResults || res.addFeatureResults || [];
|
|
const updateResults = res.updateFeatureResults || res.updateResults || [];
|
|
const deleteResults = res.deleteFeatureResults || res.deleteResults || [];
|
|
|
|
const okAdded = addResults.filter((r: any) => r && (r.success === true || Number.isFinite(r.objectId))).length;
|
|
const okUpdated = updateResults.filter((r: any) => r && (r.success === true || Number.isFinite(r.objectId))).length;
|
|
const okDeleted = deleteResults.filter((r: any) => r && (r.success === true || Number.isFinite(r.objectId))).length;
|
|
|
|
const errors: any[] = [];
|
|
for (const arr of [addResults, updateResults, deleteResults]) {
|
|
for (const r of arr) {
|
|
if (r && r.error) errors.push(r.error);
|
|
}
|
|
}
|
|
|
|
return { added: okAdded, updated: okUpdated, deleted: okDeleted, errors };
|
|
}
|
|
|
|
//
|
|
// Helper: safe queryFeatures wrapper (returns null on error and logs the where clause)
|
|
//
|
|
async function safeQueryFeatures(layer: FeatureLayer | null, where: string | null, outFields: string[] = ["*"]) {
|
|
if (!layer) return null;
|
|
if (!where || where.trim().length === 0) {
|
|
console.warn("safeQueryFeatures: skipping empty where");
|
|
return null;
|
|
}
|
|
const q = layer.createQuery();
|
|
q.where = where;
|
|
q.outFields = outFields;
|
|
q.returnGeometry = false;
|
|
q.num = 2;
|
|
|
|
console.debug("[safeQueryFeatures] where:", where);
|
|
try {
|
|
const r = await layer.queryFeatures(q);
|
|
return r;
|
|
} catch (err: any) {
|
|
console.error("[safeQueryFeatures] queryFeatures error for where:", where, err);
|
|
try {
|
|
console.debug("err.details:", err?.details);
|
|
console.debug("err.requestOptions:", err?.requestOptions);
|
|
console.debug("err.raw:", err?.raw);
|
|
} catch (e) {}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Helper: query SlumBoundary by geometry to get authoritative attributes
|
|
//
|
|
async function fetchSlumFeatureByGeometry(geometry: any): Promise<any | null> {
|
|
if (!slumBoundaryRef.current || !geometry) return null;
|
|
try {
|
|
const q = slumBoundaryRef.current.createQuery();
|
|
q.geometry = geometry;
|
|
q.spatialRelationship = "intersects";
|
|
q.returnGeometry = true;
|
|
q.outFields = ["*"];
|
|
q.num = 1;
|
|
const res = await slumBoundaryRef.current.queryFeatures(q);
|
|
if (res && res.features && res.features.length > 0) return res.features[0];
|
|
return null;
|
|
} catch (err) {
|
|
console.error("fetchSlumFeatureByGeometry error:", err);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Helper: query Slum details/table layer by slum code (layer 1)
|
|
//
|
|
async function fetchSlumDetailsByCode(code: string): Promise<any | null> {
|
|
if (!slumTableRef.current || !code) return null;
|
|
try {
|
|
const esc = (s: string) => (s || "").replace(/'/g, "''");
|
|
const where = `(slum_code = '${esc(code)}' OR slumcode = '${esc(code)}')`;
|
|
const r = await safeQueryFeatures(slumTableRef.current, where, ["*"]);
|
|
if (r && r.features && r.features.length > 0) return r.features[0];
|
|
return null;
|
|
} catch (err) {
|
|
console.error("fetchSlumDetailsByCode error:", err);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Attach click -> hitTest to read slum polygon attributes and query roads
|
|
useEffect(() => {
|
|
const maybeView = (props as any).jimuMapView?.view || (window as any).mapView || null;
|
|
if (!maybeView) return;
|
|
viewRef.current = maybeView as MapView;
|
|
|
|
const handler = viewRef.current.on("click", async (evt: any) => {
|
|
try {
|
|
setStatus({ type: "loading", message: "Identifying features..." });
|
|
const hit = await viewRef.current!.hitTest(evt);
|
|
if (!hit || !hit.results || hit.results.length === 0) {
|
|
setStatus({ type: "idle" });
|
|
return;
|
|
}
|
|
|
|
// find slum polygon result (robust detection by layer url/title containing "slum")
|
|
const slumResult = hit.results.find((r: any) => {
|
|
const l = r.graphic && r.graphic.layer;
|
|
if (!l) return false;
|
|
const u = (l.url || "").toLowerCase();
|
|
const t = (l.title || "").toLowerCase();
|
|
return u.includes("slumboundary") || t.includes("slumboundary") || t.includes("slum");
|
|
});
|
|
|
|
if (!slumResult) {
|
|
setStatus({ type: "idle" });
|
|
return;
|
|
}
|
|
|
|
const g: any = slumResult.graphic;
|
|
await handleSlumSelectedByGraphic(g);
|
|
} catch (err) {
|
|
console.error("Hit test error:", err);
|
|
setStatus({ type: "error", message: "Identify failed" });
|
|
setTimeout(() => setStatus({ type: "idle" }), 800);
|
|
}
|
|
});
|
|
|
|
clickHandlerRef.current = handler;
|
|
return () => {
|
|
if (clickHandlerRef.current && viewRef.current) clickHandlerRef.current.remove();
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [props]);
|
|
|
|
// When a slum polygon graphic is selected: populate the UI fields but DO NOT save yet.
|
|
async function handleSlumSelectedByGraphic(graphic: any) {
|
|
try {
|
|
setStatus({ type: "loading", message: "Loading slum attributes..." });
|
|
|
|
// 1) start with any attributes present on the hitTest graphic
|
|
let attrs = graphic.attributes || {};
|
|
|
|
// 2) get authoritative polygon attrs if missing slum name or slum code
|
|
const hasName = !!(attrs.slum_name || attrs.slumname || attrs.SLUMNAME);
|
|
const hasCode = !!(attrs.slumcode || attrs.slum_code || attrs.SLUM_CODE);
|
|
if ((!hasName || !hasCode) && graphic.geometry) {
|
|
const featureFromLayer = await fetchSlumFeatureByGeometry(graphic.geometry);
|
|
if (featureFromLayer && featureFromLayer.attributes) {
|
|
// merge but prefer existing attributes from graphic if present
|
|
attrs = { ...(featureFromLayer.attributes || {}), ...(attrs || {}) };
|
|
}
|
|
}
|
|
|
|
// Extract canonical slum code & name
|
|
const slumCode = attrs.slumcode || attrs.slum_code || attrs.SLUM_CODE || "";
|
|
const slumName = attrs.slum_name || attrs.slumname || attrs.SLUMNAME || "";
|
|
|
|
const slum: SlumAttrs = {
|
|
objectid: attrs.OBJECTID || attrs.objectid || attrs.objectId || attrs.ROWID,
|
|
slumcode: slumCode,
|
|
slum_cat: attrs.slum_cat || attrs.slumcategory || attrs.SLUMCATEGORY || "",
|
|
slum_name: slumName,
|
|
area_name: attrs.area_name || attrs.area || "",
|
|
locality: attrs.locality || "",
|
|
zone: attrs.zone || "",
|
|
ward: attrs.ward || "",
|
|
road_id: attrs.road_id || "",
|
|
road_name: attrs.road_name || "",
|
|
...attrs,
|
|
};
|
|
|
|
// 3) If we have a slum code, try to enrich from the Slum details (table) layer (layer 1)
|
|
if (slumCode && slumTableRef.current) {
|
|
const detailsFeature = await fetchSlumDetailsByCode(slumCode);
|
|
if (detailsFeature && detailsFeature.attributes) {
|
|
const d = detailsFeature.attributes;
|
|
// merge relevant fields (prefer the existing slum object values)
|
|
slum.slum_name = slum.slum_name || d.slum_name || d.slumname || "";
|
|
slum.area_name = slum.area_name || d.area_name || d.area || "";
|
|
slum.zone = slum.zone || d.zone || "";
|
|
slum.ward = slum.ward || d.ward || "";
|
|
// merge any other useful attributes you want:
|
|
// slum.household = slum.household || d.household;
|
|
// slum.no_ofstreets = slum.no_ofstreets || d.no_ofstreets;
|
|
slum["__slum_table_oid"] = d.OBJECTID || d.objectid || d.objectId;
|
|
}
|
|
}
|
|
|
|
// Set UI state so user can edit before saving
|
|
setSelectedSlum(slum);
|
|
setSelectedRoadIndices([]);
|
|
setRoadsInSlum([]);
|
|
|
|
// Query roads intersecting polygon (use geometry from the graphic)
|
|
if (graphic.geometry && roadLayerRef.current) {
|
|
setStatus({ type: "loading", message: "Querying roads..." });
|
|
const q = roadLayerRef.current.createQuery();
|
|
q.geometry = graphic.geometry;
|
|
q.spatialRelationship = "intersects";
|
|
q.returnGeometry = false;
|
|
q.outFields = ["Road_ID", "Road_Name", "zone", "ward", "*"];
|
|
q.num = 1000;
|
|
|
|
const res = await roadLayerRef.current.queryFeatures(q);
|
|
const matches: Array<{ road_id?: string; road_name?: string; attrs?: any }> = [];
|
|
if (res && res.features && res.features.length > 0) {
|
|
const seen = new Set<string>();
|
|
for (const f of res.features) {
|
|
const rattrs = f.attributes || {};
|
|
const rid = rattrs.Road_ID || rattrs.road_id || rattrs.RoadId || rattrs.ROAD_ID || "";
|
|
const rname = rattrs.Road_Name || rattrs.road_name || rattrs.RoadName || rattrs.Road || "";
|
|
const key = `${String(rid).trim()}__${String(rname).trim()}`;
|
|
if (!seen.has(key)) {
|
|
seen.add(key);
|
|
matches.push({ road_id: String(rid || "").trim(), road_name: String(rname || "").trim(), attrs: rattrs });
|
|
}
|
|
}
|
|
}
|
|
|
|
matches.sort((a, b) => (a.road_name || "").localeCompare(b.road_name || ""));
|
|
setRoadsInSlum(matches);
|
|
}
|
|
|
|
setStatus({ type: "success", message: "Slum loaded — review and save if needed" });
|
|
} catch (err) {
|
|
console.error("handleSlumSelectedByGraphic:", err);
|
|
setStatus({ type: "error", message: "Failed to load slum" });
|
|
} finally {
|
|
setTimeout(() => setStatus({ type: "idle" }), 900);
|
|
}
|
|
}
|
|
|
|
//
|
|
// When user selects a single road (we keep behavior: selecting one will immediately upsert that single record)
|
|
//
|
|
async function handleRoadPickSingle(idx: number) {
|
|
if (idx === -1) {
|
|
setSelectedRoadIndices([]);
|
|
return;
|
|
}
|
|
const road = roadsInSlum[idx];
|
|
if (!road || !selectedSlum) return;
|
|
|
|
// Merge authoritative road attrs into slum
|
|
const updated: SlumAttrs = { ...(selectedSlum || {}) };
|
|
updated.road_id = road.road_id || road.attrs?.Road_ID || road.attrs?.road_id || updated.road_id;
|
|
updated.road_name = road.road_name || road.attrs?.Road_Name || road.attrs?.road_name || updated.road_name;
|
|
|
|
const rattrs = road.attrs || {};
|
|
updated.zone = rattrs.zone || rattrs.Zone || updated.zone;
|
|
updated.ward = rattrs.ward || rattrs.Ward || updated.ward;
|
|
|
|
setSelectedSlum(updated);
|
|
setSelectedRoadIndices([idx]);
|
|
|
|
// upsert single
|
|
await upsertSlumTable(updated);
|
|
}
|
|
|
|
//
|
|
// multi-select handler: if exactly one selected, we call single upsert to preserve previous behaviour
|
|
//
|
|
function handleRoadsMultiSelect(e: React.ChangeEvent<HTMLSelectElement>) {
|
|
const options = e.target.options;
|
|
const selected: number[] = [];
|
|
for (let i = 0; i < options.length; i++) {
|
|
if (options[i].selected) {
|
|
const val = Number(options[i].value);
|
|
if (!isNaN(val) && val >= 0) selected.push(val);
|
|
}
|
|
}
|
|
setSelectedRoadIndices(selected);
|
|
|
|
// if single selection made, keep previous behavior: immediately upsert that one
|
|
if (selected.length === 1) {
|
|
handleRoadPickSingle(selected[0]);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Bulk save: create/update one row per selected road (uses safeQueryFeatures and robust parse)
|
|
//
|
|
async function saveSelectedRoadsBulk() {
|
|
if (!selectedSlum) {
|
|
alert("Load and/or enter slum attributes first by clicking a slum polygon.");
|
|
return;
|
|
}
|
|
if (!slumTableRef.current) {
|
|
alert("Slum table layer not ready.");
|
|
return;
|
|
}
|
|
if (!selectedRoadIndices || selectedRoadIndices.length === 0) {
|
|
alert("No roads selected.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setStatus({ type: "loading", message: "Preparing bulk save..." });
|
|
|
|
const addFeatures: any[] = [];
|
|
const updateFeatures: any[] = [];
|
|
const esc = (s: string) => (s || "").replace(/'/g, "''");
|
|
|
|
for (const idx of selectedRoadIndices) {
|
|
const road = roadsInSlum[idx];
|
|
if (!road) continue;
|
|
|
|
const candidate: any = {
|
|
slum_code: selectedSlum.slumcode || "",
|
|
slumcategory: selectedSlum.slum_cat || "",
|
|
slumname: selectedSlum.slum_name || "",
|
|
area_name: selectedSlum.area_name || selectedSlum.locality || "",
|
|
zone: selectedSlum.zone || "",
|
|
ward: selectedSlum.ward || "",
|
|
road_id: road.road_id || road.attrs?.Road_ID || road.attrs?.road_id || "",
|
|
road_name: road.road_name || road.attrs?.Road_Name || road.attrs?.road_name || "",
|
|
};
|
|
|
|
if (road.attrs) {
|
|
candidate.zone = road.attrs.zone || road.attrs.Zone || candidate.zone;
|
|
candidate.ward = road.attrs.ward || road.attrs.Ward || candidate.ward;
|
|
}
|
|
|
|
let foundFeature: any = null;
|
|
|
|
// primary match: slum_code + road_id
|
|
if (candidate.slum_code && candidate.road_id) {
|
|
const where = `(slum_code = '${esc(candidate.slum_code)}' OR slumcode = '${esc(candidate.slum_code)}') AND (road_id = '${esc(
|
|
candidate.road_id
|
|
)}')`;
|
|
const r = await safeQueryFeatures(slumTableRef.current, where, ["*"]);
|
|
if (r && r.features && r.features.length > 0) foundFeature = r.features[0];
|
|
}
|
|
|
|
// fallback: slumname + road_name (case-insensitive)
|
|
if (!foundFeature && candidate.slumname && candidate.road_name) {
|
|
const where =
|
|
`(UPPER(slumname) = UPPER('${esc(candidate.slumname)}') OR UPPER(slum_name) = UPPER('${esc(candidate.slumname)}'))` +
|
|
` AND (UPPER(road_name) = UPPER('${esc(candidate.road_name)}') OR UPPER(roadname) = UPPER('${esc(candidate.road_name)}'))`;
|
|
const r2 = await safeQueryFeatures(slumTableRef.current, where, ["*"]);
|
|
if (r2 && r2.features && r2.features.length > 0) foundFeature = r2.features[0];
|
|
}
|
|
|
|
// attributes to apply
|
|
const attributesToApply: any = {};
|
|
if (candidate.slum_code) attributesToApply["slum_code"] = candidate.slum_code;
|
|
if (candidate.slumcategory) attributesToApply["slumcategory"] = candidate.slumcategory;
|
|
if (candidate.slumname) attributesToApply["slumname"] = candidate.slumname;
|
|
if (candidate.area_name) attributesToApply["area_name"] = candidate.area_name;
|
|
if (candidate.zone) attributesToApply["zone"] = candidate.zone;
|
|
if (candidate.ward) attributesToApply["ward"] = candidate.ward;
|
|
if (candidate.road_id) attributesToApply["road_id"] = candidate.road_id;
|
|
if (candidate.road_name) attributesToApply["road_name"] = candidate.road_name;
|
|
|
|
if (foundFeature) {
|
|
const oidName =
|
|
(foundFeature.attributes &&
|
|
Object.keys(foundFeature.attributes).find((k) => /^(objectid|oid|object_id|gid)$/i.test(k))) ||
|
|
"OBJECTID";
|
|
attributesToApply[oidName] = foundFeature.attributes[oidName];
|
|
updateFeatures.push(new Graphic({ attributes: attributesToApply }));
|
|
} else {
|
|
addFeatures.push(new Graphic({ attributes: attributesToApply }));
|
|
}
|
|
} // end for
|
|
|
|
// apply edits
|
|
setStatus({ type: "loading", message: `Saving ${addFeatures.length} adds & ${updateFeatures.length} updates...` });
|
|
const applyResult = await slumTableRef.current.applyEdits({ addFeatures, updateFeatures });
|
|
console.debug("applyEdits raw:", applyResult);
|
|
|
|
const parsed = parseApplyEditsResult(applyResult);
|
|
if ((addFeatures.length === 0 || parsed.added === addFeatures.length) && (updateFeatures.length === 0 || parsed.updated === updateFeatures.length)) {
|
|
setStatus({ type: "success", message: `Bulk save OK: ${parsed.added} added, ${parsed.updated} updated` });
|
|
} else {
|
|
console.warn("applyEdits returned (partial/failed):", applyResult, parsed.errors);
|
|
setStatus({ type: "error", message: "Bulk save partially failed (see console)" });
|
|
}
|
|
} catch (err) {
|
|
console.error("bulk save error:", err);
|
|
setStatus({ type: "error", message: "Bulk save error" });
|
|
} finally {
|
|
setTimeout(() => setStatus({ type: "idle" }), 1000);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Upsert single slum-row (tries match by slum_code then slumname)
|
|
//
|
|
async function upsertSlumTable(slum: SlumAttrs) {
|
|
if (!slumTableRef.current) {
|
|
console.warn("slum table not ready");
|
|
return;
|
|
}
|
|
try {
|
|
setStatus({ type: "loading", message: "Saving slum table..." });
|
|
const esc = (s: string) => (s || "").replace(/'/g, "''");
|
|
|
|
const code = slum.slumcode || slum.slum_code || "";
|
|
const name = slum.slum_name || slum.slumname || "";
|
|
|
|
let foundFeature: any = null;
|
|
|
|
// Try match by slum_code
|
|
if (code) {
|
|
const where = `slum_code = '${esc(String(code))}' OR slumcode = '${esc(String(code))}'`;
|
|
const r = await safeQueryFeatures(slumTableRef.current, where, ["*"]);
|
|
if (r && r.features && r.features.length > 0) foundFeature = r.features[0];
|
|
}
|
|
|
|
// fallback to name match (case-insensitive)
|
|
if (!foundFeature && name) {
|
|
const where = `UPPER(slumname) = UPPER('${esc(String(name))}') OR UPPER(slum_name) = UPPER('${esc(String(name))}')`;
|
|
const r2 = await safeQueryFeatures(slumTableRef.current, where, ["*"]);
|
|
if (r2 && r2.features && r2.features.length > 0) foundFeature = r2.features[0];
|
|
}
|
|
|
|
// Build attributes map to update/add
|
|
const updateAttrs: any = {};
|
|
if (slum.slumcode) updateAttrs["slum_code"] = slum.slumcode;
|
|
if (slum.slum_cat) updateAttrs["slumcategory"] = slum.slum_cat;
|
|
if (slum.slum_name) updateAttrs["slumname"] = slum.slum_name;
|
|
if (slum.zone) updateAttrs["zone"] = slum.zone;
|
|
if (slum.ward) updateAttrs["ward"] = slum.ward;
|
|
if (slum.road_id) updateAttrs["road_id"] = slum.road_id;
|
|
if (slum.road_name) updateAttrs["road_name"] = slum.road_name;
|
|
if (slum.area_name) updateAttrs["area_name"] = slum.area_name;
|
|
if (slum.locality) updateAttrs["locality"] = slum.locality;
|
|
|
|
let applyResult: any = null;
|
|
if (foundFeature) {
|
|
const oidName =
|
|
(foundFeature.attributes && Object.keys(foundFeature.attributes).find((k) => /^(objectid|oid|object_id|gid)$/i.test(k))) ||
|
|
"OBJECTID";
|
|
updateAttrs[oidName] = foundFeature.attributes[oidName];
|
|
applyResult = await slumTableRef.current.applyEdits({ updateFeatures: [new Graphic({ attributes: updateAttrs })] });
|
|
} else {
|
|
applyResult = await slumTableRef.current.applyEdits({ addFeatures: [new Graphic({ attributes: updateAttrs })] });
|
|
}
|
|
|
|
console.debug("applyEdits raw (upsert):", applyResult);
|
|
const parsed = parseApplyEditsResult(applyResult);
|
|
if (parsed.added > 0 || parsed.updated > 0) {
|
|
setStatus({ type: "success", message: "Slum table updated" });
|
|
} else {
|
|
console.warn("applyEdits returned (upsert):", applyResult, parsed.errors);
|
|
setStatus({ type: "error", message: "Save failed (see console)" });
|
|
}
|
|
} catch (err) {
|
|
console.error("upsertSlumTable error:", err);
|
|
setStatus({ type: "error", message: "Save error" });
|
|
} finally {
|
|
setTimeout(() => setStatus({ type: "idle" }), 900);
|
|
}
|
|
}
|
|
|
|
// Manual Save (in case user edits fields and wants to explicitly save without picking road)
|
|
async function handleManualSave() {
|
|
if (!selectedSlum) {
|
|
alert("No slum data to save.");
|
|
return;
|
|
}
|
|
await upsertSlumTable(selectedSlum);
|
|
}
|
|
|
|
// UI
|
|
return (
|
|
<div style={{ padding: 12, maxWidth: 860, fontFamily: "Segoe UI, Roboto, Arial" }}>
|
|
<h3 style={{ margin: "0 0 8px 0" }}>Slum → Table (click polygon to load)</h3>
|
|
|
|
<div style={{ background: "#fff", padding: 12, borderRadius: 8, border: "1px solid #eee" }}>
|
|
<div style={{ marginBottom: 8 }}>
|
|
<strong>Status:</strong>{" "}
|
|
{status.type === "idle" ? "idle" : status.type === "loading" ? `working — ${status.message || ""}` : status.message}
|
|
</div>
|
|
|
|
<div style={{ marginBottom: 8 }}>
|
|
<button
|
|
onClick={() => {
|
|
const promptOid = prompt("Enter slum OBJECTID to load (or cancel):");
|
|
if (promptOid) {
|
|
(async () => {
|
|
if (!slumBoundaryRef.current) return;
|
|
try {
|
|
setStatus({ type: "loading", message: "Fetching slum..." });
|
|
const q = slumBoundaryRef.current.createQuery();
|
|
q.where = `OBJECTID = ${Number(promptOid)}`;
|
|
q.outFields = ["*"];
|
|
q.returnGeometry = true;
|
|
q.num = 1;
|
|
const r = await slumBoundaryRef.current.queryFeatures(q);
|
|
if (r && r.features && r.features.length > 0) await handleSlumSelectedByGraphic(r.features[0]);
|
|
else setStatus({ type: "error", message: "Slum not found" });
|
|
} catch (err) {
|
|
console.error(err);
|
|
setStatus({ type: "error", message: "Fetch failed" });
|
|
} finally {
|
|
setTimeout(() => setStatus({ type: "idle" }), 700);
|
|
}
|
|
})();
|
|
}
|
|
}}
|
|
style={{ marginRight: 8 }}
|
|
>
|
|
Load Slum by OBJECTID
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => {
|
|
setSelectedSlum(null);
|
|
setRoadsInSlum([]);
|
|
setSelectedRoadIndices([]);
|
|
setStatus({ type: "idle" });
|
|
}}
|
|
style={{ marginRight: 8 }}
|
|
>
|
|
Clear
|
|
</button>
|
|
|
|
<button onClick={handleManualSave} style={{ marginRight: 8 }}>
|
|
Save (manual single)
|
|
</button>
|
|
|
|
<button onClick={saveSelectedRoadsBulk} style={{ marginRight: 8 }}>
|
|
Save Selected Roads (bulk)
|
|
</button>
|
|
</div>
|
|
|
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
|
|
<div>
|
|
<label style={{ fontSize: 12, color: "#444" }}>Slum Code</label>
|
|
<input
|
|
value={selectedSlum?.slumcode || ""}
|
|
onChange={(e) => setSelectedSlum((s) => ({ ...(s || {}), slumcode: e.target.value }))}
|
|
style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ fontSize: 12, color: "#444" }}>Slum Name</label>
|
|
<input
|
|
value={selectedSlum?.slum_name || ""}
|
|
onChange={(e) => setSelectedSlum((s) => ({ ...(s || {}), slum_name: e.target.value }))}
|
|
style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ fontSize: 12, color: "#444" }}>Slum Category</label>
|
|
<input
|
|
value={selectedSlum?.slum_cat || ""}
|
|
onChange={(e) => setSelectedSlum((s) => ({ ...(s || {}), slum_cat: e.target.value }))}
|
|
style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ fontSize: 12, color: "#444" }}>Area / Locality</label>
|
|
<input
|
|
value={selectedSlum?.area_name || selectedSlum?.locality || ""}
|
|
onChange={(e) => setSelectedSlum((s) => ({ ...(s || {}), area_name: e.target.value }))}
|
|
style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ fontSize: 12, color: "#444" }}>Zone</label>
|
|
<input
|
|
value={selectedSlum?.zone || ""}
|
|
onChange={(e) => setSelectedSlum((s) => ({ ...(s || {}), zone: e.target.value }))}
|
|
style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ fontSize: 12, color: "#444" }}>Ward</label>
|
|
<input
|
|
value={selectedSlum?.ward || ""}
|
|
onChange={(e) => setSelectedSlum((s) => ({ ...(s || {}), ward: e.target.value }))}
|
|
style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ fontSize: 12, color: "#444" }}>Road ID</label>
|
|
<input
|
|
value={
|
|
selectedRoadIndices.length === 1
|
|
? roadsInSlum[selectedRoadIndices[0]]?.road_id || selectedSlum?.road_id || ""
|
|
: selectedRoadIndices.length > 1
|
|
? `${selectedRoadIndices.length} selected`
|
|
: selectedSlum?.road_id || ""
|
|
}
|
|
readOnly
|
|
style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ fontSize: 12, color: "#444" }}>Road Name</label>
|
|
<input
|
|
value={
|
|
selectedRoadIndices.length === 1
|
|
? roadsInSlum[selectedRoadIndices[0]]?.road_name || selectedSlum?.road_name || ""
|
|
: selectedRoadIndices.length > 1
|
|
? `${selectedRoadIndices.length} selected`
|
|
: selectedSlum?.road_name || ""
|
|
}
|
|
readOnly
|
|
style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ marginTop: 12 }}>
|
|
<label style={{ display: "block", fontSize: 12, color: "#444", marginBottom: 6 }}>
|
|
Roads intersecting this slum (Ctrl/Cmd+click to multi-select, single select will auto-save)
|
|
</label>
|
|
<select
|
|
multiple
|
|
style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd", minHeight: 120 }}
|
|
onChange={handleRoadsMultiSelect}
|
|
value={selectedRoadIndices.map(String)}
|
|
>
|
|
{roadsInSlum.length === 0 ? (
|
|
<option value={-1} disabled>
|
|
-- no roads found (click a slum polygon) --
|
|
</option>
|
|
) : null}
|
|
|
|
{roadsInSlum.map((r, i) => (
|
|
<option key={`${r.road_id}__${i}`} value={i}>
|
|
{r.road_name || "(unnamed)"} {r.road_id ? ` — ${r.road_id}` : ""}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div style={{ marginTop: 10, fontSize: 12, color: "#666" }}>
|
|
Click a slum polygon to load its attributes into the fields (you can edit them). You can multi-select roads (Ctrl/Cmd+click).
|
|
Selecting a single road will immediately merge road fields and save that row. Use <strong>Save Selected Roads (bulk)</strong> to
|
|
create/update one row per selected road in the Slum Table.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|