commit ced61698eb5892cfb6249fac137dfabd9e92b50f Author: Anitha_M Date: Sat Sep 19 16:56:36 2026 +0530 Add CreateAsset widget diff --git a/CreateAsset/config.json b/CreateAsset/config.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/CreateAsset/config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/CreateAsset/icon.svg b/CreateAsset/icon.svg new file mode 100644 index 0000000..8a756de --- /dev/null +++ b/CreateAsset/icon.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CreateAsset/manifest.json b/CreateAsset/manifest.json new file mode 100644 index 0000000..9cca794 --- /dev/null +++ b/CreateAsset/manifest.json @@ -0,0 +1,24 @@ +{ + "name": "CreateAsset", + "label": "CreateAsset", + "type": "widget", + "version": "1.17.0", + "exbVersion": "1.17.0", + "author": "DIVYA LAWRANCE HMS", + "description": "Form 7 Building Update and Approval Application.", + "copyright": "", + "license": "http://www.apache.org/licenses/LICENSE-2.0", + "dependency": [ + "jimu-arcgis" + ], + "properties": { + "useMapWidgetIds": [] + }, + "translatedLocales": [ + "en" + ], + "defaultSize": { + "width": 400, + "height": 600 + } +} \ No newline at end of file diff --git a/CreateAsset/src/config.ts b/CreateAsset/src/config.ts new file mode 100644 index 0000000..409728f --- /dev/null +++ b/CreateAsset/src/config.ts @@ -0,0 +1,9 @@ +import { type ImmutableObject } from 'seamless-immutable' + +export interface Config { + exampleConfigProperty: string + customizeLayers?: boolean + selectedLayerIds?: string[] +} + +export type IMConfig = ImmutableObject \ No newline at end of file diff --git a/CreateAsset/src/runtime/widget.tsx b/CreateAsset/src/runtime/widget.tsx new file mode 100644 index 0000000..da3ae7f --- /dev/null +++ b/CreateAsset/src/runtime/widget.tsx @@ -0,0 +1,1229 @@ +import React, { useEffect, useRef, useState } from "react"; +import { type AllWidgetProps, type ImmutableObject } from "jimu-core"; +import { JimuMapViewComponent, type JimuMapView } from "jimu-arcgis"; +import FeatureLayer from "@arcgis/core/layers/FeatureLayer"; +import Editor from "@arcgis/core/widgets/Editor"; +import FormTemplate from "@arcgis/core/form/FormTemplate"; +import Query from "@arcgis/core/rest/support/Query"; +import Graphic from "@arcgis/core/Graphic"; +import * as reactiveUtils from "@arcgis/core/core/reactiveUtils"; +import type { IMConfig } from "../config"; + +/* ========================================================= + SERVICE URLS +========================================================= */ + +const POINT_LAYER_URL = + "https://gisgcc.chennaicorporation.gov.in/server/rest/services/Revenue/Form7_Point/FeatureServer/0"; + +const BUILDING_LAYER_URL = + "https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/Buildings_Final/FeatureServer/0"; + +const ERP_API_URL = + "https://erp.chennaicorporation.gov.in/pgr/external/mobileservice"; + +/* ========================================================= + FORM 7 FIELD MAPPING +========================================================= */ + +const POINT_FIELDS = { + // Building derived + regionName: "region_name", + zone: "zone_number", + ward: "ward_number", + areaId: "area_id", + areaName: "area_name", + localityId: "locality_id", + locality: "locality", + roadId: "road_id", + roadName: "road_name", + + // Building GIS ID + finalGisId: "final_gis_id", + parentGisId: "parent_gis_id", + + // ERP house details + doorOldNo: "door_old_no", + doorNewNo: "door_new_no", + + // ERP MIS details + zoneMis: "zone_mis", + wardMis: "ward_mis", + misGisId: "mis_gis_id", + + // User entered + status: "status", + remarks: "remarks", + + // ERP property details + billNo: "bill_no", + assesseeName: "assessee_name", + address: "address", + mobile: "mobile", + category: "category", + currentUsage: "current_usage", + currentArea: "current_area" +}; + +/* ========================================================= + BUILDINGS_FINAL FIELDS +========================================================= */ + +const BUILDING_FIELDS = [ + "objectid", + "region_name", + "zone_number", + "ward_number", + "area_id", + "area_name", + "locality_id", + "locality", + "road_id", + "road_name", + "gcc_gis_id" +]; + +/* ========================================================= + BUILDING LAYER (module-level singleton, loaded once) +========================================================= */ + +const buildingLayer = new FeatureLayer({ + url: BUILDING_LAYER_URL, + outFields: BUILDING_FIELDS +}); + +/* ========================================================= + TYPES +========================================================= */ + +interface BuildingAttributes { + objectid?: number; + region_name?: string; + zone_number?: string; + ward_number?: string; + area_id?: number; + area_name?: string; + locality_id?: number; + locality?: string; + road_id?: string; + road_name?: string; + gcc_gis_id?: string; + [key: string]: any; +} + +interface ERPRecord { + BILL_NUMBER?: string; + GISID?: string; + ACTIVE_STATUS?: string; + ZONE?: string; + WARD?: string; + AREA?: string; + LOCALITY?: string; + STREET?: string; + HOUSENO?: string; + OLD_HOUSENO?: string; + OWNER_NAME?: string; + PROPERTY_TYPE?: string; + USG_NAME?: string; + TOTAL_BUILTUP_AREA?: string | number; + SITAL_AREA?: string | number; + MOBILE_NO?: string; + [key: string]: any; +} + +/* ========================================================= + HELPERS +========================================================= */ + +const safeValue = (value: any): string => + value === null || value === undefined ? "" : String(value); + +const formatZone = (zoneNumber: string | number): string => { + const value = String(zoneNumber ?? "").trim(); + if (!value) return ""; + return value.toUpperCase().startsWith("N") + ? value.toUpperCase() + : `N${value.padStart(2, "0")}`; +}; + +const formatWard = (wardNumber: string | number): string => { + const value = String(wardNumber ?? "").trim(); + if (!value) return ""; + return value.toUpperCase().startsWith("N") + ? value.toUpperCase() + : `N${value.padStart(3, "0")}`; +}; + +/* ========================================================= + GET BUILDING AT POINT +========================================================= */ + +const getBuildingFromPoint = async ( + geometry: __esri.Geometry +): Promise => { + if (!geometry) { + console.warn("No geometry supplied for building lookup."); + return null; + } + + try { + await buildingLayer.load(); + + const query = new Query({ + geometry, + spatialRelationship: "intersects", + returnGeometry: false, + outFields: BUILDING_FIELDS + }); + + const result = await buildingLayer.queryFeatures(query); + + if (!result.features || result.features.length === 0) { + console.warn("No building found at created point."); + return null; + } + + return result.features[0].attributes as BuildingAttributes; + } catch (error) { + console.error("Buildings_Final query failed:", error); + return null; + } +}; + +/* ========================================================= + WIDGET +========================================================= */ + +export default function Widget(props: AllWidgetProps>) { + /* ---------------- STATE ---------------- */ + const [jimuMapView, setJimuMapView] = useState(null); + const [isOpen, setIsOpen] = useState(false); + const [isCollapsed, setIsCollapsed] = useState(false); + const [isMaximized, setIsMaximized] = useState(false); + const [panelPos, setPanelPos] = useState<{ x: number; y: number } | null>(null); + const [panelSize, setPanelSize] = useState<{ width: number; height: number } | null>(null); + const [isLoadingLookup, setIsLoadingLookup] = useState(false); + const [lookupMessage, setLookupMessage] = useState(""); + const [erpMessage, setErpMessage] = useState(""); + const [erpStatus, setErpStatus] = useState< + "idle" | "loading" | "found" | "notfound" | "error" + >("idle"); + + /* ---------------- REFS ---------------- */ + const editorContainerRef = useRef(null); + const editorRef = useRef(null); + const pointLayerRef = useRef(null); + const lastCreatedGraphicRef = useRef(null); + const editorInitializingRef = useRef(false); + const destroyedRef = useRef(false); + const workflowWatchHandleRef = useRef<__esri.WatchHandle | null>(null); + const sketchCreateEventHandleRef = useRef<__esri.Handle | null>(null); + const billWatchHandleRef = useRef<__esri.WatchHandle | null>(null); + const billSearchTimerRef = useRef(null); + const billStatusElRef = useRef(null); + const panelRef = useRef(null); + const dragStateRef = useRef<{ startX: number; startY: number; originX: number; originY: number } | null>(null); + const resizeStateRef = useRef<{ startX: number; startY: number; originWidth: number; originHeight: number } | null>( + null + ); + const editorResizeObserverRef = useRef(null); + + /* ======================================================= + GET FORM 7 LAYER + ======================================================= */ + const getForm7Layer = (): FeatureLayer | null => { + const map = jimuMapView?.view?.map; + if (!map) return null; + + const existingLayer = map.layers.toArray().find((layer: any) => { + const title = String(layer.title || "").toLowerCase(); + const url = String(layer.url || "").toLowerCase(); + return ( + url === POINT_LAYER_URL.toLowerCase() || + title.includes("form 7") || + title.includes("form7") + ); + }) as FeatureLayer | undefined; + + return existingLayer || null; + }; + + /* ---------------- WORKFLOW / FORM VM ACCESSORS ---------------- */ + const getCurrentWorkflow = (): any => + editorRef.current?.viewModel?.activeWorkflow || null; + + const getCurrentFormViewModel = (): any => + getCurrentWorkflow()?.featureFormViewModel || null; + + const getCurrentSketchViewModel = (): any => + getCurrentWorkflow()?.sketchViewModel || null; + + /* ======================================================= + SET / GET FORM VALUE + ======================================================= */ + const setFormValue = (formVM: any, fieldName: string, value: any) => { + if (!formVM) { + console.warn("FormViewModel unavailable."); + return; + } + + const finalValue = value === null || value === undefined ? "" : value; + + try { + if (typeof formVM.setValue === "function") { + formVM.setValue(fieldName, finalValue); + return; + } + } catch (error) { + console.warn(`setValue failed for ${fieldName}:`, error); + } + + try { + if (typeof formVM.updateValues === "function") { + formVM.updateValues({ [fieldName]: finalValue }); + return; + } + } catch (error) { + console.warn(`updateValues failed for ${fieldName}:`, error); + } + + console.warn(`Could not update field ${fieldName}`); + }; + + const getFormValue = (formVM: any, fieldName: string): any => { + if (!formVM) return ""; + + try { + if (typeof formVM.getValue === "function") { + return formVM.getValue(fieldName); + } + } catch (error) { + console.warn(`getValue failed for ${fieldName}:`, error); + } + + try { + if (formVM.value && Object.prototype.hasOwnProperty.call(formVM.value, fieldName)) { + return formVM.value[fieldName]; + } + } catch (error) { + console.warn(`Unable to read ${fieldName}:`, error); + } + + return ""; + }; + + /* small helper: push a set of field values into formVM + graphic + form feature */ + const applyValuesToForm = (formVM: any, graphic: Graphic | null, values: Record) => { + if (formVM) { + Object.entries(values).forEach(([fieldName, value]) => setFormValue(formVM, fieldName, value)); + } + if (graphic?.attributes) { + Object.assign(graphic.attributes, values); + } + if (formVM?.feature?.attributes) { + Object.assign(formVM.feature.attributes, values); + } + }; + + /* ======================================================= + APPLY BUILDING LOOKUP + ======================================================= */ + const applyBuildingLookup = async (graphic: Graphic) => { + if (!graphic || !graphic.geometry) { + console.warn("Created point has no geometry."); + return; + } + + setIsLoadingLookup(true); + setLookupMessage("Finding building details..."); + + try { + const building = await getBuildingFromPoint(graphic.geometry); + + if (!building) { + setLookupMessage("No building found at the created point."); + return; + } + + console.log("Building found:", building); + + const formVM = getCurrentFormViewModel(); + + const values = { + [POINT_FIELDS.regionName]: safeValue(building.region_name), + [POINT_FIELDS.zone]: safeValue(building.zone_number), + [POINT_FIELDS.ward]: safeValue(building.ward_number), + [POINT_FIELDS.areaId]: safeValue(building.area_id), + [POINT_FIELDS.areaName]: safeValue(building.area_name), + [POINT_FIELDS.localityId]: safeValue(building.locality_id), + [POINT_FIELDS.locality]: safeValue(building.locality), + [POINT_FIELDS.roadId]: safeValue(building.road_id), + [POINT_FIELDS.roadName]: safeValue(building.road_name), + [POINT_FIELDS.finalGisId]: safeValue(building.gcc_gis_id), + [POINT_FIELDS.parentGisId]: safeValue(building.gcc_gis_id) + }; + + applyValuesToForm(formVM, graphic, values); + + lastCreatedGraphicRef.current = graphic; + + setLookupMessage( + `Building found: ${safeValue(building.gcc_gis_id) || "GIS ID unavailable"}` + ); + } catch (error) { + console.error("Building lookup failed:", error); + setLookupMessage("Building lookup failed."); + } finally { + setIsLoadingLookup(false); + } + }; + + /* ======================================================= + ERP SEARCH BY BILL NUMBER + IMPORTANT: MATCHING IS ONLY BY BILL_NUMBER. GISID IS NOT USED FOR MATCHING. + ======================================================= */ + const ensureBillStatusElement = () => { + if (billStatusElRef.current && document.body.contains(billStatusElRef.current)) { + return billStatusElRef.current; + } + + const container = editorContainerRef.current; + if (!container) return null; + + /* Recursively search shadow roots for the Bill Number field element */ + const findBillNoField = (root: ParentNode): HTMLElement | null => { + const candidates = Array.from(root.querySelectorAll("*")); + + for (const el of candidates) { + const attrMatch = + el.getAttribute?.("field-name") === POINT_FIELDS.billNo || + el.getAttribute?.("data-field-name") === POINT_FIELDS.billNo || + el.getAttribute?.("name") === POINT_FIELDS.billNo; + + if (attrMatch) return el as HTMLElement; + + const shadow = (el as HTMLElement).shadowRoot; + if (shadow) { + const found = findBillNoField(shadow); + if (found) return found; + } + } + + return null; + }; + + const fieldEl = findBillNoField(container); + + if (!fieldEl) { + console.warn(`Bill Number field element not found for ${POINT_FIELDS.billNo}`); + return null; + } + + /* Walk up to the nearest labeled block, so the status sits below the whole field */ + const wrapper = + fieldEl.closest( + "calcite-label, calcite-block, .esri-editor__form-group, [class*='field']" + ) || fieldEl; + + const statusEl = document.createElement("div"); + statusEl.className = "form7-bill-status"; + statusEl.style.cssText = "padding:0px 0px 10px;font-size:13px;color:#333;"; + + wrapper.insertAdjacentElement("afterend", statusEl); + + billStatusElRef.current = statusEl; + return statusEl; + }; + + const searchERPByBillNumber = async (billNumber: string) => { + const bill = String(billNumber || "").trim(); + if (!bill) return; + + const formVM = getCurrentFormViewModel(); + if (!formVM) { + console.warn("FormViewModel unavailable."); + return; + } + + const zoneNumber = getFormValue(formVM, POINT_FIELDS.zone); + const wardNumber = getFormValue(formVM, POINT_FIELDS.ward); + + const formattedZone = formatZone(zoneNumber); + const formattedWard = formatWard(wardNumber); + + if (!formattedZone || !formattedWard) { + setErpMessage("Zone and Ward are required before ERP lookup."); + setErpStatus("error"); + console.warn("Zone/Ward missing:", { zoneNumber, wardNumber }); + return; + } + + setErpMessage(`Searching ERP for Bill Number ${bill}...`); + setErpStatus("loading"); + + try { + const url = + `${ERP_API_URL}` + + `?serviceId=common_api` + + `&jsonResp=Yes` + + `&subService=getPropertyByGis` + + `&Zone=${encodeURIComponent(formattedZone)}` + + `&ward=${encodeURIComponent(formattedWard)}`; + + console.log("ERP API request:", url); + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`ERP API failed: ${response.status}`); + } + + const data = await response.json(); + console.log("ERP API response:", data); + + const records: ERPRecord[] = Array.isArray(data?.ListResult) ? data.ListResult : []; + + if (records.length === 0) { + setErpMessage(`No ERP records found for ${formattedZone} / ${formattedWard}.`); + setErpStatus("notfound"); + return; + } + + /* Match Bill Number only. No GISID check here. */ + const searchedBill = bill.trim().toUpperCase(); + + const matchedRecord = records.find( + (record: ERPRecord) => + String(record.BILL_NUMBER || "").trim().toUpperCase() === searchedBill + ); + + if (!matchedRecord) { + setErpMessage(`No ERP property found for Bill Number ${bill}.`); + setErpStatus("notfound"); + console.warn("Bill Number not found:", bill); + return; + } + + console.log("Matched ERP record:", matchedRecord); + + const addressParts = [ + matchedRecord.HOUSENO, + matchedRecord.STREET, + matchedRecord.LOCALITY, + matchedRecord.AREA + ] + .map((value) => String(value ?? "").trim()) + .filter(Boolean); + + const address = addressParts.join(", "); + + const erpValues = { + [POINT_FIELDS.doorOldNo]: safeValue(matchedRecord.OLD_HOUSENO), + [POINT_FIELDS.doorNewNo]: safeValue(matchedRecord.HOUSENO), + [POINT_FIELDS.zoneMis]: safeValue(matchedRecord.ZONE), + [POINT_FIELDS.wardMis]: safeValue(matchedRecord.WARD), + [POINT_FIELDS.misGisId]: safeValue(matchedRecord.GISID), + [POINT_FIELDS.billNo]: safeValue(matchedRecord.BILL_NUMBER), + [POINT_FIELDS.assesseeName]: safeValue(matchedRecord.OWNER_NAME), + [POINT_FIELDS.address]: address, + [POINT_FIELDS.mobile]: safeValue(matchedRecord.MOBILE_NO), + [POINT_FIELDS.category]: safeValue(matchedRecord.PROPERTY_TYPE), + [POINT_FIELDS.currentUsage]: safeValue(matchedRecord.USG_NAME), + [POINT_FIELDS.currentArea]: safeValue(matchedRecord.TOTAL_BUILTUP_AREA) + }; + + applyValuesToForm(formVM, lastCreatedGraphicRef.current, erpValues); + + setErpMessage(`ERP details loaded for Bill Number ${bill}.`); + setErpStatus("found"); + } catch (error) { + console.error("ERP Bill Number search failed:", error); + setErpMessage("Unable to fetch ERP property details."); + setErpStatus("error"); + } + }; + + /* ======================================================= + BILL NUMBER WATCHER + ======================================================= */ + const clearBillNumberWatcher = () => { + if (billWatchHandleRef.current) { + billWatchHandleRef.current.remove(); + billWatchHandleRef.current = null; + } + if (billSearchTimerRef.current) { + window.clearTimeout(billSearchTimerRef.current); + billSearchTimerRef.current = null; + } + }; + + const attachBillNumberWatcher = () => { + const formVM = getCurrentFormViewModel(); + + if (!formVM) { + console.warn("FormViewModel not available for Bill Number watcher."); + return; + } + + clearBillNumberWatcher(); + console.log("Attaching Bill Number watcher..."); + + billWatchHandleRef.current = reactiveUtils.watch( + () => { + const valueFromForm = + typeof formVM.getValue === "function" ? formVM.getValue(POINT_FIELDS.billNo) : null; + const valueFromFeature = formVM.feature?.attributes?.[POINT_FIELDS.billNo]; + + console.log("Bill values:", { valueFromForm, valueFromFeature }); + + return valueFromForm ?? valueFromFeature ?? ""; + }, + + (value: any) => { + const bill = String(value ?? "").trim(); + console.log("Bill Number changed:", bill); + + if (billSearchTimerRef.current) { + window.clearTimeout(billSearchTimerRef.current); + billSearchTimerRef.current = null; + } + + if (!bill) { + setErpMessage(""); + setErpStatus("idle"); + return; + } + + billSearchTimerRef.current = window.setTimeout(() => { + console.log("Calling ERP API for Bill Number:", bill); + void searchERPByBillNumber(bill); + }, 700); + } + ); + }; + + /* ======================================================= + SKETCH WATCHER + ======================================================= */ + const attachSketchWatcher = () => { + const sketchVM = getCurrentSketchViewModel(); + + if (!sketchVM) { + console.warn("SketchViewModel not available."); + return; + } + + if (sketchCreateEventHandleRef.current) { + sketchCreateEventHandleRef.current.remove(); + sketchCreateEventHandleRef.current = null; + } + + sketchCreateEventHandleRef.current = sketchVM.on("create", async (event: any) => { + console.log("Sketch create event:", event.state); + + if (event.state !== "complete") return; + + const graphic = event.graphic as Graphic; + if (!graphic) return; + + console.log("Form 7 point created:", graphic); + + lastCreatedGraphicRef.current = graphic; + setLookupMessage("Finding building..."); + setErpMessage(""); + setErpStatus("idle"); + + await applyBuildingLookup(graphic); + }); + }; + + /* ======================================================= + WATCH EDITOR WORKFLOW + ======================================================= */ + const watchEditorWorkflow = () => { + if (workflowWatchHandleRef.current) { + workflowWatchHandleRef.current.remove(); + workflowWatchHandleRef.current = null; + } + + workflowWatchHandleRef.current = reactiveUtils.watch( + () => editorRef.current?.viewModel?.activeWorkflow, + + (workflow: any) => { + console.log("Active Editor workflow:", workflow); + + if (workflow?.sketchViewModel) { + attachSketchWatcher(); + } + + if (workflow?.featureFormViewModel) { + console.log("Form 7 FormViewModel detected."); + /* Only now attach the Bill Number watcher - no ERP search UI + while the Editor is showing the layer list. */ + attachBillNumberWatcher(); + } else { + clearBillNumberWatcher(); + } + } + ); + }; + + /* ======================================================= + INITIALIZE EDITOR + ======================================================= */ + const initializeEditor = async () => { + if (destroyedRef.current || editorInitializingRef.current || editorRef.current) return; + + if (!jimuMapView?.view) { + console.warn("JimuMapView is not available."); + return; + } + + if (!editorContainerRef.current) { + console.warn("Editor container is not available."); + return; + } + + editorInitializingRef.current = true; + + try { + let pointLayer = getForm7Layer(); + + if (!pointLayer) { + pointLayer = new FeatureLayer({ + url: POINT_LAYER_URL, + title: "Form 7 Point", + outFields: ["*"] + }); + + jimuMapView.view.map.add(pointLayer); + } + + pointLayerRef.current = pointLayer; + await pointLayer.load(); + + // A broken contingent-values / arcade expression definition on the + // service throws "Identifier not recognized" during form construction + // and leaves the Editor body blank. We use a fully custom FormTemplate + // and don't rely on contingent values, so clear it defensively. + try { + if ((pointLayer as any).contingentValuesDefinition) { + (pointLayer as any).contingentValuesDefinition = null; + } + } catch (error) { + console.warn("Could not clear contingentValuesDefinition:", error); + } + + /* ================================================= + FORM TEMPLATE + ================================================= */ + const formTemplate = new FormTemplate({ + title: "Form 7", + elements: [ + { + type: "group", + label: "Property Details", + elements: [ + { type: "field", fieldName: POINT_FIELDS.regionName, label: "Region Name" }, + { type: "field", fieldName: POINT_FIELDS.zone, label: "Zone Number" }, + { type: "field", fieldName: POINT_FIELDS.ward, label: "Ward Number" }, + { type: "field", fieldName: POINT_FIELDS.areaId, label: "Area ID" }, + { type: "field", fieldName: POINT_FIELDS.areaName, label: "Area Name" }, + { type: "field", fieldName: POINT_FIELDS.localityId, label: "Locality ID" }, + { type: "field", fieldName: POINT_FIELDS.locality, label: "Locality" }, + { type: "field", fieldName: POINT_FIELDS.roadId, label: "Road ID" }, + { type: "field", fieldName: POINT_FIELDS.roadName, label: "Road Name" }, + { type: "field", fieldName: POINT_FIELDS.finalGisId, label: "Final GIS ID" }, + { type: "field", fieldName: POINT_FIELDS.parentGisId, label: "Parent GIS ID" } + ] + }, + { + type: "group", + label: "ERP Property Details", + elements: [ + { type: "field", fieldName: POINT_FIELDS.billNo, label: "Bill Number" }, + { type: "field", fieldName: POINT_FIELDS.doorOldNo, label: "Old Door No" }, + { type: "field", fieldName: POINT_FIELDS.doorNewNo, label: "New Door No" }, + { type: "field", fieldName: POINT_FIELDS.zoneMis, label: "MIS Zone" }, + { type: "field", fieldName: POINT_FIELDS.wardMis, label: "MIS Ward" }, + { type: "field", fieldName: POINT_FIELDS.misGisId, label: "MIS GIS ID" }, + { type: "field", fieldName: POINT_FIELDS.assesseeName, label: "Assessee Name" }, + { type: "field", fieldName: POINT_FIELDS.address, label: "Address" }, + { type: "field", fieldName: POINT_FIELDS.mobile, label: "Mobile" }, + { type: "field", fieldName: POINT_FIELDS.category, label: "Category" }, + { type: "field", fieldName: POINT_FIELDS.currentUsage, label: "Current Usage" }, + { type: "field", fieldName: POINT_FIELDS.currentArea, label: "Current Area" } + ] + }, + { + type: "group", + label: "Other Details", + elements: [ + { type: "field", fieldName: POINT_FIELDS.status, label: "Status" }, + { type: "field", fieldName: POINT_FIELDS.remarks, label: "Remarks" } + ] + } + ] + }); + + /* ================================================= + CREATE ARCGIS EDITOR + ================================================= */ + const editor = new Editor({ + view: jimuMapView.view, + container: editorContainerRef.current, + visibleElements: { + createFeaturesSection: true, + editFeaturesSection: false // hides "Edit features", leaving only "Create features" + }, + layerInfos: [{ layer: pointLayer, formTemplate }] + }); + + editorRef.current = editor; + await editor.when(); + + console.log("Form 7 Editor initialized successfully."); + watchEditorWorkflow(); + + // Calcite/Editor sometimes mounts with zero computed size when placed + // inside an absolutely-positioned flex container (drag/resize panel). + // Nudging a resize + re-checking on the next frame forces it to + // recompute its internal layout instead of rendering blank. + window.dispatchEvent(new Event("resize")); + requestAnimationFrame(() => { + window.dispatchEvent(new Event("resize")); + }); + + // Keep the Editor's internal layout in sync whenever this container's + // actual size changes (panel resize, restore-from-collapse, maximize). + if (editorResizeObserverRef.current) { + editorResizeObserverRef.current.disconnect(); + } + + const resizeObserver = new ResizeObserver(() => { + window.dispatchEvent(new Event("resize")); + }); + + resizeObserver.observe(editorContainerRef.current); + editorResizeObserverRef.current = resizeObserver; + } catch (error) { + console.error("Editor initialization failed:", error); + } finally { + editorInitializingRef.current = false; + } + }; + + /* ======================================================= + TEARDOWN (shared by closeEditor + unmount cleanup) + ======================================================= */ + const teardownEditor = () => { + clearBillNumberWatcher(); + + if (editorResizeObserverRef.current) { + editorResizeObserverRef.current.disconnect(); + editorResizeObserverRef.current = null; + } + + if (sketchCreateEventHandleRef.current) { + sketchCreateEventHandleRef.current.remove(); + sketchCreateEventHandleRef.current = null; + } + + if (workflowWatchHandleRef.current) { + workflowWatchHandleRef.current.remove(); + workflowWatchHandleRef.current = null; + } + + if (editorRef.current) { + try { + editorRef.current.destroy(); + } catch (error) { + console.warn("Editor destroy failed:", error); + } + editorRef.current = null; + } + }; + + /* ======================================================= + ACTIVE MAP VIEW CHANGE + ======================================================= */ + const activeViewChangeHandler = (jmv: JimuMapView) => { + setJimuMapView(jmv); + }; + + /* ======================================================= + DRAG (free movement) — disabled while maximized + ======================================================= */ + const handleDragStart = (e: React.MouseEvent) => { + if (isMaximized) return; + + const panelEl = panelRef.current; + if (!panelEl) return; + + // offsetLeft/offsetTop are relative to the panel's positioning ancestor — + // the same coordinate space its CSS `left`/`top` are set in. Using + // getBoundingClientRect (viewport-relative) here would jump the panel + // off-position as soon as dragging starts if that ancestor isn't flush + // with the browser edge. + const originX = panelPos?.x ?? panelEl.offsetLeft; + const originY = panelPos?.y ?? panelEl.offsetTop; + + dragStateRef.current = { + startX: e.clientX, + startY: e.clientY, + originX, + originY + }; + + const handleMouseMove = (moveEvent: MouseEvent) => { + const drag = dragStateRef.current; + if (!drag) return; + + const deltaX = moveEvent.clientX - drag.startX; + const deltaY = moveEvent.clientY - drag.startY; + + const nextX = Math.max(0, drag.originX + deltaX); + const nextY = Math.max(0, drag.originY + deltaY); + + setPanelPos({ x: nextX, y: nextY }); + }; + + const handleMouseUp = () => { + dragStateRef.current = null; + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + }; + + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + }; + + const toggleMaximize = () => { + setIsMaximized((prev) => !prev); + }; + + /* ======================================================= + RESIZE (mouse-driven, bottom-right handle) — disabled while maximized/collapsed + ======================================================= */ + const handleResizeStart = (e: React.MouseEvent) => { + if (isMaximized || isCollapsed) return; + + const panelEl = panelRef.current; + if (!panelEl) return; + + const rect = panelEl.getBoundingClientRect(); + const originWidth = panelSize?.width ?? rect.width; + const originHeight = panelSize?.height ?? rect.height; + + resizeStateRef.current = { + startX: e.clientX, + startY: e.clientY, + originWidth, + originHeight + }; + + const MIN_WIDTH = 280; + const MIN_HEIGHT = 200; + + const handleMouseMove = (moveEvent: MouseEvent) => { + const resize = resizeStateRef.current; + if (!resize) return; + + const deltaX = moveEvent.clientX - resize.startX; + const deltaY = moveEvent.clientY - resize.startY; + + const nextWidth = Math.max(MIN_WIDTH, resize.originWidth + deltaX); + const nextHeight = Math.max(MIN_HEIGHT, resize.originHeight + deltaY); + + setPanelSize({ width: nextWidth, height: nextHeight }); + }; + + const handleMouseUp = () => { + resizeStateRef.current = null; + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + }; + + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + }; + + /* ======================================================= + OPEN / CLOSE EDITOR + ======================================================= */ + const openEditor = () => { + setIsOpen(true); + setIsCollapsed(false); + setIsMaximized(false); + setPanelPos(null); + setPanelSize(null); + setLookupMessage(""); + setErpMessage(""); + + // Wait for the panel to actually paint before initializing the Editor — + // a plain setTimeout can fire before layout/paint settles on first open, + // which is why the form only renders correctly after a close/reopen. + requestAnimationFrame(() => { + requestAnimationFrame(() => { + void initializeEditor(); + }); + }); + }; + + const closeEditor = () => { + teardownEditor(); + + lastCreatedGraphicRef.current = null; + pointLayerRef.current = null; + + if (billStatusElRef.current) { + billStatusElRef.current.remove(); + billStatusElRef.current = null; + } + + setLookupMessage(""); + setErpMessage(""); + setIsLoadingLookup(false); + setIsOpen(false); + }; + + /* ======================================================= + INITIALIZE WHEN MAP VIEW + PANEL AVAILABLE + ======================================================= */ + useEffect(() => { + if (!jimuMapView?.view || !isOpen) return; + void initializeEditor(); + }, [jimuMapView, isOpen]); + + useEffect(() => { + const el = ensureBillStatusElement(); + if (el) { + el.textContent = erpMessage; + el.style.color = + erpStatus === "found" + ? "#1a7a1a" + : erpStatus === "notfound" || erpStatus === "error" + ? "#c0392b" + : "#333"; + } + }, [erpMessage, erpStatus]); + + /* ======================================================= + CLEANUP ON UNMOUNT + ======================================================= */ + useEffect(() => { + destroyedRef.current = false; + + return () => { + destroyedRef.current = true; + teardownEditor(); + }; + }, []); + + /* ======================================================= + RENDER + ======================================================= */ + return ( +
+ {/* MAP */} + + + {/* FLOATING FORM 7 BUTTON */} + {!isOpen && ( + + )} + + {/* FORM 7 PANEL */} + {isOpen && ( +
+ {/* HEADER (drag handle) */} +
+
Form 7
+ +
+ {/* MAXIMIZE / RESTORE */} + + + {/* COLLAPSE */} + + + {/* CLOSE */} + +
+
+ + {/* BODY — kept mounted even while collapsed so the ArcGIS Editor + stays bound to its container element; only visually hidden. */} +
+ {/* ARCGIS EDITOR */} +
+
+ + {/* RESIZE HANDLE (bottom-right) */} + {!isCollapsed && !isMaximized && ( +
+ )} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/CreateAsset/src/setting/setting.tsx b/CreateAsset/src/setting/setting.tsx new file mode 100644 index 0000000..3a07818 --- /dev/null +++ b/CreateAsset/src/setting/setting.tsx @@ -0,0 +1,39 @@ +import { React } from 'jimu-core' +import { type AllWidgetSettingProps } from 'jimu-for-builder' +import { MapWidgetSelector } from 'jimu-ui/advanced/setting-components' + +const Setting = ( + props: AllWidgetSettingProps +) => { + + const onMapWidgetSelected = ( + useMapWidgetIds: string[] + ) => { + + props.onSettingChange({ + id: props.id, + useMapWidgetIds + }) + } + + return ( +
+ + + +
+ ) +} + +export default Setting \ No newline at end of file