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

// 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>
);
}