Web Developer Work - I
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

​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
  1. // src/runtime/widget.tsx
  2. import React, { useEffect, useRef, useState } from "react";
  3. import { AllWidgetProps } from "jimu-core";
  4. import FeatureLayer from "@arcgis/core/layers/FeatureLayer";
  5. import Graphic from "@arcgis/core/Graphic";
  6. import MapView from "@arcgis/core/views/MapView";
  7. type SlumAttrs = {
  8. objectid?: number | string;
  9. slumcode?: string;
  10. slum_cat?: string;
  11. slum_name?: string;
  12. area_name?: string;
  13. locality?: string;
  14. zone?: string;
  15. ward?: string;
  16. road_id?: string;
  17. road_name?: string;
  18. [k: string]: any;
  19. };
  20. export default function SlumLinkWidget(props: AllWidgetProps) {
  21. const SLUM_BOUNDARY_URL =
  22. "https://gisgcc.chennaicorporation.gov.in/server/rest/services/gccdeptfield/SlumBoundary/FeatureServer/0";
  23. const SLUM_TABLE_URL =
  24. "https://gisgcc.chennaicorporation.gov.in/server/rest/services/gccdeptfield/SlumBoundary/FeatureServer/1";
  25. const ROAD_LAYER_URL =
  26. "https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/Transporations/FeatureServer/0";
  27. const slumBoundaryRef = useRef<FeatureLayer | null>(null);
  28. const slumTableRef = useRef<FeatureLayer | null>(null);
  29. const roadLayerRef = useRef<FeatureLayer | null>(null);
  30. const viewRef = useRef<MapView | null>(null);
  31. const clickHandlerRef = useRef<any>(null);
  32. const [selectedSlum, setSelectedSlum] = useState<SlumAttrs | null>(null);
  33. const [roadsInSlum, setRoadsInSlum] = useState<Array<{ road_id?: string; road_name?: string; attrs?: any }>>(
  34. []
  35. );
  36. const [selectedRoadIndices, setSelectedRoadIndices] = useState<number[]>([]);
  37. const [status, setStatus] = useState<{ type: "idle" | "loading" | "success" | "error"; message?: string }>({
  38. type: "idle",
  39. });
  40. // init layers
  41. useEffect(() => {
  42. slumBoundaryRef.current = new FeatureLayer({ url: SLUM_BOUNDARY_URL });
  43. slumTableRef.current = new FeatureLayer({ url: SLUM_TABLE_URL });
  44. roadLayerRef.current = new FeatureLayer({ url: ROAD_LAYER_URL });
  45. }, []);
  46. //
  47. // Helper: robustly parse applyEdits result across server versions
  48. //
  49. function parseApplyEditsResult(res: any) {
  50. if (!res) return { added: 0, updated: 0, deleted: 0, errors: [] as any[] };
  51. const addResults = res.addFeatureResults || res.addResults || res.addFeatureResults || [];
  52. const updateResults = res.updateFeatureResults || res.updateResults || [];
  53. const deleteResults = res.deleteFeatureResults || res.deleteResults || [];
  54. const okAdded = addResults.filter((r: any) => r && (r.success === true || Number.isFinite(r.objectId))).length;
  55. const okUpdated = updateResults.filter((r: any) => r && (r.success === true || Number.isFinite(r.objectId))).length;
  56. const okDeleted = deleteResults.filter((r: any) => r && (r.success === true || Number.isFinite(r.objectId))).length;
  57. const errors: any[] = [];
  58. for (const arr of [addResults, updateResults, deleteResults]) {
  59. for (const r of arr) {
  60. if (r && r.error) errors.push(r.error);
  61. }
  62. }
  63. return { added: okAdded, updated: okUpdated, deleted: okDeleted, errors };
  64. }
  65. //
  66. // Helper: safe queryFeatures wrapper (returns null on error and logs the where clause)
  67. //
  68. async function safeQueryFeatures(layer: FeatureLayer | null, where: string | null, outFields: string[] = ["*"]) {
  69. if (!layer) return null;
  70. if (!where || where.trim().length === 0) {
  71. console.warn("safeQueryFeatures: skipping empty where");
  72. return null;
  73. }
  74. const q = layer.createQuery();
  75. q.where = where;
  76. q.outFields = outFields;
  77. q.returnGeometry = false;
  78. q.num = 2;
  79. console.debug("[safeQueryFeatures] where:", where);
  80. try {
  81. const r = await layer.queryFeatures(q);
  82. return r;
  83. } catch (err: any) {
  84. console.error("[safeQueryFeatures] queryFeatures error for where:", where, err);
  85. try {
  86. console.debug("err.details:", err?.details);
  87. console.debug("err.requestOptions:", err?.requestOptions);
  88. console.debug("err.raw:", err?.raw);
  89. } catch (e) {}
  90. return null;
  91. }
  92. }
  93. //
  94. // Helper: query SlumBoundary by geometry to get authoritative attributes
  95. //
  96. async function fetchSlumFeatureByGeometry(geometry: any): Promise<any | null> {
  97. if (!slumBoundaryRef.current || !geometry) return null;
  98. try {
  99. const q = slumBoundaryRef.current.createQuery();
  100. q.geometry = geometry;
  101. q.spatialRelationship = "intersects";
  102. q.returnGeometry = true;
  103. q.outFields = ["*"];
  104. q.num = 1;
  105. const res = await slumBoundaryRef.current.queryFeatures(q);
  106. if (res && res.features && res.features.length > 0) return res.features[0];
  107. return null;
  108. } catch (err) {
  109. console.error("fetchSlumFeatureByGeometry error:", err);
  110. return null;
  111. }
  112. }
  113. //
  114. // Helper: query Slum details/table layer by slum code (layer 1)
  115. //
  116. async function fetchSlumDetailsByCode(code: string): Promise<any | null> {
  117. if (!slumTableRef.current || !code) return null;
  118. try {
  119. const esc = (s: string) => (s || "").replace(/'/g, "''");
  120. const where = `(slum_code = '${esc(code)}' OR slumcode = '${esc(code)}')`;
  121. const r = await safeQueryFeatures(slumTableRef.current, where, ["*"]);
  122. if (r && r.features && r.features.length > 0) return r.features[0];
  123. return null;
  124. } catch (err) {
  125. console.error("fetchSlumDetailsByCode error:", err);
  126. return null;
  127. }
  128. }
  129. // Attach click -> hitTest to read slum polygon attributes and query roads
  130. useEffect(() => {
  131. const maybeView = (props as any).jimuMapView?.view || (window as any).mapView || null;
  132. if (!maybeView) return;
  133. viewRef.current = maybeView as MapView;
  134. const handler = viewRef.current.on("click", async (evt: any) => {
  135. try {
  136. setStatus({ type: "loading", message: "Identifying features..." });
  137. const hit = await viewRef.current!.hitTest(evt);
  138. if (!hit || !hit.results || hit.results.length === 0) {
  139. setStatus({ type: "idle" });
  140. return;
  141. }
  142. // find slum polygon result (robust detection by layer url/title containing "slum")
  143. const slumResult = hit.results.find((r: any) => {
  144. const l = r.graphic && r.graphic.layer;
  145. if (!l) return false;
  146. const u = (l.url || "").toLowerCase();
  147. const t = (l.title || "").toLowerCase();
  148. return u.includes("slumboundary") || t.includes("slumboundary") || t.includes("slum");
  149. });
  150. if (!slumResult) {
  151. setStatus({ type: "idle" });
  152. return;
  153. }
  154. const g: any = slumResult.graphic;
  155. await handleSlumSelectedByGraphic(g);
  156. } catch (err) {
  157. console.error("Hit test error:", err);
  158. setStatus({ type: "error", message: "Identify failed" });
  159. setTimeout(() => setStatus({ type: "idle" }), 800);
  160. }
  161. });
  162. clickHandlerRef.current = handler;
  163. return () => {
  164. if (clickHandlerRef.current && viewRef.current) clickHandlerRef.current.remove();
  165. };
  166. // eslint-disable-next-line react-hooks/exhaustive-deps
  167. }, [props]);
  168. // When a slum polygon graphic is selected: populate the UI fields but DO NOT save yet.
  169. async function handleSlumSelectedByGraphic(graphic: any) {
  170. try {
  171. setStatus({ type: "loading", message: "Loading slum attributes..." });
  172. // 1) start with any attributes present on the hitTest graphic
  173. let attrs = graphic.attributes || {};
  174. // 2) get authoritative polygon attrs if missing slum name or slum code
  175. const hasName = !!(attrs.slum_name || attrs.slumname || attrs.SLUMNAME);
  176. const hasCode = !!(attrs.slumcode || attrs.slum_code || attrs.SLUM_CODE);
  177. if ((!hasName || !hasCode) && graphic.geometry) {
  178. const featureFromLayer = await fetchSlumFeatureByGeometry(graphic.geometry);
  179. if (featureFromLayer && featureFromLayer.attributes) {
  180. // merge but prefer existing attributes from graphic if present
  181. attrs = { ...(featureFromLayer.attributes || {}), ...(attrs || {}) };
  182. }
  183. }
  184. // Extract canonical slum code & name
  185. const slumCode = attrs.slumcode || attrs.slum_code || attrs.SLUM_CODE || "";
  186. const slumName = attrs.slum_name || attrs.slumname || attrs.SLUMNAME || "";
  187. const slum: SlumAttrs = {
  188. objectid: attrs.OBJECTID || attrs.objectid || attrs.objectId || attrs.ROWID,
  189. slumcode: slumCode,
  190. slum_cat: attrs.slum_cat || attrs.slumcategory || attrs.SLUMCATEGORY || "",
  191. slum_name: slumName,
  192. area_name: attrs.area_name || attrs.area || "",
  193. locality: attrs.locality || "",
  194. zone: attrs.zone || "",
  195. ward: attrs.ward || "",
  196. road_id: attrs.road_id || "",
  197. road_name: attrs.road_name || "",
  198. ...attrs,
  199. };
  200. // 3) If we have a slum code, try to enrich from the Slum details (table) layer (layer 1)
  201. if (slumCode && slumTableRef.current) {
  202. const detailsFeature = await fetchSlumDetailsByCode(slumCode);
  203. if (detailsFeature && detailsFeature.attributes) {
  204. const d = detailsFeature.attributes;
  205. // merge relevant fields (prefer the existing slum object values)
  206. slum.slum_name = slum.slum_name || d.slum_name || d.slumname || "";
  207. slum.area_name = slum.area_name || d.area_name || d.area || "";
  208. slum.zone = slum.zone || d.zone || "";
  209. slum.ward = slum.ward || d.ward || "";
  210. // merge any other useful attributes you want:
  211. // slum.household = slum.household || d.household;
  212. // slum.no_ofstreets = slum.no_ofstreets || d.no_ofstreets;
  213. slum["__slum_table_oid"] = d.OBJECTID || d.objectid || d.objectId;
  214. }
  215. }
  216. // Set UI state so user can edit before saving
  217. setSelectedSlum(slum);
  218. setSelectedRoadIndices([]);
  219. setRoadsInSlum([]);
  220. // Query roads intersecting polygon (use geometry from the graphic)
  221. if (graphic.geometry && roadLayerRef.current) {
  222. setStatus({ type: "loading", message: "Querying roads..." });
  223. const q = roadLayerRef.current.createQuery();
  224. q.geometry = graphic.geometry;
  225. q.spatialRelationship = "intersects";
  226. q.returnGeometry = false;
  227. q.outFields = ["Road_ID", "Road_Name", "zone", "ward", "*"];
  228. q.num = 1000;
  229. const res = await roadLayerRef.current.queryFeatures(q);
  230. const matches: Array<{ road_id?: string; road_name?: string; attrs?: any }> = [];
  231. if (res && res.features && res.features.length > 0) {
  232. const seen = new Set<string>();
  233. for (const f of res.features) {
  234. const rattrs = f.attributes || {};
  235. const rid = rattrs.Road_ID || rattrs.road_id || rattrs.RoadId || rattrs.ROAD_ID || "";
  236. const rname = rattrs.Road_Name || rattrs.road_name || rattrs.RoadName || rattrs.Road || "";
  237. const key = `${String(rid).trim()}__${String(rname).trim()}`;
  238. if (!seen.has(key)) {
  239. seen.add(key);
  240. matches.push({ road_id: String(rid || "").trim(), road_name: String(rname || "").trim(), attrs: rattrs });
  241. }
  242. }
  243. }
  244. matches.sort((a, b) => (a.road_name || "").localeCompare(b.road_name || ""));
  245. setRoadsInSlum(matches);
  246. }
  247. setStatus({ type: "success", message: "Slum loaded — review and save if needed" });
  248. } catch (err) {
  249. console.error("handleSlumSelectedByGraphic:", err);
  250. setStatus({ type: "error", message: "Failed to load slum" });
  251. } finally {
  252. setTimeout(() => setStatus({ type: "idle" }), 900);
  253. }
  254. }
  255. //
  256. // When user selects a single road (we keep behavior: selecting one will immediately upsert that single record)
  257. //
  258. async function handleRoadPickSingle(idx: number) {
  259. if (idx === -1) {
  260. setSelectedRoadIndices([]);
  261. return;
  262. }
  263. const road = roadsInSlum[idx];
  264. if (!road || !selectedSlum) return;
  265. // Merge authoritative road attrs into slum
  266. const updated: SlumAttrs = { ...(selectedSlum || {}) };
  267. updated.road_id = road.road_id || road.attrs?.Road_ID || road.attrs?.road_id || updated.road_id;
  268. updated.road_name = road.road_name || road.attrs?.Road_Name || road.attrs?.road_name || updated.road_name;
  269. const rattrs = road.attrs || {};
  270. updated.zone = rattrs.zone || rattrs.Zone || updated.zone;
  271. updated.ward = rattrs.ward || rattrs.Ward || updated.ward;
  272. setSelectedSlum(updated);
  273. setSelectedRoadIndices([idx]);
  274. // upsert single
  275. await upsertSlumTable(updated);
  276. }
  277. //
  278. // multi-select handler: if exactly one selected, we call single upsert to preserve previous behaviour
  279. //
  280. function handleRoadsMultiSelect(e: React.ChangeEvent<HTMLSelectElement>) {
  281. const options = e.target.options;
  282. const selected: number[] = [];
  283. for (let i = 0; i < options.length; i++) {
  284. if (options[i].selected) {
  285. const val = Number(options[i].value);
  286. if (!isNaN(val) && val >= 0) selected.push(val);
  287. }
  288. }
  289. setSelectedRoadIndices(selected);
  290. // if single selection made, keep previous behavior: immediately upsert that one
  291. if (selected.length === 1) {
  292. handleRoadPickSingle(selected[0]);
  293. }
  294. }
  295. //
  296. // Bulk save: create/update one row per selected road (uses safeQueryFeatures and robust parse)
  297. //
  298. async function saveSelectedRoadsBulk() {
  299. if (!selectedSlum) {
  300. alert("Load and/or enter slum attributes first by clicking a slum polygon.");
  301. return;
  302. }
  303. if (!slumTableRef.current) {
  304. alert("Slum table layer not ready.");
  305. return;
  306. }
  307. if (!selectedRoadIndices || selectedRoadIndices.length === 0) {
  308. alert("No roads selected.");
  309. return;
  310. }
  311. try {
  312. setStatus({ type: "loading", message: "Preparing bulk save..." });
  313. const addFeatures: any[] = [];
  314. const updateFeatures: any[] = [];
  315. const esc = (s: string) => (s || "").replace(/'/g, "''");
  316. for (const idx of selectedRoadIndices) {
  317. const road = roadsInSlum[idx];
  318. if (!road) continue;
  319. const candidate: any = {
  320. slum_code: selectedSlum.slumcode || "",
  321. slumcategory: selectedSlum.slum_cat || "",
  322. slumname: selectedSlum.slum_name || "",
  323. area_name: selectedSlum.area_name || selectedSlum.locality || "",
  324. zone: selectedSlum.zone || "",
  325. ward: selectedSlum.ward || "",
  326. road_id: road.road_id || road.attrs?.Road_ID || road.attrs?.road_id || "",
  327. road_name: road.road_name || road.attrs?.Road_Name || road.attrs?.road_name || "",
  328. };
  329. if (road.attrs) {
  330. candidate.zone = road.attrs.zone || road.attrs.Zone || candidate.zone;
  331. candidate.ward = road.attrs.ward || road.attrs.Ward || candidate.ward;
  332. }
  333. let foundFeature: any = null;
  334. // primary match: slum_code + road_id
  335. if (candidate.slum_code && candidate.road_id) {
  336. const where = `(slum_code = '${esc(candidate.slum_code)}' OR slumcode = '${esc(candidate.slum_code)}') AND (road_id = '${esc(
  337. candidate.road_id
  338. )}')`;
  339. const r = await safeQueryFeatures(slumTableRef.current, where, ["*"]);
  340. if (r && r.features && r.features.length > 0) foundFeature = r.features[0];
  341. }
  342. // fallback: slumname + road_name (case-insensitive)
  343. if (!foundFeature && candidate.slumname && candidate.road_name) {
  344. const where =
  345. `(UPPER(slumname) = UPPER('${esc(candidate.slumname)}') OR UPPER(slum_name) = UPPER('${esc(candidate.slumname)}'))` +
  346. ` AND (UPPER(road_name) = UPPER('${esc(candidate.road_name)}') OR UPPER(roadname) = UPPER('${esc(candidate.road_name)}'))`;
  347. const r2 = await safeQueryFeatures(slumTableRef.current, where, ["*"]);
  348. if (r2 && r2.features && r2.features.length > 0) foundFeature = r2.features[0];
  349. }
  350. // attributes to apply
  351. const attributesToApply: any = {};
  352. if (candidate.slum_code) attributesToApply["slum_code"] = candidate.slum_code;
  353. if (candidate.slumcategory) attributesToApply["slumcategory"] = candidate.slumcategory;
  354. if (candidate.slumname) attributesToApply["slumname"] = candidate.slumname;
  355. if (candidate.area_name) attributesToApply["area_name"] = candidate.area_name;
  356. if (candidate.zone) attributesToApply["zone"] = candidate.zone;
  357. if (candidate.ward) attributesToApply["ward"] = candidate.ward;
  358. if (candidate.road_id) attributesToApply["road_id"] = candidate.road_id;
  359. if (candidate.road_name) attributesToApply["road_name"] = candidate.road_name;
  360. if (foundFeature) {
  361. const oidName =
  362. (foundFeature.attributes &&
  363. Object.keys(foundFeature.attributes).find((k) => /^(objectid|oid|object_id|gid)$/i.test(k))) ||
  364. "OBJECTID";
  365. attributesToApply[oidName] = foundFeature.attributes[oidName];
  366. updateFeatures.push(new Graphic({ attributes: attributesToApply }));
  367. } else {
  368. addFeatures.push(new Graphic({ attributes: attributesToApply }));
  369. }
  370. } // end for
  371. // apply edits
  372. setStatus({ type: "loading", message: `Saving ${addFeatures.length} adds & ${updateFeatures.length} updates...` });
  373. const applyResult = await slumTableRef.current.applyEdits({ addFeatures, updateFeatures });
  374. console.debug("applyEdits raw:", applyResult);
  375. const parsed = parseApplyEditsResult(applyResult);
  376. if ((addFeatures.length === 0 || parsed.added === addFeatures.length) && (updateFeatures.length === 0 || parsed.updated === updateFeatures.length)) {
  377. setStatus({ type: "success", message: `Bulk save OK: ${parsed.added} added, ${parsed.updated} updated` });
  378. } else {
  379. console.warn("applyEdits returned (partial/failed):", applyResult, parsed.errors);
  380. setStatus({ type: "error", message: "Bulk save partially failed (see console)" });
  381. }
  382. } catch (err) {
  383. console.error("bulk save error:", err);
  384. setStatus({ type: "error", message: "Bulk save error" });
  385. } finally {
  386. setTimeout(() => setStatus({ type: "idle" }), 1000);
  387. }
  388. }
  389. //
  390. // Upsert single slum-row (tries match by slum_code then slumname)
  391. //
  392. async function upsertSlumTable(slum: SlumAttrs) {
  393. if (!slumTableRef.current) {
  394. console.warn("slum table not ready");
  395. return;
  396. }
  397. try {
  398. setStatus({ type: "loading", message: "Saving slum table..." });
  399. const esc = (s: string) => (s || "").replace(/'/g, "''");
  400. const code = slum.slumcode || slum.slum_code || "";
  401. const name = slum.slum_name || slum.slumname || "";
  402. let foundFeature: any = null;
  403. // Try match by slum_code
  404. if (code) {
  405. const where = `slum_code = '${esc(String(code))}' OR slumcode = '${esc(String(code))}'`;
  406. const r = await safeQueryFeatures(slumTableRef.current, where, ["*"]);
  407. if (r && r.features && r.features.length > 0) foundFeature = r.features[0];
  408. }
  409. // fallback to name match (case-insensitive)
  410. if (!foundFeature && name) {
  411. const where = `UPPER(slumname) = UPPER('${esc(String(name))}') OR UPPER(slum_name) = UPPER('${esc(String(name))}')`;
  412. const r2 = await safeQueryFeatures(slumTableRef.current, where, ["*"]);
  413. if (r2 && r2.features && r2.features.length > 0) foundFeature = r2.features[0];
  414. }
  415. // Build attributes map to update/add
  416. const updateAttrs: any = {};
  417. if (slum.slumcode) updateAttrs["slum_code"] = slum.slumcode;
  418. if (slum.slum_cat) updateAttrs["slumcategory"] = slum.slum_cat;
  419. if (slum.slum_name) updateAttrs["slumname"] = slum.slum_name;
  420. if (slum.zone) updateAttrs["zone"] = slum.zone;
  421. if (slum.ward) updateAttrs["ward"] = slum.ward;
  422. if (slum.road_id) updateAttrs["road_id"] = slum.road_id;
  423. if (slum.road_name) updateAttrs["road_name"] = slum.road_name;
  424. if (slum.area_name) updateAttrs["area_name"] = slum.area_name;
  425. if (slum.locality) updateAttrs["locality"] = slum.locality;
  426. let applyResult: any = null;
  427. if (foundFeature) {
  428. const oidName =
  429. (foundFeature.attributes && Object.keys(foundFeature.attributes).find((k) => /^(objectid|oid|object_id|gid)$/i.test(k))) ||
  430. "OBJECTID";
  431. updateAttrs[oidName] = foundFeature.attributes[oidName];
  432. applyResult = await slumTableRef.current.applyEdits({ updateFeatures: [new Graphic({ attributes: updateAttrs })] });
  433. } else {
  434. applyResult = await slumTableRef.current.applyEdits({ addFeatures: [new Graphic({ attributes: updateAttrs })] });
  435. }
  436. console.debug("applyEdits raw (upsert):", applyResult);
  437. const parsed = parseApplyEditsResult(applyResult);
  438. if (parsed.added > 0 || parsed.updated > 0) {
  439. setStatus({ type: "success", message: "Slum table updated" });
  440. } else {
  441. console.warn("applyEdits returned (upsert):", applyResult, parsed.errors);
  442. setStatus({ type: "error", message: "Save failed (see console)" });
  443. }
  444. } catch (err) {
  445. console.error("upsertSlumTable error:", err);
  446. setStatus({ type: "error", message: "Save error" });
  447. } finally {
  448. setTimeout(() => setStatus({ type: "idle" }), 900);
  449. }
  450. }
  451. // Manual Save (in case user edits fields and wants to explicitly save without picking road)
  452. async function handleManualSave() {
  453. if (!selectedSlum) {
  454. alert("No slum data to save.");
  455. return;
  456. }
  457. await upsertSlumTable(selectedSlum);
  458. }
  459. // UI
  460. return (
  461. <div style={{ padding: 12, maxWidth: 860, fontFamily: "Segoe UI, Roboto, Arial" }}>
  462. <h3 style={{ margin: "0 0 8px 0" }}>Slum → Table (click polygon to load)</h3>
  463. <div style={{ background: "#fff", padding: 12, borderRadius: 8, border: "1px solid #eee" }}>
  464. <div style={{ marginBottom: 8 }}>
  465. <strong>Status:</strong>{" "}
  466. {status.type === "idle" ? "idle" : status.type === "loading" ? `working — ${status.message || ""}` : status.message}
  467. </div>
  468. <div style={{ marginBottom: 8 }}>
  469. <button
  470. onClick={() => {
  471. const promptOid = prompt("Enter slum OBJECTID to load (or cancel):");
  472. if (promptOid) {
  473. (async () => {
  474. if (!slumBoundaryRef.current) return;
  475. try {
  476. setStatus({ type: "loading", message: "Fetching slum..." });
  477. const q = slumBoundaryRef.current.createQuery();
  478. q.where = `OBJECTID = ${Number(promptOid)}`;
  479. q.outFields = ["*"];
  480. q.returnGeometry = true;
  481. q.num = 1;
  482. const r = await slumBoundaryRef.current.queryFeatures(q);
  483. if (r && r.features && r.features.length > 0) await handleSlumSelectedByGraphic(r.features[0]);
  484. else setStatus({ type: "error", message: "Slum not found" });
  485. } catch (err) {
  486. console.error(err);
  487. setStatus({ type: "error", message: "Fetch failed" });
  488. } finally {
  489. setTimeout(() => setStatus({ type: "idle" }), 700);
  490. }
  491. })();
  492. }
  493. }}
  494. style={{ marginRight: 8 }}
  495. >
  496. Load Slum by OBJECTID
  497. </button>
  498. <button
  499. onClick={() => {
  500. setSelectedSlum(null);
  501. setRoadsInSlum([]);
  502. setSelectedRoadIndices([]);
  503. setStatus({ type: "idle" });
  504. }}
  505. style={{ marginRight: 8 }}
  506. >
  507. Clear
  508. </button>
  509. <button onClick={handleManualSave} style={{ marginRight: 8 }}>
  510. Save (manual single)
  511. </button>
  512. <button onClick={saveSelectedRoadsBulk} style={{ marginRight: 8 }}>
  513. Save Selected Roads (bulk)
  514. </button>
  515. </div>
  516. <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
  517. <div>
  518. <label style={{ fontSize: 12, color: "#444" }}>Slum Code</label>
  519. <input
  520. value={selectedSlum?.slumcode || ""}
  521. onChange={(e) => setSelectedSlum((s) => ({ ...(s || {}), slumcode: e.target.value }))}
  522. style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }}
  523. />
  524. </div>
  525. <div>
  526. <label style={{ fontSize: 12, color: "#444" }}>Slum Name</label>
  527. <input
  528. value={selectedSlum?.slum_name || ""}
  529. onChange={(e) => setSelectedSlum((s) => ({ ...(s || {}), slum_name: e.target.value }))}
  530. style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }}
  531. />
  532. </div>
  533. <div>
  534. <label style={{ fontSize: 12, color: "#444" }}>Slum Category</label>
  535. <input
  536. value={selectedSlum?.slum_cat || ""}
  537. onChange={(e) => setSelectedSlum((s) => ({ ...(s || {}), slum_cat: e.target.value }))}
  538. style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }}
  539. />
  540. </div>
  541. <div>
  542. <label style={{ fontSize: 12, color: "#444" }}>Area / Locality</label>
  543. <input
  544. value={selectedSlum?.area_name || selectedSlum?.locality || ""}
  545. onChange={(e) => setSelectedSlum((s) => ({ ...(s || {}), area_name: e.target.value }))}
  546. style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }}
  547. />
  548. </div>
  549. <div>
  550. <label style={{ fontSize: 12, color: "#444" }}>Zone</label>
  551. <input
  552. value={selectedSlum?.zone || ""}
  553. onChange={(e) => setSelectedSlum((s) => ({ ...(s || {}), zone: e.target.value }))}
  554. style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }}
  555. />
  556. </div>
  557. <div>
  558. <label style={{ fontSize: 12, color: "#444" }}>Ward</label>
  559. <input
  560. value={selectedSlum?.ward || ""}
  561. onChange={(e) => setSelectedSlum((s) => ({ ...(s || {}), ward: e.target.value }))}
  562. style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }}
  563. />
  564. </div>
  565. <div>
  566. <label style={{ fontSize: 12, color: "#444" }}>Road ID</label>
  567. <input
  568. value={
  569. selectedRoadIndices.length === 1
  570. ? roadsInSlum[selectedRoadIndices[0]]?.road_id || selectedSlum?.road_id || ""
  571. : selectedRoadIndices.length > 1
  572. ? `${selectedRoadIndices.length} selected`
  573. : selectedSlum?.road_id || ""
  574. }
  575. readOnly
  576. style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }}
  577. />
  578. </div>
  579. <div>
  580. <label style={{ fontSize: 12, color: "#444" }}>Road Name</label>
  581. <input
  582. value={
  583. selectedRoadIndices.length === 1
  584. ? roadsInSlum[selectedRoadIndices[0]]?.road_name || selectedSlum?.road_name || ""
  585. : selectedRoadIndices.length > 1
  586. ? `${selectedRoadIndices.length} selected`
  587. : selectedSlum?.road_name || ""
  588. }
  589. readOnly
  590. style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }}
  591. />
  592. </div>
  593. </div>
  594. <div style={{ marginTop: 12 }}>
  595. <label style={{ display: "block", fontSize: 12, color: "#444", marginBottom: 6 }}>
  596. Roads intersecting this slum (Ctrl/Cmd+click to multi-select, single select will auto-save)
  597. </label>
  598. <select
  599. multiple
  600. style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd", minHeight: 120 }}
  601. onChange={handleRoadsMultiSelect}
  602. value={selectedRoadIndices.map(String)}
  603. >
  604. {roadsInSlum.length === 0 ? (
  605. <option value={-1} disabled>
  606. -- no roads found (click a slum polygon) --
  607. </option>
  608. ) : null}
  609. {roadsInSlum.map((r, i) => (
  610. <option key={`${r.road_id}__${i}`} value={i}>
  611. {r.road_name || "(unnamed)"} {r.road_id ? ` — ${r.road_id}` : ""}
  612. </option>
  613. ))}
  614. </select>
  615. </div>
  616. <div style={{ marginTop: 10, fontSize: 12, color: "#666" }}>
  617. Click a slum polygon to load its attributes into the fields (you can edit them). You can multi-select roads (Ctrl/Cmd+click).
  618. Selecting a single road will immediately merge road fields and save that row. Use <strong>Save Selected Roads (bulk)</strong> to
  619. create/update one row per selected road in the Slum Table.
  620. </div>
  621. </div>
  622. </div>
  623. );
  624. }