From e465c1261f98fd4bbb277a74e1a9c8263b2ee70f Mon Sep 17 00:00:00 2001 From: Anitha_M Date: Sat, 19 Sep 2026 17:04:20 +0530 Subject: [PATCH] Custom_Widgets --- Form7ApprovalPending/config.json | 1 + Form7ApprovalPending/icon.svg | 1 + Form7ApprovalPending/manifest.json | 29 + Form7ApprovalPending/src/config.ts | 7 + Form7ApprovalPending/src/runtime/widget.tsx | 1317 +++++++++ Form7Buildingdatesummary/config.json | 5 + Form7Buildingdatesummary/icon.svg | 3 + Form7Buildingdatesummary/manifest.json | 19 + Form7Buildingdatesummary/src/config.ts | 7 + .../src/runtime/components/LoadingPopup.css | 45 + .../src/runtime/components/LoadingPopup.tsx | 23 + .../src/runtime/components/LoadingSpinner.jsx | 23 + .../src/runtime/widget.tsx | 1548 ++++++++++ .../src/setting/setting.tsx | 20 + Form7Buildingdatesummary/src/zoom-in.png | Bin 0 -> 23306 bytes Form7Generartion_Filter/config.json | 5 + Form7Generartion_Filter/icon.svg | 4 + Form7Generartion_Filter/manifest.json | 19 + Form7Generartion_Filter/src/config.ts | 7 + .../src/runtime/assets/zoom-in.svg | 25 + .../src/runtime/components/LoadingPopup.css | 45 + .../src/runtime/components/LoadingPopup.tsx | 23 + .../src/runtime/components/LoadingSpinner.jsx | 23 + .../src/runtime/simple_widget.css | 857 ++++++ .../src/runtime/widget.tsx | 1006 +++++++ .../src/setting/setting.tsx | 20 + Form7Generartion_Filter/src/zoom-in.png | Bin 0 -> 23306 bytes .../tests/simple-widget.test.tsx | 14 + Form7Query_Approval/config.json | 5 + Form7Query_Approval/icon.svg | 27 + Form7Query_Approval/manifest.json | 19 + Form7Query_Approval/src/config.ts | 7 + .../src/runtime/assets/zoom-in.svg | 25 + .../src/runtime/components/LoadingPopup.css | 45 + .../src/runtime/components/LoadingPopup.tsx | 23 + .../src/runtime/components/LoadingSpinner.jsx | 23 + .../src/runtime/simple_widget.css | 155 + Form7Query_Approval/src/runtime/widget.tsx | 820 ++++++ Form7Query_Approval/src/setting/setting.tsx | 20 + Form7Query_Approval/src/zoom-in.png | Bin 0 -> 23306 bytes .../tests/simple-widget.test.tsx | 14 + custom-popup/config.json | 1 + custom-popup/icon.svg | 4 + custom-popup/manifest.json | 20 + custom-popup/src/runtime/custom-popup.css | 80 + custom-popup/src/runtime/widget.tsx | 2571 +++++++++++++++++ custom-popup/src/setting/setting.tsx | 18 + 47 files changed, 8973 insertions(+) create mode 100644 Form7ApprovalPending/config.json create mode 100644 Form7ApprovalPending/icon.svg create mode 100644 Form7ApprovalPending/manifest.json create mode 100644 Form7ApprovalPending/src/config.ts create mode 100644 Form7ApprovalPending/src/runtime/widget.tsx create mode 100644 Form7Buildingdatesummary/config.json create mode 100644 Form7Buildingdatesummary/icon.svg create mode 100644 Form7Buildingdatesummary/manifest.json create mode 100644 Form7Buildingdatesummary/src/config.ts create mode 100644 Form7Buildingdatesummary/src/runtime/components/LoadingPopup.css create mode 100644 Form7Buildingdatesummary/src/runtime/components/LoadingPopup.tsx create mode 100644 Form7Buildingdatesummary/src/runtime/components/LoadingSpinner.jsx create mode 100644 Form7Buildingdatesummary/src/runtime/widget.tsx create mode 100644 Form7Buildingdatesummary/src/setting/setting.tsx create mode 100644 Form7Buildingdatesummary/src/zoom-in.png create mode 100644 Form7Generartion_Filter/config.json create mode 100644 Form7Generartion_Filter/icon.svg create mode 100644 Form7Generartion_Filter/manifest.json create mode 100644 Form7Generartion_Filter/src/config.ts create mode 100644 Form7Generartion_Filter/src/runtime/assets/zoom-in.svg create mode 100644 Form7Generartion_Filter/src/runtime/components/LoadingPopup.css create mode 100644 Form7Generartion_Filter/src/runtime/components/LoadingPopup.tsx create mode 100644 Form7Generartion_Filter/src/runtime/components/LoadingSpinner.jsx create mode 100644 Form7Generartion_Filter/src/runtime/simple_widget.css create mode 100644 Form7Generartion_Filter/src/runtime/widget.tsx create mode 100644 Form7Generartion_Filter/src/setting/setting.tsx create mode 100644 Form7Generartion_Filter/src/zoom-in.png create mode 100644 Form7Generartion_Filter/tests/simple-widget.test.tsx create mode 100644 Form7Query_Approval/config.json create mode 100644 Form7Query_Approval/icon.svg create mode 100644 Form7Query_Approval/manifest.json create mode 100644 Form7Query_Approval/src/config.ts create mode 100644 Form7Query_Approval/src/runtime/assets/zoom-in.svg create mode 100644 Form7Query_Approval/src/runtime/components/LoadingPopup.css create mode 100644 Form7Query_Approval/src/runtime/components/LoadingPopup.tsx create mode 100644 Form7Query_Approval/src/runtime/components/LoadingSpinner.jsx create mode 100644 Form7Query_Approval/src/runtime/simple_widget.css create mode 100644 Form7Query_Approval/src/runtime/widget.tsx create mode 100644 Form7Query_Approval/src/setting/setting.tsx create mode 100644 Form7Query_Approval/src/zoom-in.png create mode 100644 Form7Query_Approval/tests/simple-widget.test.tsx create mode 100644 custom-popup/config.json create mode 100644 custom-popup/icon.svg create mode 100644 custom-popup/manifest.json create mode 100644 custom-popup/src/runtime/custom-popup.css create mode 100644 custom-popup/src/runtime/widget.tsx create mode 100644 custom-popup/src/setting/setting.tsx diff --git a/Form7ApprovalPending/config.json b/Form7ApprovalPending/config.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/Form7ApprovalPending/config.json @@ -0,0 +1 @@ +{} diff --git a/Form7ApprovalPending/icon.svg b/Form7ApprovalPending/icon.svg new file mode 100644 index 0000000..39ab732 --- /dev/null +++ b/Form7ApprovalPending/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Form7ApprovalPending/manifest.json b/Form7ApprovalPending/manifest.json new file mode 100644 index 0000000..8b83821 --- /dev/null +++ b/Form7ApprovalPending/manifest.json @@ -0,0 +1,29 @@ +{ + "name": "Form7ApprovalPending", + "label": "Form7ApprovalPending", + "type": "widget", + "version": "1.16.0", + "exbVersion": "1.16.0", + "author": "Tanii GIS Developer", + "description": "This is a barebones, simple widget. It includes an example of how to use the config file, and it includes a placeholder SVG icon. It intentionally does not include the following so you can use this to start from scratch: i18n translations, a Settings panel, unit tests. See the documentation for more information on how to implement these features.", + "copyright": "", + "license": "http://www.apache.org/licenses/LICENSE-2.0", + "properties": { + "hasSettingPage": false + }, + "widgets": [ + { + "uri": "Approval_Pending", + "label": "Approval_Pending", + "thumbnail": "thumbnail.png", + "description": "Summarizes Street Light, Highmast, and Pillar Box counts by zone." + } + ], + "framework": "react", + "translatedLocales": ["en"], + "dataSources": [], + "elements": [] +} + + + diff --git a/Form7ApprovalPending/src/config.ts b/Form7ApprovalPending/src/config.ts new file mode 100644 index 0000000..780aacf --- /dev/null +++ b/Form7ApprovalPending/src/config.ts @@ -0,0 +1,7 @@ +import { type ImmutableObject } from 'seamless-immutable' + +export interface Config { + exampleConfigProperty: string +} + +export type IMConfig = ImmutableObject diff --git a/Form7ApprovalPending/src/runtime/widget.tsx b/Form7ApprovalPending/src/runtime/widget.tsx new file mode 100644 index 0000000..1100adf --- /dev/null +++ b/Form7ApprovalPending/src/runtime/widget.tsx @@ -0,0 +1,1317 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import FeatureLayer from '@arcgis/core/layers/FeatureLayer'; +import { + Box, + Typography, + FormControl, + Select, + MenuItem, + InputLabel, + Button +} from '@mui/material'; + +interface Form7Record { + zone: string; + ward: string; + status: string; +} + +interface ZoneData { + zone_no: string; + pendingCount: number; + toBeVerifiedCount: number; + totalCount: number; +} + +const FORM7_POINT_LAYER_URL = + 'https://gisgcc.chennaicorporation.gov.in/server/rest/services/Revenue/Form7_Point/FeatureServer/0'; + +const ZoneIntimationOnly = () => { + // ===================================================== + // DATA + // ===================================================== + + const [records, setRecords] = useState([]); + + const [loading, setLoading] = useState(true); + + const [error, setError] = useState(''); + + // ===================================================== + // FILTERS + // ===================================================== + + const [selectedZone, setSelectedZone] = useState(''); + + const [selectedWard, setSelectedWard] = useState(''); + + const [selectedStatus, setSelectedStatus] = useState(''); + + // Skeleton filters for tomorrow + const [selectedCreatedUser, setSelectedCreatedUser] = + useState(''); + + const [selectedCreatedDate, setSelectedCreatedDate] = + useState(''); + + // ===================================================== + // NORMALIZE ZONE + // ===================================================== + + const normalizeZone = (zone: any): string => { + if ( + zone === null || + zone === undefined || + zone === '' + ) { + return ''; + } + + const value = String(zone).trim(); + + const match = value.match(/\d+/); + + if (!match) { + return ''; + } + + return match[0].padStart(2, '0'); + }; + + // ===================================================== + // NORMALIZE TEXT + // ===================================================== + + const normalizeText = (value: any): string => { + if ( + value === null || + value === undefined + ) { + return ''; + } + + return String(value).trim(); + }; + + // ===================================================== + // FETCH FORM 7 DATA + // ===================================================== + + const fetchData = async () => { + setLoading(true); + + setError(''); + + try { + const form7Layer = new FeatureLayer({ + url: FORM7_POINT_LAYER_URL + }); + + await form7Layer.load(); + + console.log( + 'Form 7 Point layer loaded:', + form7Layer + ); + + console.table( + form7Layer.fields.map(field => ({ + name: field.name, + alias: field.alias, + type: field.type + })) + ); + + // ================================================= + // QUERY + // ================================================= + + const query = form7Layer.createQuery(); + + query.where = '1=1'; + + query.outFields = [ + 'zone_number', + 'ward_number', + 'status' + ]; + + query.returnGeometry = false; + + console.log( + 'Form 7 query:', + { + where: query.where, + outFields: query.outFields + } + ); + + const result = + await form7Layer.queryFeatures(query); + + console.log( + 'Form 7 query result:', + result + ); + + console.log( + 'Form 7 record count:', + result.features.length + ); + + // ================================================= + // CONVERT TO SIMPLE RECORDS + // ================================================= + + const formattedRecords: Form7Record[] = + result.features + .map(feature => { + + const attributes = + feature.attributes || {}; + + const zone = + normalizeZone( + attributes.zone_number + ); + + const ward = + normalizeText( + attributes.ward_number + ); + + const status = + normalizeText( + attributes.status + ).toLowerCase(); + + return { + zone, + ward, + status + }; + }) + .filter(record => record.zone !== ''); + + console.log( + 'Formatted Form 7 records:', + formattedRecords + ); + + setRecords(formattedRecords); + + } catch (err: any) { + + console.error( + 'Error loading Form 7 Point data:', + err + ); + + console.error( + 'Error details:', + err?.details + ); + + setError( + err?.message || + 'Unable to load Form 7 Point data.' + ); + + setRecords([]); + + } finally { + + setLoading(false); + + } + }; + + // ===================================================== + // LOAD DATA + // ===================================================== + + useEffect(() => { + fetchData(); + }, []); + + // ===================================================== + // ZONE DROPDOWN + // ===================================================== + + const zones = useMemo(() => { + + const uniqueZones = + Array.from( + new Set( + records + .map(record => record.zone) + .filter(Boolean) + ) + ); + + return uniqueZones.sort( + (a, b) => + parseInt(a, 10) - + parseInt(b, 10) + ); + + }, [records]); + + // ===================================================== + // WARD DROPDOWN + // ===================================================== + + const wards = useMemo(() => { + + const filteredRecords = + selectedZone + ? records.filter( + record => + record.zone === selectedZone + ) + : records; + + const uniqueWards = + Array.from( + new Set( + filteredRecords + .map(record => record.ward) + .filter(Boolean) + ) + ); + + return uniqueWards.sort( + (a, b) => { + + const numA = + parseInt(a.replace(/\D/g, ''), 10); + + const numB = + parseInt(b.replace(/\D/g, ''), 10); + + if ( + !isNaN(numA) && + !isNaN(numB) + ) { + return numA - numB; + } + + return a.localeCompare(b); + } + ); + + }, [records, selectedZone]); + + // ===================================================== + // STATUS DROPDOWN + // ===================================================== + + const statuses = useMemo(() => { + + const uniqueStatuses = + Array.from( + new Set( + records + .map(record => record.status) + .filter(Boolean) + ) + ); + + return uniqueStatuses; + + }, [records]); + + // ===================================================== + // FILTERED RECORDS + // ===================================================== + + const filteredRecords = useMemo(() => { + + return records.filter(record => { + + if ( + selectedZone && + record.zone !== selectedZone + ) { + return false; + } + + if ( + selectedWard && + record.ward !== selectedWard + ) { + return false; + } + + if ( + selectedStatus && + record.status !== + selectedStatus.toLowerCase() + ) { + return false; + } + + // ================================================= + // CREATED USER + // ================================================= + // Editor tracking will be connected tomorrow. + // Currently no filtering is applied. + + // ================================================= + // CREATED DATE + // ================================================= + // Editor tracking will be connected tomorrow. + // Currently no filtering is applied. + + return true; + }); + + }, [ + records, + selectedZone, + selectedWard, + selectedStatus, + selectedCreatedUser, + selectedCreatedDate + ]); + + // ===================================================== + // CREATE ZONE SUMMARY + // ===================================================== + + const zoneData: ZoneData[] = useMemo(() => { + + const pendingMap: Record< + string, + number + > = {}; + + const tbvMap: Record< + string, + number + > = {}; + + const totalMap: Record< + string, + number + > = {}; + + filteredRecords.forEach(record => { + + const zone = + record.zone; + + if (!zone) { + return; + } + + // TOTAL + + totalMap[zone] = + (totalMap[zone] || 0) + 1; + + // PENDING + + if ( + record.status === 'pending' + ) { + pendingMap[zone] = + (pendingMap[zone] || 0) + 1; + } + + // TO BE VERIFIED + + if ( + record.status === + 'to be verified' + ) { + tbvMap[zone] = + (tbvMap[zone] || 0) + 1; + } + + }); + + const allZones = + new Set([ + ...Object.keys(totalMap), + ...Object.keys(pendingMap), + ...Object.keys(tbvMap) + ]); + + const result: ZoneData[] = + Array.from(allZones).map( + zone => ({ + zone_no: zone, + + pendingCount: + pendingMap[zone] || 0, + + toBeVerifiedCount: + tbvMap[zone] || 0, + + totalCount: + totalMap[zone] || 0 + }) + ); + + result.sort( + (a, b) => + parseInt(a.zone_no, 10) - + parseInt(b.zone_no, 10) + ); + + return result; + + }, [filteredRecords]); + + // ===================================================== + // SUMMARY CARD COUNTS + // ===================================================== + + const totalPending = + filteredRecords.filter( + record => + record.status === 'pending' + ).length; + + const totalTBV = + filteredRecords.filter( + record => + record.status === + 'to be verified' + ).length; + + const totalRecords = + filteredRecords.length; + + // ===================================================== + // RESET FILTERS + // ===================================================== + + const resetFilters = () => { + + setSelectedZone(''); + + setSelectedWard(''); + + setSelectedStatus(''); + + setSelectedCreatedUser(''); + + setSelectedCreatedDate(''); + + }; + + // ===================================================== + // LOADING + // ===================================================== + + if (loading) { + + return ( + + + + + + Loading Form 7 Point summary... + + + + + + ); + } + + // ===================================================== + // ERROR + // ===================================================== + + if (error) { + + return ( + + + + + + {error} + + + + + + ); + } + + // ===================================================== + // MAIN UI + // ===================================================== + + return ( + + + {/* ================================================= + HEADER + ================================================= */} + + + + + Form 7 Point Intimation + + + + Zone-wise approval status summary + + + + + {/* ================================================= + SUMMARY CARDS + ================================================= */} + + + + {/* TOTAL */} + + + + + TOTAL + + + + {totalRecords.toLocaleString()} + + + + + {/* PENDING */} + + + + + PENDING + + + + {totalPending.toLocaleString()} + + + + + {/* TO BE VERIFIED */} + + + + + TO BE VERIFIED + + + + {totalTBV.toLocaleString()} + + + + + + + {/* ================================================= + FILTER CARD + ================================================= */} + + + + + + + Filters + + + + + + + {/* FILTER ROW */} + + + + {/* ZONE */} + + + + + Zone + + + + + + + {/* WARD */} + + + + + Ward + + + + + + + {/* STATUS */} + + + + + Approval Status + + + + + + + {/* CREATED USER SKELETON */} + + + + + Created User + + + + + + + {/* CREATED DATE SKELETON */} + + + + + Created Date + + + + + + + + + {/* EDITOR TRACKING NOTE */} + + + + + Created User and Created Date + filters will be enabled after + Editor Tracking fields are + available. + + + + + + + {/* ================================================= + ZONE TABLE + ================================================= */} + + + + {/* TABLE HEADER */} + + + + + ZONE + + + + PENDING + + + + TO BE VERIFIED + + + + TOTAL + + + + + {/* TABLE BODY */} + + + + {zoneData.length === 0 ? ( + + + + + No records found for the + selected filters. + + + + + ) : ( + + zoneData.map( + (zone, index) => ( + + + + {/* ZONE */} + + + Zone {zone.zone_no} + + + {/* PENDING */} + + + + + 0 + ? '#fff3e0' + : '#f5f5f5', + color: + zone.pendingCount > + 0 + ? '#ef6c00' + : '#9e9e9e', + textAlign: + 'center', + fontSize: 12, + fontWeight: 700 + }} + > + {zone.pendingCount.toLocaleString()} + + + + + {/* TO BE VERIFIED */} + + + + + 0 + ? '#e8f5e9' + : '#f5f5f5', + color: + zone.toBeVerifiedCount > + 0 + ? '#2e7d32' + : '#9e9e9e', + textAlign: + 'center', + fontSize: 12, + fontWeight: 700 + }} + > + {zone.toBeVerifiedCount.toLocaleString()} + + + + + {/* TOTAL */} + + + {zone.totalCount.toLocaleString()} + + + + + ) + ) + + )} + + + + + + + ); +}; + +export default ZoneIntimationOnly; \ No newline at end of file diff --git a/Form7Buildingdatesummary/config.json b/Form7Buildingdatesummary/config.json new file mode 100644 index 0000000..0ce5fd8 --- /dev/null +++ b/Form7Buildingdatesummary/config.json @@ -0,0 +1,5 @@ +{ + "exampleConfigProperty": "test", + "useMapWidget": true + +} diff --git a/Form7Buildingdatesummary/icon.svg b/Form7Buildingdatesummary/icon.svg new file mode 100644 index 0000000..77fac85 --- /dev/null +++ b/Form7Buildingdatesummary/icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Form7Buildingdatesummary/manifest.json b/Form7Buildingdatesummary/manifest.json new file mode 100644 index 0000000..7374658 --- /dev/null +++ b/Form7Buildingdatesummary/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "Form7Buildingdatesummary", + "label": "Form7Buildingdatesummary", + "type": "widget", + "version": "1.16.0", + "exbVersion": "1.16.0", + "author": "Esri R&D Center Beijing", + "description": "This is a barebones, simple widget. It includes an example of how to use the config file, and it includes a placeholder SVG icon. It intentionally does not include the following so you can use this to start from scratch: i18n translations, a Settings panel, unit tests. See the documentation for more information on how to implement these features.", + "copyright": "", + "license": "http://www.apache.org/licenses/LICENSE-2.0", + "properties": {}, + "translatedLocales": [ + "en" + ], + "defaultSize": { + "width": 800, + "height": 500 + } +} \ No newline at end of file diff --git a/Form7Buildingdatesummary/src/config.ts b/Form7Buildingdatesummary/src/config.ts new file mode 100644 index 0000000..780aacf --- /dev/null +++ b/Form7Buildingdatesummary/src/config.ts @@ -0,0 +1,7 @@ +import { type ImmutableObject } from 'seamless-immutable' + +export interface Config { + exampleConfigProperty: string +} + +export type IMConfig = ImmutableObject diff --git a/Form7Buildingdatesummary/src/runtime/components/LoadingPopup.css b/Form7Buildingdatesummary/src/runtime/components/LoadingPopup.css new file mode 100644 index 0000000..5d83905 --- /dev/null +++ b/Form7Buildingdatesummary/src/runtime/components/LoadingPopup.css @@ -0,0 +1,45 @@ +.loading-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; + } + + .loading-box { + background: #fff; + padding: 24px; + border-radius: 8px; + width: 280px; + text-align: center; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3); + } + + .loading-title { + font-size: 1.2rem; + font-weight: bold; + margin-bottom: 16px; + } + + .spinner { + border: 4px solid #e0e0e0; + border-top: 4px solid #009688; + border-radius: 50%; + width: 32px; + height: 32px; + animation: spin 1s linear infinite; + margin: 0 auto 12px auto; + } + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + + .loading-message { + color: #555; + } + \ No newline at end of file diff --git a/Form7Buildingdatesummary/src/runtime/components/LoadingPopup.tsx b/Form7Buildingdatesummary/src/runtime/components/LoadingPopup.tsx new file mode 100644 index 0000000..0a2cc49 --- /dev/null +++ b/Form7Buildingdatesummary/src/runtime/components/LoadingPopup.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import './LoadingPopup.css'; + +interface LoadingPopupProps { + show: boolean; + message?: string; +} + +const LoadingPopup: React.FC = ({ show, message = "Preparing to download ..." }) => { + if (!show) return null; + + return ( +
+
+

Please Wait..

+
+

{message}

+
+
+ ); +}; + +export default LoadingPopup; diff --git a/Form7Buildingdatesummary/src/runtime/components/LoadingSpinner.jsx b/Form7Buildingdatesummary/src/runtime/components/LoadingSpinner.jsx new file mode 100644 index 0000000..0a2cc49 --- /dev/null +++ b/Form7Buildingdatesummary/src/runtime/components/LoadingSpinner.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import './LoadingPopup.css'; + +interface LoadingPopupProps { + show: boolean; + message?: string; +} + +const LoadingPopup: React.FC = ({ show, message = "Preparing to download ..." }) => { + if (!show) return null; + + return ( +
+
+

Please Wait..

+
+

{message}

+
+
+ ); +}; + +export default LoadingPopup; diff --git a/Form7Buildingdatesummary/src/runtime/widget.tsx b/Form7Buildingdatesummary/src/runtime/widget.tsx new file mode 100644 index 0000000..0019921 --- /dev/null +++ b/Form7Buildingdatesummary/src/runtime/widget.tsx @@ -0,0 +1,1548 @@ +import React, { useEffect, useState } from 'react'; +import { AllWidgetProps } from 'jimu-core'; +import FeatureLayer from '@arcgis/core/layers/FeatureLayer'; + +type SummaryRow = { + zone: string; + countsByDate: { [date: string]: number }; + total: number; +}; + +const ZoneWardDateSummary = (props: AllWidgetProps) => { + + // ============================================================ + // FORM 7 POINT LAYER + // ============================================================ + + const pointLayerUrl = + 'https://gisgcc.chennaicorporation.gov.in/server/rest/services/Revenue/Form7_Point/FeatureServer/0'; + + // ============================================================ + // STATE + // ============================================================ + + const [zones, setZones] = useState([]); + const [selectedZone, setSelectedZone] = useState(''); + + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + + const [summaryRows, setSummaryRows] = useState([]); + const [dateColumns, setDateColumns] = useState([]); + + const [loadingZones, setLoadingZones] = useState(false); + const [loading, setLoading] = useState(false); + + const [message, setMessage] = useState(''); + const [messageType, setMessageType] = + useState<'error' | 'success' | ''>(''); + + // ============================================================ + // HELPERS + // ============================================================ + + const fmtDateKey = (d: Date) => { + const y = d.getFullYear(); + const m = (d.getMonth() + 1).toString().padStart(2, '0'); + const day = d.getDate().toString().padStart(2, '0'); + + return `${y}-${m}-${day}`; + }; + + const displayDate = (iso: string) => { + const d = new Date(`${iso}T00:00:00`); + + const dd = d.getDate().toString().padStart(2, '0'); + const mm = (d.getMonth() + 1).toString().padStart(2, '0'); + const yy = d.getFullYear().toString().slice(-2); + + return `${dd}.${mm}.${yy}`; + }; + + const rangeDates = ( + startISO: string, + endISO: string + ) => { + const arr: string[] = []; + + if (!startISO || !endISO) { + return arr; + } + + const start = new Date(`${startISO}T00:00:00`); + const end = new Date(`${endISO}T00:00:00`); + + for ( + let d = new Date(start); + d <= end; + d.setDate(d.getDate() + 1) + ) { + arr.push(fmtDateKey(new Date(d))); + } + + return arr; + }; + + const toDateWhere = ( + fieldName: string, + startISO?: string, + endISO?: string +) => { + + const parts: string[] = []; + + if (startISO) { + parts.push( + `${fieldName} >= DATE '${startISO} 00:00:00'` + ); + } + + if (endISO) { + parts.push( + `${fieldName} <= DATE '${endISO} 23:59:59'` + ); + } + + return parts.join(' AND '); +}; + const safeGetAttr = ( + attr: any, + field: string + ) => { + + if (!attr) { + return null; + } + + return ( + attr[field] ?? + attr[field.toUpperCase()] ?? + attr[field.toLowerCase()] ?? + null + ); + }; + + // ============================================================ + // LOAD ZONES + // ============================================================ + + useEffect(() => { + + const loadZones = async () => { + + setLoadingZones(true); + + try { + + const fl = new FeatureLayer({ + url: pointLayerUrl + }); + + const query = fl.createQuery(); + + query.outFields = ['zone_number']; + query.returnGeometry = false; + query.where = '1=1'; + + const result = await fl.queryFeatures(query); + + const collected: string[] = []; + + result.features.forEach(feature => { + + const value = safeGetAttr( + feature.attributes, + 'zone_number' + ); + + if ( + value !== null && + value !== undefined && + String(value).trim() !== '' + ) { + + collected.push( + String(value).trim() + ); + } + + }); + + const uniqueZones = Array.from( + new Set(collected) + ).sort((a, b) => + a.localeCompare(b, undefined, { + numeric: true + }) + ); + + setZones(uniqueZones); + + } catch (error) { + + console.error( + 'Failed to load zones:', + error + ); + + setMessage( + 'Unable to load zones from Form 7 Point layer.' + ); + + setMessageType('error'); + + } finally { + + setLoadingZones(false); + + } + }; + + loadZones(); + + }, [pointLayerUrl]); + + // ============================================================ + // FETCH SUMMARY + // ============================================================ + + const fetchAndBuildSummary = async () => { + + setMessage(''); + setMessageType(''); + + setSummaryRows([]); + setDateColumns([]); + + // ---------------------------------------------------------- + // VALIDATION + // ---------------------------------------------------------- + + if (!startDate || !endDate) { + + setMessage( + 'Please select both Start Date and End Date.' + ); + + setMessageType('error'); + + return; + } + + if (startDate > endDate) { + + setMessage( + 'Start Date cannot be later than End Date.' + ); + + setMessageType('error'); + + return; + } + + // ---------------------------------------------------------- + // DATE COLUMNS + // ---------------------------------------------------------- + + const dCols = rangeDates( + startDate, + endDate + ); + + if (dCols.length === 0) { + + setMessage( + 'Invalid date range.' + ); + + setMessageType('error'); + + return; + } + + setDateColumns(dCols); + + setLoading(true); + + try { + + const zoneFieldName = 'zone_number'; + const dateFieldName = 'created_date'; + + // -------------------------------------------------------- + // BUILD WHERE CLAUSE + // -------------------------------------------------------- + + const dateWhere = + toDateWhere( + dateFieldName, + startDate, + endDate + ); + + let zoneWhere = ''; + + if (selectedZone) { + + const trimmedZone = + selectedZone.replace(/'/g, "''"); + + zoneWhere = + `${zoneFieldName} = '${trimmedZone}'`; + } + + const whereParts = [ + dateWhere, + zoneWhere + ].filter(Boolean); + + const whereClause = + whereParts.length > 0 + ? whereParts.join(' AND ') + : '1=1'; + + console.log( + 'Form 7 Query:', + whereClause + ); + + // -------------------------------------------------------- + // QUERY LAYER + // -------------------------------------------------------- + + const fl = new FeatureLayer({ + url: pointLayerUrl + }); + + const query = fl.createQuery(); + + query.outFields = [ + zoneFieldName, + dateFieldName + ]; + + query.returnGeometry = false; + query.where = whereClause; + + const result = + await fl.queryFeatures(query); + + // -------------------------------------------------------- + // TRANSFER LIMIT + // -------------------------------------------------------- + + if ((result as any).exceededTransferLimit) { + + console.warn( + 'Query exceeded transfer limit.' + ); + + setMessage( + 'Partial results returned because the service limit was reached. Please narrow the date range or select a zone.' + ); + + setMessageType('error'); + } + + // -------------------------------------------------------- + // GROUP DATA + // -------------------------------------------------------- + + const grouping: { + [zone: string]: { + [date: string]: number; + }; + } = {}; + + result.features.forEach(feature => { + + const attributes = + feature.attributes; + + const rawZone = + safeGetAttr( + attributes, + zoneFieldName + ); + + if ( + rawZone === null || + rawZone === undefined + ) { + return; + } + + const zone = + String(rawZone).trim(); + + if (!zone) { + return; + } + + const createdValue = + safeGetAttr( + attributes, + dateFieldName + ); + + if ( + createdValue === null || + createdValue === undefined + ) { + return; + } + + let dateValue: Date; + + if ( + typeof createdValue === 'number' + ) { + dateValue = + new Date(createdValue); + } else { + dateValue = + new Date(createdValue); + } + + if ( + isNaN(dateValue.getTime()) + ) { + return; + } + + const dateKey = + fmtDateKey(dateValue); + + if (!dCols.includes(dateKey)) { + return; + } + + if (!grouping[zone]) { + grouping[zone] = {}; + } + + grouping[zone][dateKey] = + (grouping[zone][dateKey] || 0) + 1; + + }); + + // -------------------------------------------------------- + // SELECTED ZONE WITH ZERO RESULTS + // -------------------------------------------------------- + + if ( + selectedZone && + !grouping[selectedZone] + ) { + + grouping[selectedZone] = {}; + + } + + // -------------------------------------------------------- + // BUILD TABLE ROWS + // -------------------------------------------------------- + + const rows: SummaryRow[] = + Object.keys(grouping) + .sort((a, b) => + a.localeCompare( + b, + undefined, + { + numeric: true + } + ) + ) + .map(zoneName => { + + const countsByDate: { + [date: string]: number; + } = {}; + + let total = 0; + + dCols.forEach(date => { + + const count = + grouping[zoneName]?.[date] || 0; + + countsByDate[date] = + count; + + total += count; + + }); + + return { + zone: zoneName, + countsByDate, + total + }; + + }); + + setSummaryRows(rows); + + if (rows.length === 0) { + + setMessage( + 'No Form 7 Point records found for the selected criteria.' + ); + + setMessageType('error'); + + } else { + + setMessage( + `${rows.reduce( + (sum, row) => sum + row.total, + 0 + )} records found.` + ); + + setMessageType('success'); + + } + + } catch (error) { + + console.error( + 'Error fetching Form 7 summary:', + error + ); + + setMessage( + 'Error fetching Form 7 Point data. Please check the service or browser console.' + ); + + setMessageType('error'); + + setSummaryRows([]); + setDateColumns([]); + + } finally { + + setLoading(false); + + } + }; + + // ============================================================ + // TOTAL HELPERS + // ============================================================ + + const getGrandTotal = () => { + + return summaryRows.reduce( + (sum, row) => + sum + row.total, + 0 + ); + }; + + const getDateTotal = ( + date: string + ) => { + + return summaryRows.reduce( + (sum, row) => + sum + + (row.countsByDate[date] || 0), + 0 + ); + }; + + // ============================================================ + // EXPORT EXCEL + // ============================================================ + + const exportData = async () => { + + if ( + dateColumns.length === 0 || + summaryRows.length === 0 + ) { + + setMessage( + 'No data available to export.' + ); + + setMessageType('error'); + + return; + } + + const headerDates = + dateColumns.map(displayDate); + + const rowsForExport: any[] = + summaryRows.map(row => { + + const obj: any = { + Zone: row.zone + }; + + dateColumns.forEach( + (date, index) => { + + obj[headerDates[index]] = + row.countsByDate[date] || 0; + + } + ); + + obj.Total = row.total; + + return obj; + + }); + + // ---------------------------------------------------------- + // TOTAL ROW + // ---------------------------------------------------------- + + const totalRow: any = { + Zone: 'TOTAL' + }; + + dateColumns.forEach( + (date, index) => { + + totalRow[headerDates[index]] = + getDateTotal(date); + + } + ); + + totalRow.Total = + getGrandTotal(); + + rowsForExport.push(totalRow); + + const filenameBase = + `form7_point_zone_daily_summary_${startDate}_to_${endDate}`; + + const xlsxFilename = + `${filenameBase}.xlsx`; + + const csvFilename = + `${filenameBase}.csv`; + + try { + + // -------------------------------------------------------- + // XLSX FROM WINDOW + // -------------------------------------------------------- + + if ((window as any).XLSX) { + + const XLSX = + (window as any).XLSX; + + const worksheet = + XLSX.utils.json_to_sheet( + rowsForExport + ); + + const workbook = + XLSX.utils.book_new(); + + XLSX.utils.book_append_sheet( + workbook, + worksheet, + 'Form 7 Summary' + ); + + XLSX.writeFile( + workbook, + xlsxFilename + ); + + return; + } + + // -------------------------------------------------------- + // XLSX PACKAGE + // -------------------------------------------------------- + + const mod = + await import('xlsx').catch( + () => null + ); + + if ( + mod && + mod.utils + ) { + + const worksheet = + mod.utils.json_to_sheet( + rowsForExport + ); + + const workbook = + mod.utils.book_new(); + + mod.utils.book_append_sheet( + workbook, + worksheet, + 'Form 7 Summary' + ); + + mod.writeFile( + workbook, + xlsxFilename + ); + + return; + } + + // -------------------------------------------------------- + // CSV FALLBACK + // -------------------------------------------------------- + + const headers = [ + 'Zone', + ...headerDates, + 'Total' + ]; + + const lines = [ + headers.join(',') + ]; + + rowsForExport.forEach(row => { + + const values: string[] = [ + row.Zone + ]; + + dateColumns.forEach( + (_, index) => { + + values.push( + String( + row[headerDates[index]] ?? 0 + ) + ); + + } + ); + + values.push( + String(row.Total ?? 0) + ); + + const safeValues = + values.map(value => { + + if ( + value.includes(',') + ) { + + return `"${value.replace( + /"/g, + '""' + )}"`; + + } + + return value; + + }); + + lines.push( + safeValues.join(',') + ); + + }); + + const csv = + lines.join('\n'); + + const blob = + new Blob( + [csv], + { + type: + 'text/csv;charset=utf-8;' + } + ); + + const url = + URL.createObjectURL(blob); + + const link = + document.createElement('a'); + + link.href = url; + link.download = csvFilename; + + link.click(); + + URL.revokeObjectURL(url); + + } catch (error) { + + console.error( + 'Export failed:', + error + ); + + setMessage( + 'Export failed. Please check the browser console.' + ); + + setMessageType('error'); + + } + }; + + // ============================================================ + // RESET + // ============================================================ + + const resetFilters = () => { + + setSelectedZone(''); + setStartDate(''); + setEndDate(''); + + setSummaryRows([]); + setDateColumns([]); + + setMessage(''); + setMessageType(''); + }; + + // ============================================================ + // STYLES + // ============================================================ + + const wrapper: React.CSSProperties = { + width: '100%', + height: '100%', + boxSizing: 'border-box', + padding: '18px', + fontFamily: + 'Arial, Helvetica, sans-serif', + background: + '#f5f7fa', + overflow: 'auto' + }; + + const card: React.CSSProperties = { + background: '#ffffff', + borderRadius: '10px', + border: '1px solid #e1e6ed', + boxShadow: + '0 2px 8px rgba(0,0,0,0.06)', + padding: '18px', + marginBottom: '14px' + }; + + const headerTitle: React.CSSProperties = { + margin: 0, + fontSize: '20px', + fontWeight: 700, + color: '#172b4d' + }; + + const headerSubtitle: React.CSSProperties = { + margin: '5px 0 0', + fontSize: '12px', + color: '#6b778c' + }; + + const filterGrid: React.CSSProperties = { + display: 'grid', + gridTemplateColumns: + 'repeat(auto-fit, minmax(180px, 1fr))', + gap: '14px', + alignItems: 'end' + }; + + const fieldWrapper: React.CSSProperties = { + display: 'flex', + flexDirection: 'column' + }; + + const fieldLabel: React.CSSProperties = { + fontSize: '12px', + fontWeight: 600, + color: '#42526e', + marginBottom: '6px' + }; + + const inputStyle: React.CSSProperties = { + width: '100%', + height: '38px', + padding: '0 10px', + boxSizing: 'border-box', + border: + '1px solid #c7d0dc', + borderRadius: '6px', + background: '#ffffff', + color: '#172b4d', + fontSize: '13px', + outline: 'none' + }; + + const primaryButton: React.CSSProperties = { + height: '38px', + padding: '0 18px', + border: 'none', + borderRadius: '6px', + background: '#1976d2', + color: '#ffffff', + fontSize: '13px', + fontWeight: 600, + cursor: 'pointer' + }; + + const secondaryButton: React.CSSProperties = { + height: '38px', + padding: '0 18px', + border: + '1px solid #c7d0dc', + borderRadius: '6px', + background: '#ffffff', + color: '#42526e', + fontSize: '13px', + fontWeight: 600, + cursor: 'pointer' + }; + + const exportButton: React.CSSProperties = { + height: '38px', + padding: '0 18px', + border: 'none', + borderRadius: '6px', + background: '#2e7d32', + color: '#ffffff', + fontSize: '13px', + fontWeight: 600, + cursor: 'pointer' + }; + + const buttonContainer: React.CSSProperties = { + display: 'flex', + gap: '8px', + flexWrap: 'wrap' + }; + + const summaryCard: React.CSSProperties = { + display: 'grid', + gridTemplateColumns: + 'repeat(auto-fit, minmax(150px, 1fr))', + gap: '12px', + marginBottom: '14px' + }; + + const statBox: React.CSSProperties = { + background: '#ffffff', + border: + '1px solid #e1e6ed', + borderRadius: '8px', + padding: '14px 16px' + }; + + const statLabel: React.CSSProperties = { + fontSize: '11px', + color: '#6b778c', + marginBottom: '5px' + }; + + const statValue: React.CSSProperties = { + fontSize: '22px', + fontWeight: 700, + color: '#172b4d' + }; + + const tableContainer: React.CSSProperties = { + background: '#ffffff', + borderRadius: '10px', + border: + '1px solid #e1e6ed', + boxShadow: + '0 2px 8px rgba(0,0,0,0.05)', + overflow: 'auto', + maxHeight: '58vh' + }; + + const table: React.CSSProperties = { + borderCollapse: 'separate', + borderSpacing: 0, + width: '100%', + minWidth: + Math.max( + 650, + dateColumns.length * 105 + 220 + ) + }; + + const headerCell: React.CSSProperties = { + padding: '11px 12px', + borderBottom: + '1px solid #dfe4ea', + background: '#eef4fb', + color: '#344563', + fontSize: '12px', + fontWeight: 700, + textAlign: 'center', + whiteSpace: 'nowrap', + position: 'sticky', + top: 0, + zIndex: 3 + }; + + const zoneHeaderCell: React.CSSProperties = { + ...headerCell, + textAlign: 'left', + left: 0, + zIndex: 5, + minWidth: '120px' + }; + + const totalHeaderCell: React.CSSProperties = { + ...headerCell, + background: '#fff3e0', + minWidth: '100px' + }; + + const zoneCell: React.CSSProperties = { + padding: '10px 12px', + borderBottom: + '1px solid #edf0f3', + background: '#ffffff', + color: '#172b4d', + fontSize: '13px', + fontWeight: 600, + position: 'sticky', + left: 0, + zIndex: 2 + }; + + const dataCell: React.CSSProperties = { + padding: '10px 12px', + borderBottom: + '1px solid #edf0f3', + color: '#42526e', + fontSize: '13px', + textAlign: 'center', + background: '#ffffff' + }; + + const totalCell: React.CSSProperties = { + ...dataCell, + fontWeight: 700, + background: '#fffaf2', + color: '#7a4b00' + }; + + const totalRowZone: React.CSSProperties = { + ...zoneCell, + fontWeight: 800, + background: '#fff3e0', + color: '#7a4b00', + zIndex: 2 + }; + + const totalRowCell: React.CSSProperties = { + ...dataCell, + fontWeight: 800, + background: '#fff3e0', + color: '#7a4b00' + }; + + // ============================================================ + // RENDER + // ============================================================ + + return ( +
+ + {/* ======================================================== + HEADER + ======================================================== */} + +
+ +
+ +
+ +

+ Form 7 Point Summary +

+ +

+ Zone-wise daily record count based on Created Date +

+ +
+ +
+ FORM 7 POINT +
+ +
+ +
+ + {/* ======================================================== + FILTER CARD + ======================================================== */} + +
+ +
+ + {/* Zone */} + +
+ + + + + +
+ + {/* Start Date */} + +
+ + + + + setStartDate( + e.target.value + ) + } + /> + +
+ + {/* End Date */} + +
+ + + + + setEndDate( + e.target.value + ) + } + /> + +
+ + {/* Buttons */} + +
+ + + + + + + +
+ +
+ +
+ + {/* ======================================================== + MESSAGE + ======================================================== */} + + {message && ( + +
+ {message} +
+ + )} + + {/* ======================================================== + SUMMARY STATISTICS + ======================================================== */} + + {summaryRows.length > 0 && ( + +
+ +
+ +
+ TOTAL RECORDS +
+ +
+ {getGrandTotal().toLocaleString()} +
+ +
+ +
+ +
+ ZONES +
+ +
+ {summaryRows.length} +
+ +
+ +
+ +
+ DATE RANGE +
+ +
+ {startDate && + endDate + ? `${displayDate( + startDate + )} - ${displayDate( + endDate + )}` + : '-'} +
+ +
+ +
+ + )} + + {/* ======================================================== + TABLE + ======================================================== */} + +
+ + {loading ? ( + +
+ +
+ ⟳ +
+ + Fetching Form 7 Point records... + +
+ + ) : summaryRows.length === 0 ? ( + +
+ + Select a zone and date range, + then click Submit to + view the summary. + +
+ + ) : ( + + + + + + + + + + {dateColumns.map( + date => ( + + + + ) + )} + + + + + + + + + + {summaryRows.map( + row => ( + + + + + + {dateColumns.map( + date => ( + + + + ) + )} + + + + + + ) + )} + + {/* ================================================= + TOTAL ROW + ================================================= */} + + + + + + {dateColumns.map( + date => ( + + + + ) + )} + + + + + + + +
+ Zone + + {displayDate(date)} + + Total +
+ Zone {row.zone} + + {( + row + .countsByDate[ + date + ] || 0 + ).toLocaleString()} + + {row.total.toLocaleString()} +
+ TOTAL + + {getDateTotal( + date + ).toLocaleString()} + + {getGrandTotal().toLocaleString()} +
+ + )} + +
+ +
+ ); +}; + +export default ZoneWardDateSummary; \ No newline at end of file diff --git a/Form7Buildingdatesummary/src/setting/setting.tsx b/Form7Buildingdatesummary/src/setting/setting.tsx new file mode 100644 index 0000000..1e5fa3d --- /dev/null +++ b/Form7Buildingdatesummary/src/setting/setting.tsx @@ -0,0 +1,20 @@ +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[]) => { + console.log("Selected Map Widget IDs:", useMapWidgetIds); + props.onSettingChange({ + id: props.id, + useMapWidgetIds: useMapWidgetIds + }) + } + + return
+ +
+} + +export default Setting + diff --git a/Form7Buildingdatesummary/src/zoom-in.png b/Form7Buildingdatesummary/src/zoom-in.png new file mode 100644 index 0000000000000000000000000000000000000000..73ac96ef03e9878c978637f6c4ced8cc0286cd5f GIT binary patch literal 23306 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 + + + diff --git a/Form7Generartion_Filter/manifest.json b/Form7Generartion_Filter/manifest.json new file mode 100644 index 0000000..6fdf28d --- /dev/null +++ b/Form7Generartion_Filter/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "Form7Generartion_Filter", + "label": "Form7Generartion_Filter", + "type": "widget", + "version": "1.16.0", + "exbVersion": "1.16.0", + "author": "Esri R&D Center Beijing", + "description": "This is a barebones, simple widget. It includes an example of how to use the config file, and it includes a placeholder SVG icon. It intentionally does not include the following so you can use this to start from scratch: i18n translations, a Settings panel, unit tests. See the documentation for more information on how to implement these features.", + "copyright": "", + "license": "http://www.apache.org/licenses/LICENSE-2.0", + "properties": {}, + "translatedLocales": [ + "en" + ], + "defaultSize": { + "width": 800, + "height": 500 + } +} \ No newline at end of file diff --git a/Form7Generartion_Filter/src/config.ts b/Form7Generartion_Filter/src/config.ts new file mode 100644 index 0000000..780aacf --- /dev/null +++ b/Form7Generartion_Filter/src/config.ts @@ -0,0 +1,7 @@ +import { type ImmutableObject } from 'seamless-immutable' + +export interface Config { + exampleConfigProperty: string +} + +export type IMConfig = ImmutableObject diff --git a/Form7Generartion_Filter/src/runtime/assets/zoom-in.svg b/Form7Generartion_Filter/src/runtime/assets/zoom-in.svg new file mode 100644 index 0000000..bb2d2e5 --- /dev/null +++ b/Form7Generartion_Filter/src/runtime/assets/zoom-in.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Form7Generartion_Filter/src/runtime/components/LoadingPopup.css b/Form7Generartion_Filter/src/runtime/components/LoadingPopup.css new file mode 100644 index 0000000..5d83905 --- /dev/null +++ b/Form7Generartion_Filter/src/runtime/components/LoadingPopup.css @@ -0,0 +1,45 @@ +.loading-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; + } + + .loading-box { + background: #fff; + padding: 24px; + border-radius: 8px; + width: 280px; + text-align: center; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3); + } + + .loading-title { + font-size: 1.2rem; + font-weight: bold; + margin-bottom: 16px; + } + + .spinner { + border: 4px solid #e0e0e0; + border-top: 4px solid #009688; + border-radius: 50%; + width: 32px; + height: 32px; + animation: spin 1s linear infinite; + margin: 0 auto 12px auto; + } + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + + .loading-message { + color: #555; + } + \ No newline at end of file diff --git a/Form7Generartion_Filter/src/runtime/components/LoadingPopup.tsx b/Form7Generartion_Filter/src/runtime/components/LoadingPopup.tsx new file mode 100644 index 0000000..0a2cc49 --- /dev/null +++ b/Form7Generartion_Filter/src/runtime/components/LoadingPopup.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import './LoadingPopup.css'; + +interface LoadingPopupProps { + show: boolean; + message?: string; +} + +const LoadingPopup: React.FC = ({ show, message = "Preparing to download ..." }) => { + if (!show) return null; + + return ( +
+
+

Please Wait..

+
+

{message}

+
+
+ ); +}; + +export default LoadingPopup; diff --git a/Form7Generartion_Filter/src/runtime/components/LoadingSpinner.jsx b/Form7Generartion_Filter/src/runtime/components/LoadingSpinner.jsx new file mode 100644 index 0000000..0a2cc49 --- /dev/null +++ b/Form7Generartion_Filter/src/runtime/components/LoadingSpinner.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import './LoadingPopup.css'; + +interface LoadingPopupProps { + show: boolean; + message?: string; +} + +const LoadingPopup: React.FC = ({ show, message = "Preparing to download ..." }) => { + if (!show) return null; + + return ( +
+
+

Please Wait..

+
+

{message}

+
+
+ ); +}; + +export default LoadingPopup; diff --git a/Form7Generartion_Filter/src/runtime/simple_widget.css b/Form7Generartion_Filter/src/runtime/simple_widget.css new file mode 100644 index 0000000..cae5cd3 --- /dev/null +++ b/Form7Generartion_Filter/src/runtime/simple_widget.css @@ -0,0 +1,857 @@ +.form7-container { + width: 100%; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + + background: #f4f8fc; + + font-family: + "Segoe UI", + Arial, + sans-serif; + + color: #1f2937; + + box-sizing: border-box; + + padding: 16px; +} + + +/* ========================================================= + HEADER +========================================================= */ + +.form7-header { + display: flex; + align-items: center; + gap: 12px; + + background: #ffffff; + + border: 1px solid #dbe7f2; + + border-radius: 12px; + + padding: 14px 16px; + + margin-bottom: 14px; + + box-shadow: + 0 2px 8px rgba(30, 64, 100, 0.06); +} + +.form7-header-icon { + width: 42px; + height: 42px; + + border-radius: 10px; + + display: flex; + align-items: center; + justify-content: center; + + background: #e0f2fe; + + color: #0369a1; + + font-size: 14px; + font-weight: 700; + + border: 1px solid #bae6fd; +} + +.form7-title { + font-size: 18px; + + font-weight: 700; + + color: #123b5d; + + line-height: 1.2; +} + +.form7-subtitle { + margin-top: 4px; + + font-size: 12px; + + color: #6b7f93; +} + + +/* ========================================================= + SUMMARY +========================================================= */ + +.form7-summary { + display: grid; + + grid-template-columns: + repeat(3, 1fr); + + gap: 10px; + + margin-bottom: 14px; +} + +.summary-card { + display: flex; + + align-items: center; + + gap: 10px; + + background: #ffffff; + + border: 1px solid #dbe7f2; + + border-radius: 11px; + + padding: 12px; + + box-shadow: + 0 2px 7px rgba(30, 64, 100, 0.05); +} + +.summary-icon { + width: 34px; + height: 34px; + + flex-shrink: 0; + + display: flex; + align-items: center; + justify-content: center; + + border-radius: 9px; + + background: #e0f2fe; + + color: #0284c7; + + font-size: 14px; + + font-weight: 700; +} + +.summary-label { + font-size: 10px; + + color: #718096; + + margin-bottom: 2px; + + text-transform: uppercase; + + letter-spacing: 0.4px; +} + +.summary-value { + font-size: 20px; + + font-weight: 700; + + color: #164e70; +} + +.summary-small { + font-size: 17px; +} + + +/* ========================================================= + FILTER CARD +========================================================= */ + +.form7-filter-card { + background: #ffffff; + + border: 1px solid #dbe7f2; + + border-radius: 12px; + + padding: 16px; + + margin-bottom: 14px; + + box-shadow: + 0 2px 8px rgba(30, 64, 100, 0.06); +} + +.section-title { + font-size: 14px; + + font-weight: 700; + + color: #174b70; +} + +.section-description { + font-size: 11px; + + color: #718096; + + margin-top: 3px; + + margin-bottom: 14px; +} + +.filter-grid { + display: grid; + + grid-template-columns: + repeat(3, 1fr); + + gap: 12px; +} + +.filter-field { + display: flex; + + flex-direction: column; + + min-width: 0; +} + +.filter-field label { + font-size: 11px; + + font-weight: 600; + + color: #486174; + + margin-bottom: 6px; +} + +.required { + color: #dc2626; + + margin-left: 3px; +} + +.filter-field select, +.filter-field input { + width: 100%; + + height: 36px; + + box-sizing: border-box; + + border: 1px solid #cbd9e6; + + border-radius: 7px; + + background: #ffffff; + + color: #263f52; + + padding: 0 10px; + + font-size: 12px; + + outline: none; + + transition: + border-color 0.15s ease, + box-shadow 0.15s ease; +} + +.filter-field select:hover, +.filter-field input:hover { + border-color: #8dbbd8; +} + +.filter-field select:focus, +.filter-field input:focus { + border-color: #38a3d1; + + box-shadow: + 0 0 0 3px + rgba(56, 163, 209, 0.12); +} + +.filter-field select:disabled, +.filter-field input:disabled { + background: #f3f6f9; + + color: #9aa8b5; + + cursor: not-allowed; +} + +.disabled-input-wrapper { + position: relative; +} + +.disabled-input-wrapper input { + padding-right: 48px; +} + +.tracking-badge { + position: absolute; + + right: 7px; + + top: 50%; + + transform: translateY(-50%); + + background: #e5eef5; + + color: #7890a2; + + border-radius: 5px; + + padding: 3px 5px; + + font-size: 9px; + + font-weight: 600; +} + + +/* ========================================================= + ACTION BUTTONS +========================================================= */ + +.filter-actions { + display: flex; + + justify-content: flex-end; + + gap: 8px; + + margin-top: 16px; + + padding-top: 13px; + + border-top: 1px solid #edf2f7; +} + +.reset-button, +.search-button { + height: 36px; + + border-radius: 7px; + + padding: 0 16px; + + font-size: 12px; + + font-weight: 600; + + cursor: pointer; + + display: flex; + + align-items: center; + + justify-content: center; + + gap: 6px; + + transition: + background 0.15s ease, + border-color 0.15s ease, + transform 0.1s ease; +} + +.reset-button { + background: #ffffff; + + color: #526b7d; + + border: 1px solid #cbd9e6; +} + +.reset-button:hover { + background: #f3f8fc; + + border-color: #9dbbd0; +} + +.search-button { + background: #168ac1; + + color: #ffffff; + + border: 1px solid #168ac1; +} + +.search-button:hover { + background: #0f78a9; +} + +.search-button:disabled { + opacity: 0.55; + + cursor: not-allowed; +} + +.button-spinner { + width: 12px; + height: 12px; + + border: 2px solid + rgba(255, 255, 255, 0.4); + + border-top-color: #ffffff; + + border-radius: 50%; + + animation: + form7-spin 0.7s linear infinite; +} + +@keyframes form7-spin { + to { + transform: rotate(360deg); + } +} + + +/* ========================================================= + ERROR +========================================================= */ + +.error-message { + margin-top: 12px; + + display: flex; + + align-items: center; + + gap: 7px; + + background: #fff5f5; + + border: 1px solid #fecaca; + + color: #b91c1c; + + border-radius: 7px; + + padding: 9px 11px; + + font-size: 11px; +} + +.error-message span { + width: 17px; + height: 17px; + + border-radius: 50%; + + display: flex; + align-items: center; + justify-content: center; + + background: #fee2e2; + + font-weight: 700; +} + + +/* ========================================================= + RESULTS +========================================================= */ + +.form7-results-card { + background: #ffffff; + + border: 1px solid #dbe7f2; + + border-radius: 12px; + + overflow: hidden; + + box-shadow: + 0 2px 8px rgba(30, 64, 100, 0.06); +} + +.results-header { + display: flex; + + align-items: center; + + justify-content: space-between; + + padding: 14px 16px; + + border-bottom: 1px solid #e5edf4; +} + +.results-count { + margin-top: 3px; + + font-size: 10px; + + color: #7b8d9d; +} + +.result-badge { + background: #e0f2fe; + + color: #0369a1; + + border: 1px solid #bae6fd; + + padding: 5px 9px; + + border-radius: 6px; + + font-size: 10px; + + font-weight: 600; +} + + +/* ========================================================= + TABLE +========================================================= */ + +.table-wrapper { + width: 100%; + + overflow-x: auto; +} + +.form7-table { + width: 100%; + + border-collapse: collapse; + + min-width: 850px; +} + +.form7-table th { + background: #f1f7fb; + + color: #49677b; + + font-size: 10px; + + font-weight: 700; + + text-align: left; + + padding: 10px 9px; + + border-bottom: 1px solid #dbe7f2; + + white-space: nowrap; +} + +.form7-table td { + padding: 9px; + + border-bottom: 1px solid #edf2f7; + + font-size: 11px; + + color: #43596a; + + vertical-align: middle; +} + +.form7-table tbody tr { + transition: + background 0.12s ease; +} + +.form7-table tbody tr:hover { + background: #f7fbfe; +} + +.form7-table tbody tr.selected-row { + background: #eaf7fd; +} + +.serial-cell { + width: 35px; + + color: #8a9bab !important; + + text-align: center; +} + +.zone-badge, +.ward-badge { + display: inline-flex; + + align-items: center; + + justify-content: center; + + min-width: 30px; + + padding: 4px 7px; + + border-radius: 5px; + + font-size: 10px; + + font-weight: 600; +} + +.zone-badge { + background: #e0f2fe; + + color: #0369a1; +} + +.ward-badge { + background: #edf6ff; + + color: #2563a6; +} + +.road-cell { + max-width: 170px; + + overflow: hidden; + + text-overflow: ellipsis; + + white-space: nowrap; +} + +.gis-cell { + max-width: 130px; + + overflow: hidden; + + text-overflow: ellipsis; + + white-space: nowrap; + + font-family: + Consolas, + monospace; + + font-size: 10px !important; + + color: #24627f !important; +} + +.name-cell { + max-width: 140px; + + overflow: hidden; + + text-overflow: ellipsis; + + white-space: nowrap; +} + +.status-badge { + display: inline-block; + + max-width: 100px; + + overflow: hidden; + + text-overflow: ellipsis; + + white-space: nowrap; + + background: #f0f7fb; + + color: #39708c; + + border: 1px solid #d7eaf4; + + border-radius: 5px; + + padding: 4px 7px; + + font-size: 9px; + + font-weight: 600; +} + + +/* ========================================================= + ZOOM BUTTON +========================================================= */ + +.zoom-button { + display: inline-flex; + + align-items: center; + + gap: 5px; + + border: 1px solid #b9dceb; + + background: #eff9fd; + + color: #0876a5; + + border-radius: 6px; + + height: 28px; + + padding: 0 8px; + + font-size: 10px; + + font-weight: 600; + + cursor: pointer; +} + +.zoom-button:hover { + background: #dff3fb; + + border-color: #83c7df; +} + +.zoom-button span { + font-size: 13px; +} + + +/* ========================================================= + PAGINATION +========================================================= */ + +.pagination { + display: flex; + + align-items: center; + + justify-content: center; + + gap: 12px; + + padding: 12px; + + border-top: 1px solid #edf2f7; + + color: #6d8090; + + font-size: 10px; +} + +.pagination button { + width: 28px; + height: 28px; + + border-radius: 6px; + + border: 1px solid #cbd9e6; + + background: #ffffff; + + color: #477089; + + cursor: pointer; + + font-size: 16px; +} + +.pagination button:hover:not(:disabled) { + background: #edf8fd; + + border-color: #8fc7df; +} + +.pagination button:disabled { + opacity: 0.4; + + cursor: not-allowed; +} + +.pagination strong { + color: #185579; +} + + +/* ========================================================= + EMPTY STATE +========================================================= */ + +.empty-state { + display: flex; + + flex-direction: column; + + align-items: center; + + justify-content: center; + + min-height: 180px; + + padding: 25px; +} + +.empty-icon { + width: 42px; + height: 42px; + + border-radius: 50%; + + background: #eef7fc; + + border: 1px solid #d6ebf5; + + display: flex; + + align-items: center; + + justify-content: center; + + color: #7db6cf; + + font-size: 22px; + + margin-bottom: 10px; +} + +.empty-title { + font-size: 13px; + + font-weight: 600; + + color: #526b7d; +} + +.empty-description { + margin-top: 5px; + + font-size: 10px; + + color: #8a9aa8; + + text-align: center; +} + + +/* ========================================================= + RESPONSIVE +========================================================= */ + +@media (max-width: 700px) { + + .form7-summary { + grid-template-columns: + 1fr; + } + + .filter-grid { + grid-template-columns: + 1fr; + } + + .filter-actions { + justify-content: stretch; + } + + .reset-button, + .search-button { + flex: 1; + } +} \ No newline at end of file diff --git a/Form7Generartion_Filter/src/runtime/widget.tsx b/Form7Generartion_Filter/src/runtime/widget.tsx new file mode 100644 index 0000000..e9cc45c --- /dev/null +++ b/Form7Generartion_Filter/src/runtime/widget.tsx @@ -0,0 +1,1006 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { AllWidgetProps } from "jimu-core"; +import { JimuMapViewComponent, JimuMapView } from "jimu-arcgis"; + +import FeatureLayer from "@arcgis/core/layers/FeatureLayer"; + +/* ========================================================= + TYPES +========================================================= */ + +interface Form7Record { + objectid: number; + zone: string; + ward: string; + roadName: string; + gisId: string; + status: string; + assesseeName: string; + address: string; +} + +interface Props extends AllWidgetProps {} + +/* ========================================================= + LAYER / FIELD CONFIGURATION +========================================================= */ + +const FORM7_POINT_LAYER_URL = + "https://gisgcc.chennaicorporation.gov.in/server/rest/services/Revenue/Form7_Point/FeatureServer/0"; +const FORM7_POINT_LAYER_NAME = "Form 7 Point"; + +const FORM7_FIELDS = { + objectId: "objectid", + zone: "zone_number", + ward: "ward_number", + road: "road_name", + gisId: "final_gis_id", + status: "status", + assesseeName: "assessee_name", + address: "address", +} as const; + +const PAGE_SIZE = 10; + +/* ========================================================= + PURE HELPERS (no component state needed) +========================================================= */ + +const escapeSql = (value: string): string => value.replace(/'/g, "''"); + +/** Safe string coercion for feature attributes — collapses null/undefined to "". */ +const attrToString = (attrs: Record, field: string): string => { + const value = attrs[field]; + return value === null || value === undefined ? "" : String(value); +}; + +const mapFeatureToRecord = (attrs: Record): Form7Record => ({ + objectid: Number(attrs[FORM7_FIELDS.objectId]), + zone: attrToString(attrs, FORM7_FIELDS.zone), + ward: attrToString(attrs, FORM7_FIELDS.ward), + roadName: attrToString(attrs, FORM7_FIELDS.road), + gisId: attrToString(attrs, FORM7_FIELDS.gisId), + status: attrToString(attrs, FORM7_FIELDS.status), + assesseeName: attrToString(attrs, FORM7_FIELDS.assesseeName), + address: attrToString(attrs, FORM7_FIELDS.address), +}); + +/** Builds a `field = 'value' AND ...` WHERE clause from a list of [field, value] pairs. */ +const buildWhere = (pairs: Array<[string, string]>): string => { + const clauses = pairs + .filter(([, value]) => Boolean(value)) + .map(([field, value]) => `${field} = '${escapeSql(value)}'`); + return clauses.length ? clauses.join(" AND ") : "1=1"; +}; + +/* ========================================================= + COMPONENT +========================================================= */ + +const Form7ApprovalPending = (props: Props) => { + const [jimuMapView, setJimuMapView] = useState(null); + const [form7Layer, setForm7Layer] = useState(null); + + const [selectedZone, setSelectedZone] = useState(""); + const [selectedWard, setSelectedWard] = useState(""); + const [selectedRoadName, setSelectedRoadName] = useState(""); + + // Reserved for Editor Tracking fields — enabled once that capability ships. + const [selectedUser, setSelectedUser] = useState(""); + const [selectedDate, setSelectedDate] = useState(""); + + const [zones, setZones] = useState([]); + const [wards, setWards] = useState([]); + const [roads, setRoads] = useState([]); + + const [tableData, setTableData] = useState([]); + const [pointCount, setPointCount] = useState(0); + + const [currentPage, setCurrentPage] = useState(1); + const [loading, setLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + const [selectedObjectId, setSelectedObjectId] = useState(null); + + /* ======================================================= + MAP VIEW + ======================================================= */ + + const activeViewChangeHandler = useCallback((jmv: JimuMapView) => { + if (jmv) setJimuMapView(jmv); + }, []); + + const getMapForm7Layer = useCallback((): FeatureLayer | null => { + if (!jimuMapView?.view) return null; + const layer = jimuMapView.view.map.layers + .toArray() + .find( + (item: any) => + item.url === FORM7_POINT_LAYER_URL || item.title === FORM7_POINT_LAYER_NAME + ); + return (layer as FeatureLayer) ?? null; + }, [jimuMapView]); + + /* ======================================================= + LOAD FORM 7 POINT LAYER (once) + ======================================================= */ + + useEffect(() => { + const layer = new FeatureLayer({ + url: FORM7_POINT_LAYER_URL, + title: FORM7_POINT_LAYER_NAME, + outFields: ["*"], + }); + + layer + .load() + .then(() => setForm7Layer(layer)) + .catch((error) => { + console.error("Form 7 Point layer loading failed:", error); + setErrorMessage("Unable to load Form 7 Point layer."); + }); + + return () => layer.destroy(); + }, []); + + /* ======================================================= + DISTINCT VALUE LOOKUP (shared by zone/ward/road dropdowns) + ======================================================= */ + + const getDistinctValues = useCallback( + async (layer: FeatureLayer, fieldName: string, where = "1=1"): Promise => { + try { + const query = layer.createQuery(); + query.where = where; + query.outFields = [fieldName]; + query.returnGeometry = false; + query.returnDistinctValues = true; + query.orderByFields = [`${fieldName} ASC`]; + + const result = await layer.queryFeatures(query); + const values = result.features + .map((feature) => attrToString(feature.attributes, fieldName).trim()) + .filter(Boolean); + + return [...new Set(values)]; + } catch (error) { + console.error(`Error loading ${fieldName}:`, error); + return []; + } + }, + [] + ); + + /* ======================================================= + DROPDOWN POPULATION + ======================================================= */ + + useEffect(() => { + if (!form7Layer) return; + + setLoading(true); + getDistinctValues(form7Layer, FORM7_FIELDS.zone) + .then(setZones) + .finally(() => setLoading(false)); + }, [form7Layer, getDistinctValues]); + + useEffect(() => { + setSelectedWard(""); + setSelectedRoadName(""); + + if (!form7Layer || !selectedZone) { + setWards([]); + // Zone cleared: still let roads reflect "no filter" rather than wiping them out. + if (!selectedZone) setRoads([]); + return; + } + + const zoneWhere = buildWhere([[FORM7_FIELDS.zone, selectedZone]]); + getDistinctValues(form7Layer, FORM7_FIELDS.ward, zoneWhere).then(setWards); + getDistinctValues(form7Layer, FORM7_FIELDS.road, zoneWhere).then(setRoads); + }, [selectedZone, form7Layer, getDistinctValues]); + + useEffect(() => { + setSelectedRoadName(""); + + if (!form7Layer) return; + + const where = buildWhere([ + [FORM7_FIELDS.zone, selectedZone], + [FORM7_FIELDS.ward, selectedWard], + ]); + getDistinctValues(form7Layer, FORM7_FIELDS.road, where).then(setRoads); + // Only re-run when ward changes (zone changes are handled by the effect above). + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedWard]); + + /* ======================================================= + WHERE CLAUSE FOR CURRENT FILTER SELECTION + ======================================================= */ + + const currentWhere = useMemo( + () => + buildWhere([ + [FORM7_FIELDS.zone, selectedZone], + [FORM7_FIELDS.ward, selectedWard], + [FORM7_FIELDS.road, selectedRoadName], + ]), + [selectedZone, selectedWard, selectedRoadName] + ); + + /* ======================================================= + MAP FILTER (highlight only matching features on the map) + ======================================================= */ + + const setLayerFilter = useCallback( + async (where: string | null) => { + const mapLayer = getMapForm7Layer(); + if (!jimuMapView?.view || !mapLayer) return; + + try { + const layerView = await jimuMapView.view.whenLayerView(mapLayer); + (layerView as any).filter = where ? { where } : null; + } catch (error) { + console.error("Unable to update Form 7 map filter:", error); + } + }, + [jimuMapView, getMapForm7Layer] + ); + + /* ======================================================= + QUERY FORM 7 FEATURES → TABLE ROWS + ======================================================= */ + + const queryForm7Features = useCallback( + async (where: string): Promise => { + if (!form7Layer) return []; + + const query = form7Layer.createQuery(); + query.where = where; + query.outFields = ["*"]; + query.returnGeometry = true; + query.orderByFields = [`${FORM7_FIELDS.zone} ASC`, `${FORM7_FIELDS.ward} ASC`]; + + const result = await form7Layer.queryFeatures(query); + return result.features.map((feature) => mapFeatureToRecord(feature.attributes)); + }, + [form7Layer] + ); + + // Note: the map is no longer auto-zoomed on Search — only the row-level + // "Zoom" button (handleRowZoom, below) moves the view now. + + /* ======================================================= + SUBMIT + ======================================================= */ + + const handleSubmit = useCallback(async () => { + if (!form7Layer) { + setErrorMessage("Form 7 Point layer is not loaded."); + return; + } + if (!selectedZone) { + setErrorMessage("Please select a zone."); + return; + } + + setErrorMessage(""); + setLoading(true); + setCurrentPage(1); + setSelectedObjectId(null); + + try { + const where = currentWhere; + + const [count, features] = await Promise.all([ + form7Layer.queryFeatureCount({ where }).catch((error) => { + console.error("Feature count error:", error); + return 0; + }), + queryForm7Features(where), + ]); + + setPointCount(count); + setTableData(features); + + // Filter the map to matching features, but don't move the view — + // zooming only happens when the person clicks a row's Zoom button. + await setLayerFilter(where); + } catch (error) { + console.error("Form 7 search error:", error); + setErrorMessage("Unable to retrieve Form 7 Point records."); + setTableData([]); + setPointCount(0); + } finally { + setLoading(false); + } + }, [form7Layer, currentWhere, queryForm7Features, setLayerFilter]); + + /* ======================================================= + RESET + ======================================================= */ + + const handleReset = useCallback(async () => { + setSelectedZone(""); + setSelectedWard(""); + setSelectedRoadName(""); + setSelectedUser(""); + setSelectedDate(""); + setWards([]); + setRoads([]); + setTableData([]); + setPointCount(0); + setCurrentPage(1); + setSelectedObjectId(null); + setErrorMessage(""); + await setLayerFilter(null); + }, [setLayerFilter]); + + /* ======================================================= + ROW ZOOM + HIGHLIGHT + ======================================================= */ + + const handleRowZoom = useCallback( + async (objectid: number) => { + if (!form7Layer || !jimuMapView?.view) return; + + try { + const query = form7Layer.createQuery(); + query.where = `${FORM7_FIELDS.objectId} = ${objectid}`; + query.returnGeometry = true; + query.outFields = ["*"]; + + const result = await form7Layer.queryFeatures(query); + const feature = result.features[0]; + if (!feature) return; + + setSelectedObjectId(objectid); + + const mapLayer = getMapForm7Layer(); + if (mapLayer) { + const layerView = await jimuMapView.view.whenLayerView(mapLayer); + const highlightHandle = (layerView as any).highlight(feature); + setTimeout(() => { + try { + highlightHandle?.remove(); + } catch { + // ignore — handle may already be gone + } + }, 3000); + } + + if (feature.geometry) { + await jimuMapView.view.goTo({ target: feature.geometry, zoom: 19 }, { duration: 800 }); + } + } catch (error) { + console.error("Row zoom error:", error); + } + }, + [form7Layer, jimuMapView, getMapForm7Layer] + ); + + /* ======================================================= + PAGINATION (derived, memoized) + ======================================================= */ + + const totalPages = Math.max(1, Math.ceil(tableData.length / PAGE_SIZE)); + const startIndex = (currentPage - 1) * PAGE_SIZE; + const endIndex = startIndex + PAGE_SIZE; + const paginatedData = useMemo( + () => tableData.slice(startIndex, endIndex), + [tableData, startIndex, endIndex] + ); + + const goToPreviousPage = useCallback( + () => setCurrentPage((page) => Math.max(page - 1, 1)), + [] + ); + const goToNextPage = useCallback( + () => setCurrentPage((page) => Math.min(page + 1, totalPages)), + [totalPages] + ); + + /* ========================================================= + STYLES — memoized so they aren't rebuilt every render. + Only the two `loading`/`selectedZone`-dependent props are + applied via a small override at the JSX call site instead + of rebuilding the whole object. + ========================================================= */ + + const styles = useMemo( + () => + ({ + container: { + width: "100%", + height: "100%", + overflowY: "auto", + overflowX: "hidden", + background: "#f4f7fb", + fontFamily: '"Segoe UI", Arial, sans-serif', + color: "#1f2937", + boxSizing: "border-box", + padding: "16px", + }, + header: { + display: "flex", + flexDirection: "column", + justifyContent: "center", + background: "linear-gradient(135deg, #1565c0 0%, #2e8fe0 60%, #4aa7f0 100%)", + borderRadius: "14px", + padding: "20px 22px", + marginBottom: "16px", + boxShadow: "0 4px 14px rgba(21, 101, 192, 0.28)", + }, + title: { fontSize: "19px", fontWeight: 700, color: "#ffffff", lineHeight: 1.25 }, + subtitle: { marginTop: "4px", fontSize: "12px", color: "rgba(255,255,255,0.85)" }, + summary: { + display: "grid", + gridTemplateColumns: "repeat(3, 1fr)", + gap: "10px", + marginBottom: "14px", + }, + summaryCard: { + display: "flex", + flexDirection: "column", + gap: "4px", + background: "#ffffff", + border: "1px solid #e3e9f0", + borderRadius: "12px", + padding: "14px", + boxShadow: "0 2px 7px rgba(30, 64, 100, 0.05)", + }, + summaryLabel: { + fontSize: "10px", + color: "#8a97a8", + marginBottom: "2px", + textTransform: "uppercase", + letterSpacing: "0.5px", + fontWeight: 600, + }, + summaryValue: { fontSize: "24px", fontWeight: 700, color: "#1f2937" }, + summarySmall: { fontSize: "17px" }, + card: { + background: "#ffffff", + border: "1px solid #e3e9f0", + borderRadius: "12px", + padding: "16px", + marginBottom: "14px", + boxShadow: "0 2px 8px rgba(30, 64, 100, 0.05)", + }, + sectionTitle: { fontSize: "14px", fontWeight: 700, color: "#1f2d3d" }, + description: { + fontSize: "11px", + color: "#8a97a8", + marginTop: "3px", + marginBottom: "14px", + }, + filterGrid: { + display: "grid", + gridTemplateColumns: "repeat(3, 1fr)", + gap: "12px", + }, + field: { display: "flex", flexDirection: "column", minWidth: 0 }, + label: { fontSize: "11px", fontWeight: 600, color: "#5b6b7d", marginBottom: "6px" }, + required: { color: "#dc2626", marginLeft: "3px" }, + select: { + width: "100%", + height: "36px", + boxSizing: "border-box", + border: "1px solid #d7dfe8", + borderRadius: "7px", + background: "#ffffff", + color: "#263f52", + padding: "0 10px", + fontSize: "12px", + outline: "none", + }, + selectDisabled: { background: "#f5f7fa", color: "#a4aebb" }, + disabledInputWrapper: { position: "relative" }, + disabledInput: { + width: "100%", + height: "36px", + boxSizing: "border-box", + border: "1px solid #e1e7ed", + borderRadius: "7px", + background: "#f5f7fa", + color: "#a4aebb", + padding: "0 50px 0 10px", + fontSize: "12px", + outline: "none", + }, + trackingBadge: { + position: "absolute", + right: "7px", + top: "50%", + transform: "translateY(-50%)", + background: "#eef2f6", + color: "#8a97a8", + borderRadius: "5px", + padding: "3px 5px", + fontSize: "9px", + fontWeight: 600, + }, + actions: { + display: "flex", + justifyContent: "flex-end", + gap: "8px", + marginTop: "16px", + paddingTop: "13px", + borderTop: "1px solid #eef2f6", + }, + resetButton: { + height: "36px", + borderRadius: "7px", + padding: "0 16px", + fontSize: "12px", + fontWeight: 600, + cursor: "pointer", + display: "flex", + alignItems: "center", + justifyContent: "center", + gap: "6px", + background: "#ffffff", + color: "#5b6b7d", + border: "1px solid #d7dfe8", + }, + searchButton: { + height: "36px", + borderRadius: "7px", + padding: "0 18px", + fontSize: "12px", + fontWeight: 600, + display: "flex", + alignItems: "center", + justifyContent: "center", + gap: "6px", + background: "linear-gradient(135deg, #1565c0 0%, #2e8fe0 100%)", + color: "#ffffff", + border: "1px solid #1565c0", + }, + error: { + marginTop: "12px", + display: "flex", + alignItems: "center", + gap: "7px", + background: "#fff5f5", + border: "1px solid #fecaca", + color: "#b91c1c", + borderRadius: "7px", + padding: "9px 11px", + fontSize: "11px", + }, + resultsHeader: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "14px 16px", + borderBottom: "1px solid #eef2f6", + }, + resultCount: { marginTop: "3px", fontSize: "10px", color: "#8a97a8" }, + resultBadge: { + background: "#e7f1fd", + color: "#1565c0", + border: "1px solid #bcd9f7", + padding: "5px 9px", + borderRadius: "6px", + fontSize: "10px", + fontWeight: 600, + }, + tableWrapper: { width: "100%", overflowX: "auto" }, + table: { width: "100%", borderCollapse: "collapse", minWidth: "850px" }, + th: { + background: "#eaf2fc", + color: "#3c5a78", + fontSize: "10px", + fontWeight: 700, + textAlign: "left", + padding: "10px 9px", + borderBottom: "1px solid #dbe7f2", + whiteSpace: "nowrap", + }, + td: { + padding: "9px", + borderBottom: "1px solid #eef2f6", + fontSize: "11px", + color: "#43596a", + verticalAlign: "middle", + }, + serial: { width: "35px", color: "#a4aebb", textAlign: "center" }, + plainCellText: { color: "#334155", fontSize: "11px", fontWeight: 600 }, + roadCell: { + maxWidth: "170px", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }, + gisCell: { + maxWidth: "130px", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + fontSize: "11px", + color: "#334155", + }, + nameCell: { + maxWidth: "140px", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }, + statusCell: { + maxWidth: "130px", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + fontSize: "11px", + color: "#334155", + }, + zoomButton: { + display: "inline-flex", + alignItems: "center", + gap: "5px", + border: "1px solid #bcd9f7", + background: "#eef6ff", + color: "#1565c0", + borderRadius: "6px", + height: "28px", + padding: "0 8px", + fontSize: "10px", + fontWeight: 600, + cursor: "pointer", + }, + pagination: { + display: "flex", + alignItems: "center", + justifyContent: "center", + gap: "12px", + padding: "12px", + borderTop: "1px solid #eef2f6", + color: "#8a97a8", + fontSize: "10px", + }, + pageButton: { + width: "28px", + height: "28px", + borderRadius: "6px", + border: "1px solid #d7dfe8", + background: "#ffffff", + color: "#1565c0", + cursor: "pointer", + fontSize: "16px", + }, + empty: { + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + minHeight: "180px", + padding: "25px", + }, + emptyIcon: { + width: "42px", + height: "42px", + borderRadius: "50%", + background: "#eef6ff", + border: "1px solid #d6e9fb", + display: "flex", + alignItems: "center", + justifyContent: "center", + color: "#7db6e8", + fontSize: "22px", + marginBottom: "10px", + }, + emptyTitle: { fontSize: "13px", fontWeight: 600, color: "#5b6b7d" }, + emptyDescription: { + marginTop: "5px", + fontSize: "10px", + color: "#a4aebb", + textAlign: "center", + }, + }) satisfies { [key: string]: React.CSSProperties }, + [] + ); + + const searchDisabled = loading || !selectedZone; + + /* ========================================================= + JSX + ========================================================= */ + + return ( +
+ {props.useMapWidgetIds?.length === 1 && ( + + )} + + {/* HEADER */} +
+
Form 7 Point Search
+
Search and locate Form 7 Point records
+
+ + {/* SUMMARY CARDS */} +
+
+
Total Points
+
{pointCount}
+
+ +
+
Selected Zone
+
+ {selectedZone || "All"} +
+
+ +
+
Selected Ward
+
+ {selectedWard || "All"} +
+
+
+ + {/* FILTER CARD */} +
+
Search Filters
+
+ Select zone, ward and road to filter Form 7 Point records. +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + Soon +
+
+ +
+ +
+ + Soon +
+
+
+ +
+ + + +
+ + {errorMessage && ( +
+ ! + {errorMessage} +
+ )} +
+ + {/* RESULTS CARD */} +
+
+
+
Form 7 Point Records
+
+ {tableData.length > 0 + ? `Showing ${startIndex + 1} - ${Math.min(endIndex, tableData.length)} of ${ + tableData.length + }` + : "No records found"} +
+
+ + {tableData.length > 0 && ( +
{tableData.length} Records
+ )} +
+ + {tableData.length > 0 ? ( + <> +
+ + + + + + + + + + + + + + + + {paginatedData.map((record, index) => ( + + + + + + + + + + + + + + + + + + ))} + +
#ZoneWardRoad NameGIS IDAssessee NameStatusAction
+ {startIndex + index + 1} + + {record.zone || "-"} + + {record.ward || "-"} + + {record.roadName || "-"} + + {record.gisId || "-"} + + {record.assesseeName || "-"} + + {record.status ? ( + {record.status} + ) : ( + "-" + )} + + +
+
+ + {totalPages > 1 && ( +
+ + + + Page {currentPage} of {totalPages} + + + +
+ )} + + ) : ( +
+
◌
+
No Form 7 Point Records
+
+ Select a zone and click Search to retrieve Form 7 Point records. +
+
+ )} +
+
+ ); +}; + +export default Form7ApprovalPending; \ No newline at end of file diff --git a/Form7Generartion_Filter/src/setting/setting.tsx b/Form7Generartion_Filter/src/setting/setting.tsx new file mode 100644 index 0000000..1e5fa3d --- /dev/null +++ b/Form7Generartion_Filter/src/setting/setting.tsx @@ -0,0 +1,20 @@ +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[]) => { + console.log("Selected Map Widget IDs:", useMapWidgetIds); + props.onSettingChange({ + id: props.id, + useMapWidgetIds: useMapWidgetIds + }) + } + + return
+ +
+} + +export default Setting + diff --git a/Form7Generartion_Filter/src/zoom-in.png b/Form7Generartion_Filter/src/zoom-in.png new file mode 100644 index 0000000000000000000000000000000000000000..73ac96ef03e9878c978637f6c4ced8cc0286cd5f GIT binary patch literal 23306 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 { + it('simple test', () => { + const Widget = wrapWidget(_Widget, { + config: { exampleConfigProperty: 'a' } + }) + const { queryByText } = render() + expect(queryByText('exampleConfigProperty: a').tagName).toBe('P') + }) +}) diff --git a/Form7Query_Approval/config.json b/Form7Query_Approval/config.json new file mode 100644 index 0000000..0ce5fd8 --- /dev/null +++ b/Form7Query_Approval/config.json @@ -0,0 +1,5 @@ +{ + "exampleConfigProperty": "test", + "useMapWidget": true + +} diff --git a/Form7Query_Approval/icon.svg b/Form7Query_Approval/icon.svg new file mode 100644 index 0000000..d2c9ea5 --- /dev/null +++ b/Form7Query_Approval/icon.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Form7Query_Approval/manifest.json b/Form7Query_Approval/manifest.json new file mode 100644 index 0000000..edf7f9e --- /dev/null +++ b/Form7Query_Approval/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "Form7Query_Approval", + "label": "Form7Query_Approval", + "type": "widget", + "version": "1.16.0", + "exbVersion": "1.16.0", + "author": "Esri R&D Center Beijing", + "description": "This is a barebones, simple widget. It includes an example of how to use the config file, and it includes a placeholder SVG icon. It intentionally does not include the following so you can use this to start from scratch: i18n translations, a Settings panel, unit tests. See the documentation for more information on how to implement these features.", + "copyright": "", + "license": "http://www.apache.org/licenses/LICENSE-2.0", + "properties": {}, + "translatedLocales": [ + "en" + ], + "defaultSize": { + "width": 800, + "height": 500 + } +} \ No newline at end of file diff --git a/Form7Query_Approval/src/config.ts b/Form7Query_Approval/src/config.ts new file mode 100644 index 0000000..780aacf --- /dev/null +++ b/Form7Query_Approval/src/config.ts @@ -0,0 +1,7 @@ +import { type ImmutableObject } from 'seamless-immutable' + +export interface Config { + exampleConfigProperty: string +} + +export type IMConfig = ImmutableObject diff --git a/Form7Query_Approval/src/runtime/assets/zoom-in.svg b/Form7Query_Approval/src/runtime/assets/zoom-in.svg new file mode 100644 index 0000000..bb2d2e5 --- /dev/null +++ b/Form7Query_Approval/src/runtime/assets/zoom-in.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Form7Query_Approval/src/runtime/components/LoadingPopup.css b/Form7Query_Approval/src/runtime/components/LoadingPopup.css new file mode 100644 index 0000000..5d83905 --- /dev/null +++ b/Form7Query_Approval/src/runtime/components/LoadingPopup.css @@ -0,0 +1,45 @@ +.loading-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; + } + + .loading-box { + background: #fff; + padding: 24px; + border-radius: 8px; + width: 280px; + text-align: center; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3); + } + + .loading-title { + font-size: 1.2rem; + font-weight: bold; + margin-bottom: 16px; + } + + .spinner { + border: 4px solid #e0e0e0; + border-top: 4px solid #009688; + border-radius: 50%; + width: 32px; + height: 32px; + animation: spin 1s linear infinite; + margin: 0 auto 12px auto; + } + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + + .loading-message { + color: #555; + } + \ No newline at end of file diff --git a/Form7Query_Approval/src/runtime/components/LoadingPopup.tsx b/Form7Query_Approval/src/runtime/components/LoadingPopup.tsx new file mode 100644 index 0000000..0a2cc49 --- /dev/null +++ b/Form7Query_Approval/src/runtime/components/LoadingPopup.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import './LoadingPopup.css'; + +interface LoadingPopupProps { + show: boolean; + message?: string; +} + +const LoadingPopup: React.FC = ({ show, message = "Preparing to download ..." }) => { + if (!show) return null; + + return ( +
+
+

Please Wait..

+
+

{message}

+
+
+ ); +}; + +export default LoadingPopup; diff --git a/Form7Query_Approval/src/runtime/components/LoadingSpinner.jsx b/Form7Query_Approval/src/runtime/components/LoadingSpinner.jsx new file mode 100644 index 0000000..0a2cc49 --- /dev/null +++ b/Form7Query_Approval/src/runtime/components/LoadingSpinner.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import './LoadingPopup.css'; + +interface LoadingPopupProps { + show: boolean; + message?: string; +} + +const LoadingPopup: React.FC = ({ show, message = "Preparing to download ..." }) => { + if (!show) return null; + + return ( +
+
+

Please Wait..

+
+

{message}

+
+
+ ); +}; + +export default LoadingPopup; diff --git a/Form7Query_Approval/src/runtime/simple_widget.css b/Form7Query_Approval/src/runtime/simple_widget.css new file mode 100644 index 0000000..703e43b --- /dev/null +++ b/Form7Query_Approval/src/runtime/simple_widget.css @@ -0,0 +1,155 @@ +.property-widget-parent { + /* Enables scroll for the whole widget */ + padding: 8px; + overflow-x: hidden; + max-width: 100%; +} + +.property-widget { + font-family: Arial, sans-serif; + background-color: #f9f9f9; + border-radius: 8px; + padding: 16px; + min-width: 350px; +} + + +.widget-title { + font-size: 20px; + margin-bottom: 16px; + color: #1a1a1a; + font-weight: 600; + border-bottom: 1px solid #ddd; + padding-bottom: 4px; +} + +.property_form { + display: grid; + grid-template-columns: 1fr 2fr; + align-items: center; + gap: 8px; + margin-bottom: 10px; + overflow-x: auto; + overflow-y: auto; + max-height: 300px; /* Restrict form height */ +} + + +.property_form label { + margin-bottom: 4px; + font-weight: 500; + color: #333; +} + +.property_form select { + padding: 6px 10px; + border-radius: 6px; + border: 1px solid #ccc; + font-size: 14px; + background-color: #fdfdfd; + transition: border-color 0.2s ease; +} + +.property_form select:focus { + outline: none; + border-color: #0078d4; +} + +#countDiv { + margin-bottom: 20px; + background: #eef6fd; + padding: 12px; + border-radius: 6px; + border: 1px solid #d0e4f3; +} + +#countDiv p { + margin: 4px 0; + font-size: 14px; + color: #333; +} + +button.property_form { + padding: 10px; + border-radius: 6px; + cursor: pointer; + font-weight: 600; + border: none; + font-size: 14px; + transition: background 0.2s ease; +} + +button.property_form:hover { + opacity: 0.9; +} + +.attribute-table { + margin-top: 20px; + background: #fff; + border: 1px solid #ccc; + border-radius: 8px; + padding: 10px; + overflow: auto; /* Enables both vertical and horizontal scroll */ + max-height: 300px; /* Restrict vertical space */ + max-width: 100%; /* Ensure it doesn't overflow container */ +} + +.attribute-table table { + width: max-content; /* Enables horizontal scrolling */ + min-width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.attribute-table thead th { + position: sticky; + top: 0; + background-color: #f0f0f0; + z-index: 2; +} + + +.attribute-table th { + background: #f3f3f3; + font-weight: 600; +} + +.attribute-table tr:hover { + background-color: #f9f9f9; +} + +button { + font-family: inherit; +} + +.submit-button-fixed { + position: fixed; + bottom: 20px; + right: 20px; + background-color: #0078d4; + color: white; + padding: 10px 20px; + border: none; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + cursor: pointer; + z-index: 1000; +} + +.submit-button-fixed:hover { + background-color: #005fa3; +} +.property-widget { + position: relative; +} + +.submit-button-fixed { + position: absolute; + bottom: 10px; + right: 10px; +} +.widget-container, .esri-widget, .property_form { + max-width: 100%; + box-sizing: border-box; + overflow-x: hidden; +} diff --git a/Form7Query_Approval/src/runtime/widget.tsx b/Form7Query_Approval/src/runtime/widget.tsx new file mode 100644 index 0000000..16dd742 --- /dev/null +++ b/Form7Query_Approval/src/runtime/widget.tsx @@ -0,0 +1,820 @@ +import React, { useState, useEffect } from "react"; +import FeatureLayer from "@arcgis/core/layers/FeatureLayer"; +import { type AllWidgetProps } from 'jimu-core'; +//import "./simple_widget.css"; +import { JimuMapViewComponent, type JimuMapView } from 'jimu-arcgis'; +//import { mapViewManager } from '../../../utils/mapview'; +import zoomInIcon from './assets/zoom-in.svg'; + +const RoadAssetFilter = (props: AllWidgetProps) => { + // State variables (building-only, no road/gis) + const [selectedZone, setSelectedZone] = useState("All"); + const [selectedWard, setSelectedWard] = useState("All"); + const [zoneOptions, setZoneOptions] = useState([]); + const [wardOptions, setWardOptions] = useState([]); + const [jimuMapView, setJimuMapView] = useState(); + const [highlightHandle, setHighlightHandle] = useState(null); + const [selectedUser, setSelectedUser] = useState("All"); + const [selectedDate, setSelectedDate] = useState("All"); // ISO yyyy-mm-dd + const [selectedApproval, setSelectedApproval] = useState("All"); + const [userOptions, setUserOptions] = useState([]); + const [dateOptions, setDateOptions] = useState([]); + const [approvalOptions, setApprovalOptions] = useState([]); + const [buildingCount, setBuildingCount] = useState(null); + const [tableData, setTableData] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage] = useState(10); + const [selectedFeatureId, setSelectedFeatureId] = useState(null); + + // URLs for layers (only Building) + const ZONE_LAYER_URL = "https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/GCC_Administration_Boundaries/FeatureServer/3"; + const WARD_LAYER_URL = "https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/GCC_Administration_Boundaries/FeatureServer/4"; + const BUILDING_LAYER_URL = "https://gisgcc.chennaicorporation.gov.in/server/rest/services/Revenue/Building_Updation/FeatureServer/0"; + + // Layer name (used to locate map layer) + const BUILDING_LAYER_NAME = "Form 6 Building"; + + // Field mapping (Building only) + const FIELD_MAPPING = { + Building: { + zone: "zone", + ward: "ward", + user: "created_user", + date: "created_date", + approval: "approval" + } + }; + + // Utility to escape single quotes in SQL + const safe = (val: string) => val.replace(/'/g, "''"); + + // ------------------------------------------------------------------------ + // DATE HELPER – using TIMESTAMP literals (proven in summary widget) + // ------------------------------------------------------------------------ + const toTimestampWhere = (fieldName: string, startISO: string, endISO: string) => { + return `${fieldName} >= TIMESTAMP '${startISO} 00:00:00' AND ${fieldName} <= TIMESTAMP '${endISO} 23:59:59'`; + }; + + // Format a value (ms or ISO/string) for display in table (locale) + const formatDate = (dateValue: any) => { + if (!dateValue) return '-'; + if (typeof dateValue === 'number') { + return new Date(dateValue).toLocaleString(); + } + if (typeof dateValue === 'string' && dateValue.includes('T')) { + return new Date(dateValue).toLocaleString(); + } + if (typeof dateValue === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(dateValue)) { + return new Date(`${dateValue} GMT+0530`).toLocaleString(); + } + return dateValue; + }; + + // Convert a feature attribute date (number or string) into ISO yyyy-mm-dd for dropdown + const formatAttrDateToISO = (val: any): string | null => { + if (val == null) return null; + let d: Date; + if (typeof val === 'number') { + d = new Date(val); + } else { + d = new Date(val); + } + if (isNaN(d.getTime())) return null; + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, '0'); + const dd = String(d.getDate()).padStart(2, '0'); + return `${yyyy}-${mm}-${dd}`; + }; + + // Apply ward definition expression on building layer + const applyWardDefinition = async (wardValue: string) => { + if (!jimuMapView?.view || !wardValue || wardValue === 'All') return; + const view = jimuMapView.view; + const buildingLayer = view.map.layers.find(l => l.title === BUILDING_LAYER_NAME) as FeatureLayer; + if (buildingLayer) { + buildingLayer.definitionExpression = `${FIELD_MAPPING.Building.ward} = '${safe(wardValue)}'`; + await view.whenLayerView(buildingLayer); + } + }; + + const clearWardDefinition = async () => { + if (!jimuMapView?.view) return; + const map = jimuMapView.view.map; + const layer = map.layers.find(l => l.title === BUILDING_LAYER_NAME) as FeatureLayer; + if (layer) { + layer.definitionExpression = ''; + } + }; + + // Zoom to Zone geometry + const zoomToZoneGeometry = async (zoneValue: string) => { + if (!jimuMapView || !zoneValue || zoneValue === 'All') return; + const view = jimuMapView.view; + const zoneLayer = new FeatureLayer({ url: ZONE_LAYER_URL }); + try { + await zoneLayer.load(); + const zoneField = zoneLayer.fields.find(f => f.name === "zone"); + let whereClause: string; + if (zoneField && (zoneField.type === "integer" || zoneField.type === "double")) { + const num = Number(zoneValue); + if (isNaN(num)) return; + whereClause = `zone = ${num}`; + } else { + whereClause = `zone = '${safe(zoneValue)}'`; + } + const query = zoneLayer.createQuery(); + query.where = whereClause; + query.outFields = ["zone"]; + query.returnGeometry = true; + const result = await zoneLayer.queryFeatures(query); + if (result.features.length > 0) { + const geometry = result.features[0].geometry; + await view.goTo({ + target: geometry, + padding: { top: 50, bottom: 50, left: 50, right: 50 }, + duration: 1000 + }); + } + } catch (error) { + console.error("Error zooming to zone geometry:", error); + } + }; + + // Zoom to Ward geometry + const zoomToWardGeometry = async (wardValue: string) => { + if (!jimuMapView || !wardValue || wardValue === 'All') return; + const view = jimuMapView.view; + const wardLayer = new FeatureLayer({ url: WARD_LAYER_URL }); + try { + await wardLayer.load(); + const wardField = wardLayer.fields.find(f => f.name === "ward_no"); + let whereClause: string; + if (wardField && (wardField.type === "integer" || wardField.type === "double")) { + const num = Number(wardValue); + if (isNaN(num)) return; + whereClause = `ward_no = ${num}`; + } else { + whereClause = `ward_no = '${safe(wardValue)}'`; + } + const query = wardLayer.createQuery(); + query.where = whereClause; + query.outFields = ["ward_no"]; + query.returnGeometry = true; + const result = await wardLayer.queryFeatures(query); + if (result.features.length > 0) { + const geometry = result.features[0].geometry; + sessionStorage.setItem("wardgeometry", JSON.stringify(geometry.toJSON())); + await view.goTo({ + target: geometry, + padding: { top: 50, bottom: 50, left: 50, right: 50 }, + duration: 1000 + }); + } + } catch (error) { + console.error("Error zooming to ward geometry:", error); + } + }; + + // Zoom to filtered features (building-only) + const zoomToFilteredFeature = async () => { + if (!jimuMapView) return; + + const view = jimuMapView.view; + const layerUrl = BUILDING_LAYER_URL; + const whereClause = buildWhereClause(); + + const layer = new FeatureLayer({ url: layerUrl }); + try { + await layer.load(); + + const query = layer.createQuery(); + try { + query.where = whereClause; + query.returnGeometry = true; + query.outFields = ["*"]; + } catch (error) { + console.error("Invalid where clause in zoomToFilteredFeature:", whereClause, error); + return; + } + + const result = await layer.queryFeatures(query); + + if (result.features.length === 1) { + const feature = result.features[0]; + await handleRowZoom(feature); + } else if (result.features.length > 1) { + const extentResult = await layer.queryExtent(query); + if (extentResult.extent) { + await view.goTo({ target: extentResult.extent.expand(1.2), duration: 1000 }); + } + } + } catch (err) { + console.error("Error zooming to filtered feature:", err); + } + }; + + useEffect(() => { + const shouldZoom = + (selectedUser && selectedUser !== 'All') || + (selectedDate && selectedDate !== 'All') || + (selectedApproval && selectedApproval !== 'All'); + + if (shouldZoom) { + zoomToFilteredFeature(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedUser, selectedDate, selectedApproval]); + + // Populate Zone dropdown from Building layer distinct zone values + const populateZone = async () => { + try { + const layer = new FeatureLayer({ url: BUILDING_LAYER_URL }); + const query = layer.createQuery(); + query.returnDistinctValues = true; + query.returnGeometry = false; + query.outFields = [FIELD_MAPPING.Building.zone]; + query.where = "1=1"; + const result = await layer.queryFeatures(query); + if (result) { + const zones = Array.from(new Set( + result.features.map(f => f.attributes[FIELD_MAPPING.Building.zone]).filter(Boolean) + )); + zones.sort((a, b) => { + const na = Number(a), nb = Number(b); + if (!isNaN(na) && !isNaN(nb)) return na - nb; + return String(a).localeCompare(String(b)); + }); + setZoneOptions(['All', ...zones]); + } + } catch (error) { + console.error("Error fetching zones:", error); + } + }; + + useEffect(() => { + populateZone(); + }, []); + + // Populate Ward dropdown based on selected Zone (from Building layer) + const populateWard = async () => { + try { + const layer = new FeatureLayer({ url: BUILDING_LAYER_URL }); + const query = layer.createQuery(); + query.returnDistinctValues = true; + query.returnGeometry = false; + query.outFields = [FIELD_MAPPING.Building.ward]; + if (selectedZone && selectedZone !== 'All') { + query.where = `${FIELD_MAPPING.Building.zone} = '${safe(selectedZone)}'`; + } else { + query.where = "1=1"; + } + const result = await layer.queryFeatures(query); + if (result) { + const wards = Array.from(new Set( + result.features.map(f => f.attributes[FIELD_MAPPING.Building.ward]).filter(Boolean) + )); + wards.sort((a, b) => { + const na = Number(a), nb = Number(b); + if (!isNaN(na) && !isNaN(nb)) return na - nb; + return String(a).localeCompare(String(b)); + }); + setWardOptions(['All', ...wards]); + } + } catch (error) { + console.error("Error fetching wards:", error); + } + }; + + useEffect(() => { + populateWard(); + }, [selectedZone]); + + // Fetch filter options based on building layer (user, date, approval) + const fetchFilterOptions = async () => { + if (!selectedZone) return; + + const layerUrl = BUILDING_LAYER_URL; + const mapping = FIELD_MAPPING.Building; + + try { + const layer = new FeatureLayer({ url: layerUrl }); + const baseQuery = layer.createQuery(); + baseQuery.returnDistinctValues = true; + baseQuery.returnGeometry = false; + + // Zone and ward filtering + let where = "1=1"; + if (selectedZone && selectedZone !== 'All') where = `${mapping.zone} = '${safe(selectedZone)}'`; + if (selectedWard && selectedWard !== 'All') where += ` AND ${mapping.ward} = '${safe(selectedWard)}'`; + + // Approval filter (if already selected) should further narrow options + if (selectedApproval && selectedApproval !== 'All') { + where += ` AND ${mapping.approval} = '${safe(selectedApproval)}'`; + } + + // Fetch Created Users + const userQuery = layer.createQuery(); + userQuery.returnDistinctValues = true; + userQuery.returnGeometry = false; + userQuery.where = where; + userQuery.outFields = [mapping.user]; + const userResult = await layer.queryFeatures(userQuery); + const users = Array.from(new Set( + userResult.features.map(f => f.attributes[mapping.user]).filter(v => v != null) + )); + users.sort(); + setUserOptions(['All', ...users]); + + // Fetch Created Dates -> store as ISO (yyyy-mm-dd) + const dateQuery = layer.createQuery(); + dateQuery.returnDistinctValues = true; + dateQuery.returnGeometry = false; + dateQuery.where = where; + dateQuery.outFields = [mapping.date]; + const dateResult = await layer.queryFeatures(dateQuery); + const rawDates = dateResult.features + .map(f => formatAttrDateToISO(f.attributes[mapping.date])) + .filter(v => v != null) as string[]; + const uniqueDates = Array.from(new Set(rawDates)); + uniqueDates.sort(); + setDateOptions(['All', ...uniqueDates]); + + // Fetch Approval (distinct values). If none, fallback to coded values array. + try { + const approvalQuery = layer.createQuery(); + approvalQuery.returnDistinctValues = true; + approvalQuery.returnGeometry = false; + approvalQuery.where = where; + approvalQuery.outFields = [mapping.approval]; + const approvalResult = await layer.queryFeatures(approvalQuery); + let approvals = Array.from(new Set( + approvalResult.features.map(f => f.attributes[mapping.approval]).filter(v => v != null) + )); + approvals.sort(); + if (approvals.length === 0) { + approvals = ['Approved', 'Pending']; // fallback to coded values + } + setApprovalOptions(['All', ...approvals]); + } catch (err) { + setApprovalOptions(['All', 'Approved', 'Pending']); + } + } catch (error) { + console.error("Error fetching filter options:", error); + } + }; + + useEffect(() => { + fetchFilterOptions(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedZone, selectedWard, selectedApproval]); + + // ------------------------------------------------------------------------ + // BUILD WHERE CLAUSE – FIXED DATE USING TIMESTAMP LITERALS + // ------------------------------------------------------------------------ + const buildWhereClause = () => { + const clauses: string[] = []; + const mapping = FIELD_MAPPING.Building; + + if (selectedZone && selectedZone !== 'All') { + clauses.push(`${mapping.zone}='${safe(selectedZone)}'`); + } + + if (selectedWard && selectedWard !== 'All') { + clauses.push(`${mapping.ward}='${safe(selectedWard)}'`); + } + + if (selectedUser && selectedUser !== 'All') { + clauses.push(`${mapping.user}='${safe(selectedUser)}'`); + } + + if (selectedApproval && selectedApproval !== 'All') { + clauses.push(`${mapping.approval}='${safe(selectedApproval)}'`); + } + + // Date range – using TIMESTAMP literals (works for esriFieldTypeDate) + if (selectedDate && selectedDate !== 'All') { + const dateWhere = toTimestampWhere(mapping.date, selectedDate, selectedDate); + clauses.push(dateWhere); + } + + return clauses.length > 0 ? clauses.join(" AND ") : "1=1"; + }; + + // Count helper for building layer + const getFeatureCount = async (layerUrl: string, whereClause: string): Promise => { + const layer = new FeatureLayer({ url: layerUrl }); + const query = layer.createQuery(); + query.returnGeometry = false; + + try { + query.where = whereClause; + } catch (error) { + console.error("Invalid where clause in count query:", whereClause, error); + return 0; + } + + try { + return await layer.queryFeatureCount(query); + } catch (error) { + console.error("Error counting features:", error); + return 0; + } + }; + + // Fetch counts whenever filters change (only building) + useEffect(() => { + const fetchCounts = async () => { + try { + const buildingWhere = buildWhereClause(); + const bc = await getFeatureCount(BUILDING_LAYER_URL, buildingWhere); + setBuildingCount(bc); + } catch (err) { + console.error(err); + setBuildingCount(null); + } + }; + + fetchCounts(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedZone, selectedWard, selectedUser, selectedDate, selectedApproval]); + + // Row Zoom: zoom to a single feature, highlight it (building) + const handleRowZoom = async (feature: any) => { + const view = jimuMapView?.view; + if (!view || !feature) return; + + if (highlightHandle) { + highlightHandle.remove(); + setHighlightHandle(null); + } + + try { + const layerForHighlight = view.map.layers.find(l => l.url === BUILDING_LAYER_URL); + if (layerForHighlight) { + await layerForHighlight.load(); + const layerView = await view.whenLayerView(layerForHighlight as any); + const highlight = layerView.highlight(feature); + setHighlightHandle(highlight); + } + + setSelectedFeatureId(feature.attributes.OBJECTID); + + const geom = feature.geometry; + if (geom) { + await view.goTo({ target: geom, zoom: 18, duration: 1000 }); + } + } catch (err) { + console.error("Error zooming to feature:", err); + } + }; + + // Submit: apply filters, populate table + const handleSubmit = async (e: React.MouseEvent) => { + e.preventDefault(); + if (!selectedZone) { + alert("Please select a Zone"); + return; + } + const view = jimuMapView?.view; + if (!view) return; + + // Zoom to Ward extent if selected + if (selectedWard && selectedWard !== 'All') { + await zoomToWardGeometry(selectedWard); + } + + // Build whereClause + const whereClause = buildWhereClause(); + console.log("Built WHERE clause:", whereClause); + + // Query features for table (building layer) + const layerUrl = BUILDING_LAYER_URL; + const queryLayer = new FeatureLayer({ url: layerUrl }); + const query = queryLayer.createQuery(); + query.where = whereClause; + query.outFields = ["*"]; + query.returnGeometry = true; + try { + const results = await queryLayer.queryFeatures(query); + setTableData(results.features); + setCurrentPage(1); + + if (results.features.length === 0) { + alert(`No features found for the selected filters in Building layer.`); + } + + // Apply filter to building layer in the active map + const viewMap = view.map; + const buildingLayer = viewMap.layers.find(l => l.title === BUILDING_LAYER_NAME) as FeatureLayer; + if (buildingLayer) { + try { + await buildingLayer.load(); + const lv = await view.whenLayerView(buildingLayer); + if (lv) { + lv.filter = { where: whereClause }; + } + } catch (err) { + console.error("Error applying filter to building layer:", buildingLayer.title, err); + } + } + + // Zoom to filtered extent if needed + const shouldZoomToFeatures = + !selectedWard || selectedWard === 'All'; + + if (shouldZoomToFeatures) { + const fl = new FeatureLayer({ url: layerUrl }); + await fl.load(); + const extentResult = await fl.queryExtent({ where: whereClause }); + if (extentResult.extent) { + await view.goTo({ + target: extentResult.extent.expand(1.2), + duration: 1000 + }); + } + } + } catch (error) { + console.error("Query failed:", error); + alert("An error occurred while querying features."); + } + }; + + // Reset: clear all selections (building-only) + const handleReset = () => { + setSelectedZone("All"); + setSelectedWard("All"); + setSelectedUser("All"); + setSelectedDate("All"); + setSelectedApproval("All"); + setUserOptions([]); + setDateOptions([]); + setApprovalOptions([]); + setBuildingCount(null); + setTableData([]); + setCurrentPage(1); + setSelectedFeatureId(null); + + const view = jimuMapView?.view; + if (highlightHandle) { + highlightHandle.remove(); + setHighlightHandle(null); + } + view?.graphics.removeAll(); + + if (view) { + const clearFilter = async (layerTitle: string) => { + const layer = view.map.layers.find(l => l.title === layerTitle) as FeatureLayer; + if (layer) { + try { + await layer.load(); + const lv = await view.whenLayerView(layer); + if (lv) lv.filter = null; + } catch (err) { + console.error("Error clearing filter on layer:", layerTitle, err); + } + } + }; + + clearFilter(BUILDING_LAYER_NAME); + } + + clearWardDefinition(); + }; + + // Active view change handler + const activeViewChangeHandler = (jmv: JimuMapView) => { + if (jmv && jmv.view) { + setJimuMapView(jmv); + } + }; + + useEffect(() => { + const interval = setInterval(() => { + const view = mapViewManager.getMapView(); + if (view && view.view) { + setJimuMapView(view); + clearInterval(interval); + } + }, 500); + return () => clearInterval(interval); + }, []); + + // Pagination for table + const paginatedData = tableData.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage + ); + + return ( +
+
+ +

Building Search

+ +
+ Count Summary: +

Buildings: {buildingCount !== null ? buildingCount : '-'}

+
+ + {/* Zone selection */} +
+ + +
+ + {/* Ward selection */} +
+ + +
+ + {/* Filters (Zone-Ward-Approval-Date only) */} +
+ {/* Approval */} + + + + {/* Created User */} + + + + {/* Date */} + + + + {/* Submit / Reset */} +
+ + +
+
+ + {/* Feature table */} + {tableData.length > 0 && ( +
+

Feature Details

+ + + + + + + + + + + + + {paginatedData.map((feature, index) => { + const attr = feature.attributes; + const mapping = FIELD_MAPPING.Building; + return ( + + + + + + + + + + + ); + })} + +
ActionSl.NoZoneWardApprovalCreated Date
{(currentPage - 1) * itemsPerPage + index + 1}{attr[mapping.zone]}{attr[mapping.ward]}{attr[mapping.approval] || '-'}{formatDate(attr[mapping.date])}
+ + {/* Pagination controls */} +
+ + + Page {currentPage} of {Math.ceil(tableData.length / itemsPerPage)} + + +
+
+ )} +
+
+ ); +}; + +export default RoadAssetFilter; \ No newline at end of file diff --git a/Form7Query_Approval/src/setting/setting.tsx b/Form7Query_Approval/src/setting/setting.tsx new file mode 100644 index 0000000..1e5fa3d --- /dev/null +++ b/Form7Query_Approval/src/setting/setting.tsx @@ -0,0 +1,20 @@ +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[]) => { + console.log("Selected Map Widget IDs:", useMapWidgetIds); + props.onSettingChange({ + id: props.id, + useMapWidgetIds: useMapWidgetIds + }) + } + + return
+ +
+} + +export default Setting + diff --git a/Form7Query_Approval/src/zoom-in.png b/Form7Query_Approval/src/zoom-in.png new file mode 100644 index 0000000000000000000000000000000000000000..73ac96ef03e9878c978637f6c4ced8cc0286cd5f GIT binary patch literal 23306 zcmcFrhdb77*#14X?7df#tn9tXNVc*wvv)k%JVMAy$R?ZYBtkrPl1<27;mO|HcYBZT z`xCy7V^mXLR0W@wQJ1mYPcb&oMo8r4j1}B+?+m$$u@) zbIrHa>|$*o;AyJw-x<$AOls>G{_g*W-)dCvm7r7ONpe(J<-On&VJ5czAn@>k2JZbI zlgzLANh5{Z$WJbAqJ?@f1Gb;(R@_LP=^yhGuj9amuh_(RLf z<85GTFq3Cjg+ z9fp+8vSE&Z)tJ6rxI?x=lui2Y1WCyRC%5MQ;saT%EG-$c`MO*)BGx$&*^mTOt?jM! z?j=;lQuE_pr8k?}e1Xm_hk9 zm_eh*{jWd6#NA6THw>=Dq4F_}@;Nn_dZIl4xc}WP6PXkQKOAhl5CvUEj;RZoBJ+wc zbpHA#hGy!q+&~hs-uJ@4zRd~m`0I8f1k37APer24GH$GXEF>e=V~27Ide*bTI=vj; z=lDMP8i?Majx?NHh9Sl^G~;G{fBKOy-`o$mhMdP9b32nOz9lN?J#49e(Wi-`?w(8o zqdInFkK}!m+f`QeKu3_H7%7i~pk4iBJ}u?m4|i@G1%pP{#{YHpvIfHyHiUP%_Ga8n zoLQFaKL4<1xjh>k-#SM2$5c*r)BZl>Q41+EzZ{&jqfJEmv2?YT#bwB>#)ifWFTxO4 zA%bl6*aaK8Id1dIvaIU7Dl97(_oGtI&M;!a8G{>Y%~LlkFImuhFpx%L{M>myFmX2f zcAxng7rIhw>S%H)=#hkhSvgrz#=RALNTbBlupkky5b5LA`}NxxTMP{Y5k$f%QTQwr zy7Yb$WrFz#4Nhd;T5Eh~ub{rbT?CF}t=xq!7Zw%UuT}YV`5|O} zECeYiR&yW(_WIKT3<>3#`qIcp_CPZj$Vams)CsyAD`IYs&&!ZjvY^Rg95$3}VC$wIagXk>Vw=RcRy07Vpptqmd)(8aUFX_;X#&}}FbD0s|o8!M*Q<7aI z9y4?piY>w9_=ig?M$y_(YcLN2IG*(#u{#`$(44Yk%`MoWrF_xHcJVuCoLAB52En-6hx+*cD)-$9)1V3Xt&FLD*G6RNHvIi0W z_A|+>>^M=4WQW8`ou~yPdG1>_c0d0Nmj-8*^lJ>Z`ODei%!Pa4tr4Pu5|P{(!H@np z`JJd?-g8)MlNXzh8x*bGj7A(3){z`6h)97R>Gbf~x|gxJSJZ#8mjs>UcB9KtM?H4> zK#OBIpNJ$zQFh;rRJkMGZSr}`X4eslW|o4&aQz*xA(GAuh*rCvk22xj-?Xsiw&#so z#PB=Txkz-V?)EqbrOg!Pu&#I7sOv~^eG}`wrtwk4-G|*75bhMI$(OH~fu03O;@uQk z>qTZ`Ia5n`Q zAe_2P`ljWiRU4>YyO^*0S{JvV;Sq=0`eUi~7ZzVkV>rJ)>oDyGFTQ6z!K41)-7+*O#Kdvy=t>-JkG=mN48Bg1*eDu4^%$u)i8RMV`*|agg@z)~n zFLt#om+>J&Xy)k|3-e)L%_DLadQykQ!jR8wi~l`B>YYmin}c_8K@!z$bdn07FG zh1qsT_WKfd2u5@hGBXih#>fjA-!G_XDp*-bEiGG;5ERLtnKXdSrK-7 zTV#}GKhnS=ASgIgpriOg#5FQ3ObjIj$-(QGBI4=0uF)er7KSXzshGO2mlP-K?3i9{ zP7-KQ$PUPu;#d@^q*gt;eIF^i9Ye7pf~$arLEz%Xc<`$YfBkToBi}NqmoqaHTv-)T z3^LPec%N7@iee+iv6v4DeK7NtEL=%ary-F2PV}oqxjh!uUhh&JVq`py@x$HB$pA!S zcuhLEest}NKZD7U%L~E9FKBHGay{k!zqpN(ZvFY25AO>QV|axsf5%Z(()s6zZm*+J z!?4zs^|#w3{>fkpi=isLh@a+rb^; zU7t%VmOtD|n0gQ@!BKvC1#{+^i4*=OYSsTI*09EpM>Cr{e?UM8mh{-N@#Ci#{{+B# zukR2+M+wQR?oTIMp-u}_lInujDzM(eH<2UuxGcZFf|rX;uV=Q|l+ z$yumZ>}J=IJW9K|ZoS9i;DNZJVT4ee6)s;`7qSQ=TgEt)Kg=;xgbw_Rm05e$G4)*H z^5&txelWXcX6O6WvKseZPHy97wBkNrqKr2I3Xx9IY~eZJIX!OOg|w*voT3-tLq~oC z-Her3qT{bm<0)bt$*;w^uwdNqk7o?7b-Z7lxBfd5p4w``@so?QG%1% zNG(Zr{i337eb{~at%cgR%vDP>7 zsfP2P(eAy~ZISWOxdWEMUtD?^0|ZvN0%QUSHM1l%vhwM1o7Z`G8si&7Ys|r3pbvEF zr;X5?V*IcjB-UkWcsY5M7BSce*~B54HrKrXF6KXa#U=M^Jrrl$x7;tc_VHLzLM^KB@&a%G-ZarF)uFKI<%@Yg_?TiY{gfjvIZ1adOqgTE9{B#0Qc1&=CD?*Gt?)~UPgZ}Sb)~Gl zw;z1Pso`r!)B+!g=IaL+4xU+~z8F!JvEAt9OVaI@Wk1uvE~RQ?o-uKCes<05_ETIp>;0sv z)5?(fPeQ#5-?o>hC$uA~f3%?^2k9#>*`b)iZ5)GF84UGvJ`~#xtF)buRCLz(x+q-d z!`ZWaar+D^3pH`+6c$p2Fb69o8Usb)c+vP$5aixyY|6sI9(og)IV?Uav0Pen!cy!& z3BMzlYQr_LP~*XCbul14#$lEll zl;A&9Y#@pvx3!STA=(Mz!{ohMV+h(E($at0_4KG??)xQg=muvVHq^u=YXFr&yCm>d zt6C%e{of?izW?f~r+gk-<<_*G5_iX3QGMVx*T_=PR14UaHuDlkFvIZ#DFx=3HL<-z ztlHzFrYCkf^?b{3hqO-hMf5T=VIRrhj~m=2Hhp%tr*|($t=}yzVUj4bixzWKHakD~ zshMIu`r$5$UM-79r&2M zq2$27QS_}pUywg{e&>#C6=~jOVa76K;J~}g9)~1UhCj|%g?sI69*E9?GlAGkHbjN-51gjGpx_fQs z8sw@Ma5&J)@Abp?vMeWDxOg##)mx!pLb(+c($0Asdt@|gsX!y8O+M=XJ z*!ce0Y+-GwVyefZACR_%u*$PRw4v9HVY&eU+-8A$95Z4|FU|Jt)YR0}k-O1@5fcWn zOuyao25{>hez2dA0T2xxjwARc@|4p3eore#NMDKMgF2T{v>eR<8cc>i0O{!S+SbxY zH+joX|2{MP;>Ff%#st^>fy07jl=b-FDtj5b)FkyBzZG{VZDi>s$7=ByLy{}4hd86} z?!zD-BEih({>O`dC2tsYRB-WF2qyzRyt1*$&;*dCj>ZoYx&5;boev$6)foP|-?qbZ zx!Yo;g4IIgKv~vS3OsEMhj}cMtvAeHw<1&R);lkF+Wx}4H^@=MMIy_D5DlizZ$vQ3 z-NS;`L#)!%mG8Lg(@l~95;lG|s-TuyFzxJqWZmWMM1rq|x;Kma5tE6u6V+FZju<7m zAw{%}F_1Tsf9bGey2hR|N*m*d2@fMi1B?D>eIb(C>dq0+;s`!VQ_stBzZXcdOV3;A zYUwDj!U*~61_fKTn^GU<> zS!Y}Gaz##`>Wv93vi}DhGt`O^VTk&m-L&_oZx2|uMV(=(9yRN-nr7z^%T@LM1zhaa zuSwas!lY8$95&8{hgn*)?PJyrFysrZKqe4(G334w$=Lg~Ok{DGB`OygX>w?>NtBnL z@4hz_^59PkS<}g8@xX90)R>;H`x|SfP_o1%)fitG64TwU#NOqDt2|H=XYe^WTH|w8 z4Mvf7x4#{CY{LvaU+U}?xNInWC!wO%ZY6S5-_semP$p#rf{iiDR2VYhLrUz&(>o+y zedea7`1ikGkT!a+7Sjs8x_>_rH_+cZqQkZ4bFx#3rx$Bs%=>EFi&-{3U>g!K0#GW? zKoSj<4xV4Nw_G$ifv22*>Kz_uwHr?UGaVv)l|j&FK;w-Ay?9b4Xp=v$XZ+}S`RE@hFFiklz0y-o(cF5sq}E>l#6VunI-A0x&gfjc-@(oQd(UYl zbexjJ1kvO2q$9&hZy{gq(7jrcmdr~mTG5{$H7(V}!3!Yg=f_F;(9Gcn3o|voa@yu! ztd_Ud<~sG&yTzA~MXX04{zqZ3QMa&jP}YxuW}_6?2S*vaPfDn7ptCvr34yPp1s}Du z-K&Cfd>=K|zs@i8Mb(I)fh5Y5KAfqec|KMGz5Cza0&fHEP*HR9jIDikVyyT96`cp0 z+~nr(78#YFChjSZ2J%UbDoQ1xdm^#*I0yKTkRh@Jf+KuK`4b7feb+G#_eI(1Xf_;9 zZHEO=NJC{6JN@03aqoh+(+gCQN%zEKey=z6dBI@T?08QgTS zU-DBbta<&5SyGIssT(p>K~1|H&zX6i)0IfH+o4y_Ydb=b4(}50hW;&u=*M{c{1(%K z7cS)kO9ZK-A1L4)ZcGt4O}Mp(EGdqOr{iLv9Z%XjsgG~4dX|_N`ta*uk0~SF0u=x~ zz~Dz4*hA2;xzq+woYK9F{0AZ=>p3rH^xZ8H zNg2Nrwik9bx*!$}D)!Z+3>Y6j4cdwy<4E7VK=+7iSRgT1ib}|d{^j^!(*WA+^{tnq zJ+YL%3D{?8naHW(J4UXX4AuHmq44_+UA`6%mn0-Mh7RNHz?ixESi*e5Boui?O$9T( zF9j+8{<0M`^LNO!%tTE=ET>tLUpw-HnUu2HsLMh? zpw@i0FIYVtQ=`>mz(5^ZkST^DROSjefnL~%bIbJE@hyyAgBX+Fcd1WReW5itrt~h) zc(^hR0pCdm+Ze~u-;;}q%1#WuuBcEvz)WOSGeDUiu<~K13bVPwEEr~iiyU6g^KXH< z>ObK*$gtkIzKz-vB?{+Ww^>j*J>4{E`I(puXcJ{0Gc;8vI-U^h62U|F<+=|3h30Hv z12qzU$5Ge`S>ve^`E%f*H+fp_#0|&JC;Nbo5M20az}UXW$}7Ws2GGzS)0q&fT^I5L$TO*!LFx7SHu z3*25DEH?eX);Nhmv)64OIi`}=#hx2WWi*C|n@v_gVplCq7D&Zl#+Dn+72(060gD)i z7h_u!`5zVOP%(a7yx#LMokeege$YzR?$Sb^Khx_jFrx4);9DawQB!{!IF_}#B4sp9 zRZK8F|F90&y`>{Rmv0*BQmV6U3cTeHVPf0_mDui>@Q4q2nS=p{+pP(yNOTcvWilqF zh}2Yj5COgiQ@f|mz^Qe=8A`$cw`VPX3B1 z+mq5zlTs5y`>?Jrc2D};(4&IJ7>paQik#kMbTnPlM3b|+G2~um$>?Za)Q%Bwd$n6Z zd}kg+L4VLKaHfo6`B8Bsx93d^0xh5G4HXN4@U9-mEPClz+&#%tHHgO0aICYOvGuXv z4#W*_8x@}{ch6N_9?5kZZo!@BX;aLl!X)y|Mg(r9EQfJ@ ze3=_WWz-1~+f58L{3ym3ehbLOgHxThgb+WnkG-U;eoS&`9ufC%R~fh^-m;ccgF{KN z8rJ39Jwh?BS3DLaOUy^{#Vt0}{|*(vM1cvF(4^i=Y{*_=>SJL>WfPi4tw5pF6ZU=Pl$yt33XNX>SjHIgo;iP*fbs)`uWkL9meOSf~a4VF8*&pztPSh zFH}~AX;J7G%wr?$5Z1^04FOJ)(zaX5hdoUDfN<@(ul*bJ ze7BHs$6R2$;c5D3{roQw>0BEvC`Y}6xNnz9a_Foyw;pEz_D*GQ&e$m}B@_>f4n?fk zo%&{*0iO6d0{z(s1Jft7$V0`$VqMz56E}({vz43XuHC8Ez~}RCUS5yUz(3z07qNIG z%`HkIzSP{@xe_%{mM6dWknrpR^)YZ-|3C1DWzGF-R&rn2kbA7Bax6QElt7S_sTfQZ z=L}jXucXFD5>KzH%my*Q(X7>Bz(72DtL1O|wi|>AcTAZBLYTR5bKXd{{`5OkAUOsO34A7{eRi70j~GU&=Jp0@b`lgPB@c zx=!P5s0b)PaklLX#Rv>(^xUH*O#6g`M%F1kIf?~+ytyu7+!dM}~LuP$rirw(0WsZRIE7YQ4xctFo@2@>t!)bHC7-_-Lt|t_)YF z=&1T$I~V-sLFRbr>@J(9Y=#<`Bw3?01el#L#D3q+-d;_E;c&lHxho-)_#2ZQKWYP0 zQQbr$S3qZ+$ez=G$HMQ4u4+cF8Z47%)9b1bHC4*eX1{N#d$NeN8KWQqiP=~@fb2@< zw0I57JxIVyg~F%2Yp$6SI)~2NQ|=or6uiY*MaA2)Pl)y0hvDniWd7N|+TAe_*8cZu zmef7>Kgm&=MSh|93}d!O_}}nQ3RZsKCk5T0wOJ6y%U+QxOYdTE&N4oqcLISkwaBG1 zw^57n#EIpiJWfbPrGXp8f?==6#WT@(M!DP7<-tB8IWdx2V_#Ub&IR;~pkbiFVuE%b zHy^awvG@1+R}S6frwgmCmxO;s!iH_j>hYG89)W_^-ArRwfPwnp6r&2u#GQsm+ zi1n5Q?n?TzLdXb}*b6v)~I>6n;_ugJN_lvyZ>C;Mts2>vN*!-!G`AnAWABFQ0Lvg3enaT13KyrD*WxT6;>&uevLGdM2W@VPy2rS)*`ZD{XZnWx>d}a-R;6AI;gsfKIDzbOud== z?_UJ6`XE`hHS#Lz{m8Q6cUyFLch0#VArs+?7YaD|G?OV3`TB3a?x>|o1vVP%;a#8y zVnZy+j1aA`Gvf8JONn4=oPN1+v;|*tj(g*)S!JJ3ry=snB3{+T=!oK<(HgSb4GE2* z19sxWc+Y>28=pjlV1Q-z8+LW$9QyGvkfh zs5>TpaoU? zVGOwiu~eHX@xH5u(5YMMo_-(MFWLtyc$`C zD;E0KYgSJ=-$DYM(R_b9!wO1N4DGb{f<}s7Z*$P6k~+SJI^#P z>!JwtMbFD2=qDirBD#0CP!sskT)z`tCnjXbk`??u9)Y9FY z+rivoT~m_Ak0zoyOL`o7us?eMT|-5(+q8sE-3Oh^(fQZv{yK z_HSPcF+mtZI{zl{3kNWNntiv3p?OqH=u%4N=p4K!Ke`!?=Sv|c4@T4d@yfW>wnRwv ziO%-7nHc`%4S;W9APBZP`2GC~@U&YSHMrVSu#cW9HlsAwSR-HIExL9I^t{pobTEp7 zBgg{nUH+can;11cQR`m~fIzu1-n+JuH7iwMQ}Q}Mp$QQ(57*V?E|>upjQ2h znD7WqOw=W_xW1plRQ^0`+E*@UyvR1a#SCZ4S$}1Xqsga@8eD)a6CL(3!@b-N56Ob3 z$35l+BVN$EyB{|-hyIFBRZo@QLQpKoKA~)Y=)JW6adavx`xve*Bs7oCob()KKYBM_ zB80#D+C1^so5;AAx^`oc(|X}-eFpoiPbu=ZAm)#o&`=&GQB_RMuBAZQkW?`TTsd}Y zvMydrM=z!b9l=qu%>cO=f2v*nz)wkI{I~QV1Lb36^bn3-#=rxvD}@>Chg0Q1K|ggg zzV96ZXtlWSOcUy|7l9sq+aG~FayatKP0Gv+J3TeLMTHl!AAC`pWio+{Qpw;D<34af zG_)=Hs=|GLdu{}}+qKLpCAbocfBsx9p)=e9y|n z%koA>;&9s1G*7BLoo$7F^9jc~X3n2cNh>deoYJE%XXrC$3t1&7zJzoPtTl#(D#wo0 z)VX8ByUiuMR;WLP|3KSi^|({U(vJm2TF{aQ-t&4zNdzSY$S@TRnj$zxZLN;d32?AN z#~zd9H~xtSIl!dkWb3u<@2T|`9QT`#8Ljg2`FxPy8c_R|YSQVyBWbw=piG(PA)yyU zUm+ss=g>>ck0I%;HzC~w$>05Qg<541k8=W|4Cc>?RpLHquT`6umUvnJOsVBt?K8T~ zjW9Nzhy)qmPx$o)jXtid#JzCj6&M|902)cz08R zQ!lnJjv7vXPP@;idOVPPE}uo_K`X4ghW)dRmCXn|4S$8`fTwgY{;vN(oZ;2I`}-}) z+I|;TS{}p6^;S+gr@T&|b5uF@rl>M3CzF6Ntvl)qV5_)NUwJa-pz~=3FI9|4V+JCB~ zCt968`T~l?8Wa13!Y3v5u^0z|yQ0s+VsWWps%9LMY)BJRz-x()-S^~Yxl6}B$pMV& z1tTw@T`no;9y^;ZJM|yyWwS?x(;(pR`r^DwtDYmCBhfKLN;cM@*SqC0U6AyLfv?A_ zuoogGAOHFAVwVSTZq)LUr^VC;&%*Ab zBQO=FC=i`K*R`KgQsz@tvyciay9w5*5-uX}H^9UQQq*7$6MAER|UI6-m(0;X6Jle(u`GMRe4IGV?&%2Ejj03O} z@Oh*-rz~`_F-sIsKZCUXTfmKj{YT?!F?o_@p(;{Fi;kFQl0$ZK zL7#^9A6T%7XEmdJtRs+hE!4DlB;v~Xh>yBpfR8?!6J3rO$8!j@dmDKQL+1S;$>5y& z?kx+3UMk&R8d|g-qj|GjUAC7mLA=Xqe`g>17#Ya~g=z0&ELXqU-6cU`D6bDC?#Vnda7geILkr|Ol!LQGD!Ab74(302^%VsuBuO$ z2qD>jwig^+VMI;?p|vqT(k>zC16L{OipE7?55J(!o_*ekq&F|m*Sg~O<#-T44mHNy zIB%JAa4RAsZ;lgnfoW)#-;IB0a~>J5cWhc+y?RpSVHe)-8u~cBd5odl_6sfOjq+$8 zU0o<8dFmY)@#jxxbugiEn6N&yDMI)9elI%Wt`H4`p$;~6Uw;q!SogR8b14~h0wC#@ zmVGf*ovs~)kDpC$y-Iy;duuZyMCk8zr(9ain25@Zo#rbkoJCs59*|~@mrBE5vz(Dn zp@UpGu@Lr|;xiK33h1Lk^hB=qv#wzMy1yWYUK7ZL!dL#NQm+X%ZspmE=TFDlHu@C} z#HXQk2ZZhv4B3$adGUL>%L3G5rC){_dT~-Tz*q26X+Yn7a&tGM-%*`QJp+d14RyTS zkP3Z!$Hp$DTEuJD*M0I#>YeIK*TLF^q$JfWOV!m?#{kVK27N;`7?A1Y_CgjPUI|}& zWy?=(yabk|rW)Bnk@8$Ft4(43IHRgaldpXLj_|S3w9Bj0D-kpO|855la}omtKvO>W z(`mQHHwLh~j|R7&4}G(iMB#UE@)|5hs09Sj|FW$DM?Y!{J>b$iAw_ZkI#~lSk}=IL z7yvq__1kul-84jiJFCg{5|^1pM&w;mQYX`=w?pg9A9-XTHM0|OI7$fqI(OFNVB9`! z9$edv^~&kWJz_e>{%(8NBCJB5!Vu&O;YeNcQcm9@Kn+G9~` zRH~PX^|_XIIp3R|nS~`zqH8xUxn%Dj6J%v&Jr-s2hX5kg?WI7ls`x@U$p_fST^DXG zh{;rY6uCap*7VnQgPi85n8T;2q)6?&A?RHyb=3+{n`+@K8^n=xNo20;M;{`GkYKes z+?>O#Z4F@JG1={wqj>IpRCxMw|KDviq!~pW5&y|EO+cJ~5kyR7;#>NVy5lwsFt-ml zq~)_nbin+q|BiX@ElAh5Z!CDCJslF_B1>d%iSe-BS^G}U@@fG=J721R^FmmS7ldrW z#Z!>Q>!&X`@l6hqDNs@@$awhk=~8bd@v%*jVAEfND5hOC+*v`xQTYOZ;X= ze%bjC(?8Z!3lqGrmbH2ZD_WoTo}~m@iSNbQcHqTuAz6-)&g_6gvl&s8?XT1; zf|l%BVy6k0*MmL9QM!*cH5IBD?H2QLuZC+)sODC+Hsxw)WwmKV)v(bNVjaR^LocCs z`;kMaE(PZu8MC@PN+zagr^;sP2~%JGPt9h|ryg9dw&$XyFW&0t=tO-M2qdJ+5cke~ zSXEXBvU(5f=amm8>i(1iM0Z!1ji&#zU_!<;{#k#90?xA=5xRIzE(oyfto9WC0KE1) zh(JDMmlyQK+%V|pmFZ;d8U#nXpoo}_t9&l;(J6(nvDF~oR2b_v=GyJ<)6%o6CdR`+ z6Z0~REi0p;A$VbAN*|oc`KYN0dbNFsroCT$n0<9=2k>Cf@unn{Q!CyLD*2We>U8DBSkxqXHNQz^{`0Wdg7h!Mgl_#6z~soyHlw1}yhpKMXgqCgZ)rJ4MP3?(7e z#P=x>^s!oXYAT%HR(tx@W|3wIiNQcSh+NJPQ%1Avk+nlP; z0aHz1dm)d8;x^!efOJL1^YGyVKiZ++cNrmS>qYB>-*->19Gx`>23@dflcb;r%F`N% zh#+@#?U(HKdu^-_IG(MQn#Ay1UwBxPz!hSJozhnytE;bElv3m$J%@lG>Nb=ppGNu{ zg6=G5<1;|KQU2Jrn~){!eOYUQ`TI>;0T;OqE{ zIgAf8`8mw+h82i=`Tf|nK3KNi9fVZf-o3>l(ESbK%0Ht45RO{zCbYZUT@i4X**a$C z#3?u|$+5g-P1FrbP(1^<;VlOl$mdD)E3}fKbGTw|-bgHuNxhyOU-#XaPr|a=U__wx z!Xa5$)x!iWAshv{DF^p1Z`5U%bqvkur=1Bs&_v&Mc?8Ja^nmj@KBR`2$>(qy-eW`E z)VZUp6l&7wA2rJU(v{K~&aMkU7%a)N*t=ImblT6iiUI4XR5I=)@}QQAgN20E__NT0J(M$lfYL9;EHPIYis$_|F?m#}&k{h^`>HPlDao1ETW}i-DCWeVp|_vEFI10^xzv zxImSdu<&0TICdFb7SQz*{|t(DooZyb1=6>>@GFlEb$KqDrfxsY8H@{BNnT+=4#q{( zPXE-^qLl9NbksD~{)bkqYNt!c3~{*i?V%t2y$Yd8D{kOk&2ET5PmQ?eVI{x+9UXmj zulbBeT}}yd&cM#^DJN*^ds8I47H;3{OjZgP2vvOCwtyGU-$4$?CL^@hv<`N-O|GSZ zQ39A+yVCgxfB_G-vf*Bo>tcQ1;!){A+GwRq+1r(sNf)r#ZLb-0NLAG)Sh-31nc~MOO>z4EoYR+#+7tlzA=@L7uHVqeM-xV0s7e&9j7Qar3atM=A6T~ch9=>C+rHV z$xM3SHSz0PxA)Ho4_tx5X(I-%_wTKeubk~{ef`R@MQf15$-?{7Pp0sosj)98?*g)TAM(wwH6ojm+bkrJz3-MI zs-%}m0(6w)3?IW<=4{%3V~XP~9GsO1I+iO=|91aI+6w%^QSk+|Ak%V%4fPxbF?~M` zkO>g>lmV;K0i?ii-4kq7j3BM=G)`*2?Jbp7I%*57@~60Tqv}TeUPK;jJs20lL3?S2 zGp8b|SJa=UX(8diJ~!RIE#W^}u5S8jnE3WHX@4S%**74_s2eF&EMwr{7>)q8ausd` zArOBqAQ6{`3V^J>rP^-OgS*ik|N36dSf6ltKIJdV0Anhu!Cecr0F*JpWN0k_#3K&C zIeS(zTSkOXa8LtumFt_{gsqE_X(6jDg;mM9CVL4&6 z@dWqe4f%W6!rC-!B5djVp@7XF?!xU+n>s2cs!-AUQUt{vntz5{T)1McE*^|v6}9@} zr7Rde1`I$2zF1X?Bpd&pB#N*o_8mSTu3lRim&`pO0z6?e4IkPUpoej5zQpIIOpo=} z5TIMU63vO|@7B5zY$ReYt9#E##t^=3=bg?PrhUxfwpkCjY}K+%G`Xzk=XSf-Dte{h zJ{@k&GE}u53T=KJs>E3N`@$MkFafl-cQ!6Q4^4mb=hd$;QvrNkpr5x&xgd%R%p~g_ z0d%W4M?+=b!GTP^y8fp|6g0%aryZh8WY@%*O7Vzy&DvdE5lu+QrhR{6{pG$!cG1{h zS%`YF$}V~O`M|^t!Mzvg;JLFM8+Pdc^e{?4{Y%VcSnd7X9QvI0siaOuLoRi zW8Ayfxp6GR9RT9^WxXyD3dzk^hnpV1W4dGM{DFkzm4XDsY@c?gix>S@dJ{1=;XY4w z!d>C6aILvu%p2cD+yc>|;|X7IKzza7_htn5m1Q>BUk4m+2t92aKWE$;Kj7|U?+jA^ z-{gj;)d39RQasBxKDa>lv0Ex0qv7s+*lYpmbDyH!_@Q=w47wPvP1G3ktk0j&t6v;y zsAr!Fq>C;WUn-FT0(?h9*Nw$XF2^kUtsZVO-X9Bvpu;q2Hn*gARthEa22XS}vGsYg zpcfBcsnAA6lk2g zaywLX7AwPg^uik^4Lf_fKR&i)?d<8%W&g}ajE^-OygvRji_@<+Jal)&GoI+N#@jUl z1eW5<({CNgH#~?r@|}w0f`Uf@CtE^bY@j#c$NZIEk5fN)#0RFMku8>cnRjr}6h@4a z`uuJjr@zXbV~*9=oFGfggOn+t(;EY*($kYj$*%L~dd%|Q5Deuz6%U^Ffxs9G!D)B4 z25IUx*MBhcwqu#zR0>>W3?!P1FQKN#Re&5%P zAWO>VP1K$3b(@&*xFk~ER*3}AIE=W#;)n0FX{Apgk-PN`-ITHvY_#d=PDP>Ud`1lL zmjCsBbd(vcc748MIz$O%vQ@g?d{@Axx8GO%gI&rUT^rwq4RhH)daQw0`vH<6p1j`y zXU(4ST&UrXBO24SwM9Yw(qoR;nmfDOJz{?96+M0e-G?W8cIKAVhHapuICybxohx#?X!E zW4`RfraoqiNdFC)igdr(9BDOUis~Q5m{-IJ3Z4M3= zK~3*HYW$@f-sb;Wqu(C9$xf;7+2xGqfO2$m=0q5=nruVtmGC|c=^1cwf-uya%?)=; zsc3KS;<`;Hc4h~>YR{2jx|{jjyT8z^sjJ{`9iBed%I!^!qAI2PBJgVF$?o~sW%M9* zSt~`^Z>!(UN+3n~K5Lx%d!x6n=);F`-oh$~YHCQT2d@}idq;UnaI(IYx~fGy3UJKI z$Q`1Ey<+pcfs)z`WmA^$*-zWDP+2l)4`CFBjl|&xzFq`z+8 zYAO*#-Fqm4)iEIJrtBEd%n}q+(@Oae9creWegvUh(xgG|F1lvfC(@StJvo!qYpVOr zN?AH~DdsO_NS8^k0pw9&cgBL~XAN;-9W&~!?t6EBr5+IgeFFC*#z03Yka&{)?UtR9M=+9ouL3O)z^M#$;U zDGgB07+VSh#8l7l`socvNUNB~J~`v+6Icw*pQyy*-P2lLJ-e~AE~{=AQ8PHRIEy6~ z91pLe8WbkVrC%*Wr=^l#1gw9H-cppevI+KhnWMX!nPQK@GN{akn5L#QvPl$f^$+&p zv(SR{S5K;f(|14=KqPxj|DV<#XfvF<%iZ__{|11yfSR+0ts*DF9JQFW(BQk;lLQci zfU?6vj?n;__7Lk);VXeL{!{(K#vV-4dQTjMVK2J|yQ#+bxAI^OoB zBU^1hE*KTzb(Y6s0+b#iZc}gUvJEa+iU9HFJ@>NzdL;~_V+P*k$m@&^kfi*9AcPNh zaqQ^kIbDmsoV4hE`c!A5JL@Dae6aX9{_hiB`mk3n#7ai<+y8u#h_-;O6+9g6p`=_8 zv>1Y$JcjPI!v zq4CXh-{xj@PGsX4nvQQvkAjYmFLIU=2V*__?0^uG&u8IVd~v}erXrxTQ;gC;wElHp z{f-_Iw^EEQOhRMyMH{5j21$H%Tf4x{NU24sb(J{JNrksJ_Sid>l$41bpn1=jzdAYO1 z8+`2l)*0=x%F+9AxcHxE90+-!(-R9=q5ev_jRd07wimjtGS=!27tutEZ%MotL5TqT z?R|j}y**U47LRLcw#XZxgJNQ9wQOcns!~`0AnZH0X)_v;>_9!X0HxY(d5N&-T`*%z| zt{yl~w?s=Q30_@fhh8Yp@27H!Uq3-n0uiJs41(r|^E8ONB^h=n%mP*!n)e1&fa{_$ zPM(PV=hnoO5zN>aiv%ji1Z)-8Cz>(A*8=tF#Pe%;KE}~I{rVO!I2IqXzH6^vT>6YK4D<) zvULVf&|jgZ512d+gmOagXtRPO@EGEL&Ja=#aur0;cPAb7rQB?u&FdE1Z@1`(TCbl4 zR;j_iiRb}otR0Z#u5lfu<|m5cMYgsfQ_x<^NBQD?xGOD9QDO2I6g3oOq8ve{`h+P} zFqj|y_V1rB13jl6L|U%z()YRHczU7xDNxWN&y}1n{Q>ehu0#Ua&%<8&3%8iRD5QTF~Cs$g{|( zxTPK(L4EY%J}$nc4;R%h&KyzX&tn(_jgPlRT?};<6j;E>5xH1U5>w?@rNZ(gV>!vi6f@qpx6JO|1^2eR!m^h7eRAUG^)IZL$$ud&si5=ZKNa;^ z#iD9&Fg2-`o_E+!HY&pQ(_*Gu;ghB%cR9V754FK_**hTp_EB?K$4|}H-upq!M`S<){($>W6aihmg{)~VovlOdE$@j_)pPSrsvMr>c@H}oDgwObLn=uSpq z!y#&#;Ob=Y(_)AFRCS1VK|!Yrkl+I4Ivc_qJedY+76L%pEUfyZt#cwr<+gN+BPPMe zX^EZsq{;vQ)lxmL(a%br>K7{&G#a#pMES0kD1k!|#SW}orWDFOnx$R#n%rxbW$GGg zyDHJ6#!CZ--W}&pk|@J}+_3}5UVdv-Qx{LVU7d!is>D1-BLva9-Tu7*VvlD##%z8_ zL#=;Lgp4er_K)I{t{|kAUFKl+2@fCxy>|XY?GRhM@m}hDJmcOInkqbDsB)3r3wF73 zd+z&zZ!a!x6A+dE!Iy=gh){jI{Q#UjQHXy`IQPrnh}cQvFX%ulKu6dVD-pr}{IWWT z?2WlU``fQud78ts1QYl@S@mX~wOsrjBETeabVOm-Z`MwaL!nA&q1Y9-4%o8QkP>UP z4c88tRrkSg@u>Thu=m0`|FY}xh0?{>irte7*I-t0eviAL=H*SKG2N@iea3ghgF4>Y zKvdv!8Y1a28rcU5KYPpJb7e0(I0Ws@yR zGPilvXp9%~0m#%TaWJZVro1Q31BlnWWbs6}a;T7FUmQG1DjO}y{|gXF1w_i8ZO_!4 zd?>4Gco%T&U%3tW!`6E4f*LbWV=!QU{_D(VrFTyZ&S^z>e5uz=iu^+A3m)(T14dbkSj{=Y z$S1^Yp+g{s+S{`mwX885stI&5Be1Z{-}J&{yzHsncZ3i zT&>|H4y=V9iDCmaPtY!?(KX8CNF!dCn^o8vTFgIv@pGcunA?8-E5$EcFp1n}0lPTL zbE1?tzRTzh(u}A~_a1*V!|#taM^4;8;U-6NEwcwBs93W9FpHrT`<)0B%z!t6UD9QQwCsxctlXEs(sEv_0tOI?oJHiMhPb*`VC=cK2hwW zw_Irw5j0~cVY=P*2CC}Y)co7eKGw`-pl&;;D{YcfFCF~}?9s2#P{+FfwV%W_(y&%G zV(6EFS|OT8fW7rO|8}$RPaLmA4?iRV(l?QUF5{#NpT)fkDTGE!P1-eH>03*A?YP|5 z-^p6O8&3l=qlx+Ku!*_!Fizf`?FmwILW{b#oFzMpC(grvUL-`bwEdg28_IpoC3SwH zv#vW7ClSt30CN8v<^!ET+DAr0w3FMe2$gYwD#eHg24=;~oeGf07|)_APDtC;VAuD2 zS7oyUXB0hU_v<$1mhA7)pq3Aw|H+=qzg%PrIw*sMcG+(W6GHF@l)@NkOvwNw1xl<4 zf8{wuW|7gDICqRHN6o@Cg-=j>Msr&{L_`W1cEH!pR%Dy#OlXaqy zC1uSPC2JyN-xXy`*0GK~Yb41wc<+4Q_dj_3G|rrR&bgoGob%kd_nfO%&A82^&d&iG z%bp|4p>Xed^!euXAZlDq>nZ4Ae4^G=e|OOX{}vrt4Rk#TIhY51KFVX|ZWn#2kC?Y8 zZBM}y0}Y^E4aU0LQV^?ejBve=Yozl1W(v>%v)n*IRZ>#ImVGnp`7=)ajy4VLBJVNg z-?|X!UNtT~6R5k%#jSq_dT_g@kncT;7vnoFiIEGs435078nhRJ9h8seeaW}9D+d?zyU(eHOq4mR4d`!;fFcs z$g3(;G`|lU&Fh0MfTexZxHnT9xQO`WE2o?OvUUk9znGc z{`{BaXZNdUL1xqO)pMdY^_O-SMLWRnX`|JDMuO!ubIG`Go#Z$Q(giDL!X7i@Hm*5quQs6fvAxm?{g z)(0+}3j(45j2(0^#TD6f6(XmsUAHUC7F$Fq`N ztq6jK(XCajxoY;YMhE4q?i0~f>sRm2gq%F>ca4w4eQPJj8Elj`zJcA7#yPL0=q!Jn z1P5mHV&2{_vQxu}Q;y%hyT*d!%e?18{|sIEBA9J(?(35>&T*@>pD(I-bt#~f_oF!LSow-PL0L(NaF$Pii;)Q+6 z6Fy~`hP1STQ1?&slrMUkcY2qO4w(Klt0THX9uc}48vM3KHdO<6yG6>@FELpzMxI*16{f<@uHScNsHZA-Qri8!hzXc3s`wjl9vh=`5_nV`Dkad~YYr zjoWOIRaH`t9^=;5?}ZY}kB^y`yiQNM4#Ur0bo>1+=WvMGLE3_Sa|?fRe75SeHDjXX zRKLk{7Dvsdurkj{CkV>wgb0J+6jI(~TcU^yCd^4@M9&20?V8@}PLHp*fz2*{SrDt= z+EjknOq`st@$8=oS#?XOx5oZql{jDQ4oA}7pvy8dU%j?n2j!IXrc0UZO&^s2Dds3~FbYxG6=Ank4 z9Of^tpOHru?{8uC;l>=WR(3j+AF-{f-yU%!Y)PYV#vH0E!V$uR)m(*aQPCem+tpZ20miEgHBd_+PBAcFXvq9%e;EyVd(tm*8-oKJNT)oAX|kpcIE*on@5!vgR7q( zP2c|F1nXuZGiq-aQ=NX@r7o1KSXu)m^RQ8`rm&W&`VXBgt=;@oUA?vMoBb{7AEgGZ zt-;PZM_w30$PkWPm3gVTe3oxou&51a%hXq277hN(Du7B~S`cdzs;S$|R!CQfGnSn8) zqe)wt6;DnO4dhVx=1qZvWYTG4Y$=0!WUEx1YH;jkAfue%^hZuIbe9 z@+!*JyQu>kB#@)>yCXN2Jve4|2IZFzFfT$xYSeG8$(_XpolZ>F3+f+6ud9X@3k(e0 zwZDpDSqg}miNXzPLrLm%`hF!9qf>$|CF@c+J@5Xkl$N>R^%KvZ!+AAvoY;!eJvP0O zEckDQ9D70NunXYs%q00A4upMNW{hQ zz-DR4jP6D>%UI0HBv=TBgxs3bdako}jK`dGkEE8F%lmDtWY-eBd5SkZAzCA^eb}ux zZ@d0I8V$%!mc{Qf+SZPAzO8qhE>^{3nhtrLd<_D=T(+~uDlyNcY;yFAxF-#-fqCrT zM6Pl}!WezG^-; zH?$z*XF84R*O`wjP(v6jnSrqa+u;1u<-M{x_k)>|W}orpSSdw(Mg&@cKv-qQCcb-F zTC*Sbl`JnDDiqHb7#J5BZ;0zP#@pc@(osi#T5LZ!ULUpb3f-%?k^S1f7q_k-LRsUN zxx~;*fuh7)feeyF0n1T(^iuOHLqZ+;cKo_jYxeob&z6t6=H<=GR*t7`#4Lxljj$4< z#fB%;_-&gmR75ur=H)rg)@nn>^6113OUIHX>p>V$X)gPgkavF1rn!DkhDoe`_OY(= znmR{S*XyE3OQAaORxBLiV?Wma`tyEruO*5bA82H4ndm9j^x)(>1Cv;t_jtuYb8rm7 z>y|HV>jET*fEsUz4#*XA1?DCn>+udP23!=LT->CdY_M3-6 z9MoDar|%I>QMpw)cp*Uv(|>IVK|yZ$fRL%7$7KEc$B3K6iLr);M)z+1Z3aCb0}-8m zT~|Ngs33^cV9-{l=B2Db#wZ?sJ)n4?mcf z?=#~Ag|EpHRQ2+IlZ(jJ1LO^0(cw^QkxK?+42R;B)e8%^l9XrKa1#*@Y;~nm=5dCL zlVQm0cPs2kr0YTDQ3t)pYPyH&5jpAt$yE@)ImvKyGW6s<4t;?iK4}s?+oSPQ!_NJ$ z2ahd}zHG<$rTJ}IyN!wl4DIIYGI^}3-~%irc%XtP2q&>NIYAd5?wI`26!lXDbr+u@ zsbZP9-5%@W)HFPgVS`9)p(Y`@^{|+k4oj|KDgT5njBGd*{by{P@lw@?X@y(wZs_Or{?6+ z{{Eyu*@^MJ@(8%}i* z25Qe;T+y;guQ4yu982<1j_L2jw`VFrd#Q(hHQUWER(_*+{0T9R{FpVgX~#@mt#MgC z&>9VF$ z=ktlde!^h`ZXu&3og2I1EO8m!(H-P7@*ihoBj+*UFvzQgs9_vN=XXW4}4h|gtg z6nV!df+JRL zta0wS$!i~sIVEj0BUOGNdC<|)Jgyrj z{ghL4VY(87y>d1a3}Gll`H9Mn2AKiig~0Wj#a{*x{+Qcw|G`FJ2OH5Q?*zu1d(F2R zcwogG*<=t-So$M&d1&-iasmYggiSW3ztew`icvmd@)a|Tf@CH!bijz~L+VL*#8o_ff^`jykmtt^>k6N!EUDZ!`Ohbf*o! z0ZfeM3C()p`$e#@CR}g@}1wheMk;Azumga*kZD<02u(mUz1QCftsdxJ>b{# zHLfa@T!4rbnXEbHX6DJcWI`;bhGvHs2aE2&-4dpnAW+FxnyHr9+j`J%-ux8(44 zL`^V+U?M)f7h~%|G~vndq5&H~3kiCAO*AF?%>qScn#gfpk}k1S16neMFz_++1x9K2 zx@5`}|Ff@T4zVQ4M?eXZOc?_c=w<{HP`IMaYg-eAt00LtuoTalxD>^2BR}XxOXl#D zG$0{Jsfyo-Ro&J;rJisb&Ok>7W;vU*G!%Tw9?d_KqzO-=1jtB=daVwJKE2l!o{&-t z_#8M%e70j$H<0r$qHqb?Kvsl_c-EndP>O=6OZmFk4WnvWB#FvSYjg-43n%eh3)uo# zNtOg9>J_{`-!bpfCF<3bhv*iDkvPh{R-2cv57=vIX(B1P0b6sA!)MN6hrb*}{Bt)g@6 zd2v7*y|s8E4~&fjq5)eRj)r!Y0`NW%NJV*2Y0=$f&RP!8qfIh~1K^4sidlgeVArVswe5G?|Ll2oAu=v0oevR;T(hFU2c^-%a6p zz(Rs3=iTB^FzfXJ&6rrnI$-X19vl#tl!+0jQZvrb(MewgN;ONal>AwnUq62w92o73 zMwhUXK@Y-i$`0(}lAFeNU+|27z-47jS1v5wn~DYTf$ZweLmQe5ppZ=^X{l~*0Xo>m zmwQK!|LI#AEGh^u{~|o8F+8bC3#yM9QNLPAKzKRjLELtl=_l}7Fts=L8> ztFY^L)x|dtJZ}L{F#WbU8Pa6m@-rYR-u3g-sp~D`q>$=1QOlU{wC2}FA_=gTQhgF> zd;swSZ+&q)e76KUbMY}CB{c+HtvYT>*xrrrQ@lGehtHz~*7ARILi3g;Zse9mQdK(g zE3F;ZN&+hjX)?UWho8SUz8g<|?-C%iHjo(0%1z_zIHWP>$XK@*>PZl`yMMp%*W-@J zDlJ|wLc{10fgJF9b~~Y~wyLz6n^0vs=OWkpGD0w~fC_3di#IUJ=lBS!QS^s!(u}={?}lJF{M{7-5;a| z6H8Mw5ojUej~cfl-@6c`YiOvD{!QI6FUDFzk-DWXzdAnE{Ag|FmLl9gCm z0qMIt$%c1`{3PT|iUis87h62?Z18#nEv3u7amvKQm?Q9{{u}klR}!6SEtczqkx*n5 z*}-N1->?z)`7h}!k7sK>e45%#Qr`}^sy^5q$3G(EkQ*5-XV8s|J zb_a}3ZR|0Hl7hpy!R$_-{b%1)or#rz=<>sR^;oN01dI;&>Z6;U5^HIbIW=#qj-lhB z7Qyt(5kMX_7**-Yd(ifk-`adp`BaD%A%TB==O5F0#N&I@2nd&eZ&}t6SDhcQ?KMd9 zarC_J+MN8M$0i@|XCLj|lKW8XXCLr7Ch&XmLc)p7?Ks1lZ>U2b;0pIqXRcqT-H?|%-N$2(V>FGt_`p|Q { + it('simple test', () => { + const Widget = wrapWidget(_Widget, { + config: { exampleConfigProperty: 'a' } + }) + const { queryByText } = render() + expect(queryByText('exampleConfigProperty: a').tagName).toBe('P') + }) +}) diff --git a/custom-popup/config.json b/custom-popup/config.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/custom-popup/config.json @@ -0,0 +1 @@ +{} diff --git a/custom-popup/icon.svg b/custom-popup/icon.svg new file mode 100644 index 0000000..c22ca44 --- /dev/null +++ b/custom-popup/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/custom-popup/manifest.json b/custom-popup/manifest.json new file mode 100644 index 0000000..fbe5cd2 --- /dev/null +++ b/custom-popup/manifest.json @@ -0,0 +1,20 @@ +{ + "name": "custom-popup", + "type": "widget", + "version": "1.15.0", + "dependency": [ + "jimu-arcgis" + ], + "exbVersion": "1.15.0", + "description": "A hello world starter widget", + "copyright": "", + "license": "", + "properties": {}, + "translatedLocales": [ + "en" + ], + "defaultSize": { + "width": 800, + "height": 500 + } +} \ No newline at end of file diff --git a/custom-popup/src/runtime/custom-popup.css b/custom-popup/src/runtime/custom-popup.css new file mode 100644 index 0000000..65a392e --- /dev/null +++ b/custom-popup/src/runtime/custom-popup.css @@ -0,0 +1,80 @@ +.content-wrapper { + background-color: white !important; +} + +td { + width: 47%; + word-wrap: break-word; + padding: 0% 3%; +} + +.loading-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1000; + color: rgb(13, 42, 78, 0.79); +} + + +/* +.custom-popup { + padding: 10px; + background-color: #fff; + border-radius: 8px; + max-height: 100%; + overflow-y: auto; +} + +.popup-item { + margin-bottom: 15px; + border: 1px solid #ccc; + border-radius: 6px; + background-color: #f0f0f0; + padding: 10px; +} + +.popup-header { + cursor: pointer; + font-weight: bold; + display: flex; + justify-content: space-between; + padding: 10px; + background-color: #eee; + border-bottom: 1px solid #ddd; +} + +.expand-toggle { + font-size: 18px; + color: #007bff; +} + +.popup-content { + background-color: #fff; + border-top: 1px solid #ddd; + display: flex; + flex-direction: column; + row-gap: -2px; + font-size: 0.7rem; +} + +.popup-row { + display: flex; + padding: 3px 9px; + justify-content: space-between; + box-shadow: 0px 1px #dcd7d7; +} + +.popup-key { + font-weight: bold; + width: 50%; + word-wrap: break-word +} + +.popup-value { + word-wrap: break-word; + flex: 1; + font-weight: bold; + width: 50%; +} */ \ No newline at end of file diff --git a/custom-popup/src/runtime/widget.tsx b/custom-popup/src/runtime/widget.tsx new file mode 100644 index 0000000..dff16a6 --- /dev/null +++ b/custom-popup/src/runtime/widget.tsx @@ -0,0 +1,2571 @@ +// import React, { useEffect, useState, useRef } from 'react'; +// import { JimuMapViewComponent, JimuMapView } from 'jimu-arcgis'; +// import { fetchGISIDFetch, fetchBillNoData_New } from '../../../utils/queries'; +// import './custom-popup.css'; +// import FeatureLayer from '@arcgis/core/layers/FeatureLayer' +// //import { isArray } from 'lodash-es'; +// // import { loadMapView, getJimuMapView } from "../../../../shared/mapviewservice"; +// import { useMapView } from '../../../utils/mapviewservice'; // Import the context +// import { AllWidgetProps, appActions, getAppStore, IMState } from 'jimu-core'; +// import { useSelector } from 'react-redux'; +// //import { cleanup } from '@testing-library/react'; +// //import { RiLoader3Line } from "react-icons/ri"; +// //import LoadingWidget from '../../../common/loading'; + + + + + +// const Popup = (props: AllWidgetProps) => { +// const [jimuMapView, setJimuMapView] = useState(null); +// const [popupContent, setPopupContent] = useState(null); +// const [clickHandle, setClickHandle] = useState(null); // Store event handle +// const [buildingPopup, storeBuildingPopup] = useState(null); +// const [buldingPopupContent, storePopupContent_Building] = useState(null) +// const [buildingLayer, storeBuildingLayer] = useState(null) +// const [clickedFeatDet, storeclickedFeatureDet] = useState(null) +// // const [isVisible, setIsVisible] = useState(false); +// const isVisibleRef = useRef(false); +// const [definitionExpr, setBuildDefinition] = useState(null) +// const [definitionSetActive, flag_definitionSet] = useState(null) +// const [isLoading, setIsLoading] = useState(null) +// const { setMapView } = useMapView(); +// let loadingTimeout; + +// const Establishment_TABLE = new FeatureLayer({ +// url: "https://gisgcc.chennaicorporation.gov.in/server/rest/services/Revenue/SurveyTables/FeatureServer/1" +// }) +// const Property_TABLE = new FeatureLayer({ +// url: 'https://gisgcc.chennaicorporation.gov.in/server/rest/services/Revenue/Buildings_Revenue_MIS/MapServer/5' + +// // url: "https://gisgcc.chennaicorporation.gov.in/server/rest/services/Revenue/SurveyTables/FeatureServer/4" +// }) +// const Professional_TABLE = new FeatureLayer({ +// url: "https://gisgcc.chennaicorporation.gov.in/server/rest/services/Revenue/SurveyTables/FeatureServer/3" +// }) +// const Floor_TABLE = new FeatureLayer({ +// url: "https://gisgcc.chennaicorporation.gov.in/server/rest/services/Revenue/SurveyTables/FeatureServer/2" +// }) + + + + +// const monitoredWidgetIds = ['widget_58', 'widget_59', 'widget_72', 'widget_73', 'widget_66', 'widget_67']; // Replace with actual IDs + + +// const [isVisible, setIsVisible] = useState(false); + + +// const checkMonitoredWidgets = (): boolean => { +// const widgetStates = getAppStore().getState().widgetsRuntimeInfo; +// return monitoredWidgetIds.some( +// (widgetId) => +// widgetStates[widgetId] && widgetStates[widgetId]?.state === 'OPENED' +// ); +// }; + + +// // Function to handle dependent widget visibility +// const updateDependentWidgetVisibility = (): void => { +// const isAnyWidgetOpen = checkMonitoredWidgets(); +// setIsVisible(isAnyWidgetOpen); +// }; + +// useEffect(() => { +// const storeUnsubscribe = getAppStore().subscribe(() => { +// updateDependentWidgetVisibility(); +// }); + +// return () => { +// storeUnsubscribe(); +// }; +// }, []); + +// useEffect(() => { +// let cleanup; + +// if (jimuMapView) { + +// jimuMapView.view.on('popup-open', () => { +// alert('popup open triggered') +// console.log('Popup has been opened!'); +// }); +// jimuMapView.view.on('popup-close', () => { +// console.log('Popup has been closed!'); +// }); + +// if (jimuMapView && !isVisible) { + +// // cleanup = handleMapClick(jimuMapView); +// fetchBuildingLyr() +// } +// } +// }, [jimuMapView]); + + + + +// const BuildDefinition_Usage = useSelector( +// (state: IMState) => state.widgetsState?.['widget_59']?.state?.building +// ); +// const BuildDefinition_Type = useSelector( +// (state: IMState) => state.widgetsState?.['widget_58']?.state?.building +// ); +// const BuildDefinition_RevenueFilter = useSelector( +// (state: IMState) => state.widgetsState?.['widget_67']?.state?.building +// ); +// const BuildDefinition_CommonQry = useSelector( +// (state: IMState) => state.widgetsState?.['widget_36']?.state?.building +// ); +// const BuildDefinition_floor = useSelector( +// (state: IMState) => state.widgetsState?.['widget_66']?.state?.building +// ); + +// useEffect(() => { +// if (BuildDefinition_Usage) { +// setBuildDefinition(BuildDefinition_Usage) +// buildingLayer.refresh() +// } +// }, [BuildDefinition_Usage]) + +// useEffect(() => { +// if (BuildDefinition_Type) { +// setBuildDefinition(BuildDefinition_Type) +// buildingLayer.refresh() +// } +// }, [BuildDefinition_Type]) + +// useEffect(() => { +// if (BuildDefinition_RevenueFilter) { +// setBuildDefinition(BuildDefinition_RevenueFilter) +// buildingLayer.refresh() +// } +// }, [BuildDefinition_RevenueFilter]) + +// useEffect(() => { +// if (BuildDefinition_CommonQry) { +// setBuildDefinition(BuildDefinition_CommonQry) +// buildingLayer.refresh() +// } +// }, [BuildDefinition_CommonQry]) +// useEffect(() => { +// if (BuildDefinition_floor) { +// setBuildDefinition(BuildDefinition_floor) +// buildingLayer.refresh() +// } +// }, [BuildDefinition_floor]) + + +// useEffect(() => { +// if (definitionSetActive) { +// if (buildingLayer.definitionExpression !== definitionExpr) { +// buildingLayer.when(() => { +// buildingLayer.defintionExpression = definitionExpr +// }) +// } +// } +// }, [definitionSetActive]) + + +// useEffect(() => { +// let watchHandle +// if (buildingLayer && jimuMapView) { +// jimuMapView.view.whenLayerView(buildingLayer).then((layerView) => { +// // Watch for layer view updates +// watchHandle = layerView.watch('updating', (isUpdating) => { +// if (isUpdating) { +// // Set loading icon when layer is updating +// if (!isLoading) { +// setIsLoading(true); +// } +// } else { +// // Add a small delay before removing the loading icon + +// clearTimeout(loadingTimeout); +// loadingTimeout = setTimeout(() => { +// setIsLoading(false); +// }, 300); +// } + +// // Update definition expression if required +// buildingLayer.when(() => { +// if (buildingLayer.definitionExpression !== definitionExpr) { +// buildingLayer.definitionExpression = definitionExpr; +// // console.log('Updated definitionExpression:', definitionExpr); +// } +// }); +// }); +// }); +// } + +// // Cleanup +// return () => { +// if (watchHandle) { +// watchHandle.remove(); +// } +// clearTimeout(loadingTimeout); +// }; + +// }, [definitionExpr]) + + + + +// useEffect(() => { +// isVisibleRef.current = isVisible; +// }, [isVisible]); + + + + +// // Function to fetch data from external API +// const fetchSystemPropertyUsage = async (gisId) => { +// let response = await fetchBillNoData_New(gisId); +// if (response && response.length > 0) { +// return response; +// } else { +// return null +// } +// }; +// async function queryRelatedRecords(table, whereClause, id) { + +// let query = table.createQuery() +// query.where = whereClause + +// // const url = BuildingLayeSourceURL + `/` + id + `/query`; // Update with your actual URL +// // const response = await fetch(`${url}?where=${whereClause}&outFields=*&f=json`); +// // const data = await response.json(); +// let data = await table.queryFeatures(query) +// if (data.features.length > 0) { +// return data.features.map(feature => feature.attributes); +// } + +// } +// async function fetchTradeDetails(gccGisId) { +// const relatedBills = await queryRelatedRecords(Establishment_TABLE, `new_formatted_gis_id = '${gccGisId}'`, 1); +// let allTradeDetails = [] +// if (relatedBills) { +// relatedBills.forEach(bill => { +// allTradeDetails.push(`Establishment Name: ${bill.establishment_name}, Trade Nature: ${bill.trade_nature_updated}`); +// }); +// } +// return allTradeDetails.length > 0 ? allTradeDetails.join('
') : "N/A."; +// } + +// async function fetchBillNumbers(gccGisId) { +// const relatedBills = await queryRelatedRecords(Property_TABLE, `new_formatted_gis_id = '${gccGisId}'`, 4); +// let allBills = []; +// if (relatedBills) { +// relatedBills.forEach(bill => { +// allBills.push(bill.bill_no); +// }); +// } +// return allBills.length > 0 ? allBills.join('
') : "N/A"; + +// } + +// async function fetchProfessionCount(gccGisId) { +// const relatedBills = await queryRelatedRecords(Professional_TABLE, `new_formatted_gis_id = '${gccGisId}'`, 1); +// let count +// if (relatedBills) { +// count = relatedBills.length; +// } + +// return count === undefined || count === 0 ? "N/A" : count; +// } + +// async function fetchMaxFloor(gccGisId) { +// const floors = await queryRelatedRecords(Floor_TABLE, `new_formatted_gis_id = '${gccGisId}'`, 2); +// let maxFloor +// if (floors) { +// maxFloor = Math.max(...floors.map(floor => floor.gcc_floor_id), 0); +// } +// return maxFloor && maxFloor > 0 ? `${maxFloor}th Floor` : "N/A"; +// } + +// async function fetchTradeCount(gccGisId) { +// const relatedBills = await queryRelatedRecords(Establishment_TABLE, `new_formatted_gis_id = '${gccGisId}'`, 1); +// let count +// if (relatedBills) { +// count = relatedBills.length; +// } +// return count === undefined || count === 0 ? "N/A" : count; +// } + +// async function fetchAssessmentCount(gccGisId) { +// const relatedBills = await queryRelatedRecords(Property_TABLE, `new_formatted_gis_id = '${gccGisId}'`, 4); +// return relatedBills != undefined && relatedBills.length > 0 ? relatedBills.length : "N/A"; +// } + + +// const fetchBuildingLyr = () => { +// if (jimuMapView) { +// const LandCoverLyr = jimuMapView.view.map.layers.find(layer => layer.title === 'LandCover') +// if (!LandCoverLyr) { +// console.log('LandCover Layer not found in the web map') +// } else { +// // const buildingLyr_qryLyr = LandCoverLyr.layers.find(layer => layer.title === 'Buildings Revenue MIS') +// // const buildingLyr = LandCoverLyr.layers.find(layer => layer.title === 'Surveyed Buildings') +// // const titles = ['buildings', 'current balance', 'arrear balance']; + +// // const buildingLyr_Final = LandCoverLyr.layers.find( +// // layer => titles.includes(layer.title.toLowerCase()) +// // ); + +// const buildingLyr_Final = LandCoverLyr.layers.find(layer => layer.title === 'Buildings') + +// if (buildingLyr_Final) { +// storeBuildingLayer(buildingLyr_Final) +// // setBuildDefinition(buildingLyr_Final.definitionExpression) + +// } else { +// storeBuildingLayer(null) +// console.log('Buildings sublayer not found') +// } +// } +// } +// } + + +// const handleMapClick = (jmv) => { +// try { +// if (jmv) { + + +// // let clickHandle = jmv.view.whenLayerView(buildingLayer).then((layerView) => { +// // if (!layerView || !layerView.hasAllFeatures) { +// // console.warn("LayerView not ready for client-side queries."); +// // return; +// // } + +// // let clickHandle = jmv.view.on('click', async (evt) => { +// // jmv.view.whenLayerView(buildingLayer).then((layerView) => { +// jmv.view.on('click', async (evt) => { + + + + + + +// // if (isVisibleRef.current) { +// // evt.preventDefault(); +// // evt.stopPropagation(); +// // return; +// // } + +// const response = await jmv.view.hitTest(evt); +// if (response.results.length > 0) { +// // const buildingsLayerResult = response.results.find(result => result.layer && result.layer.title === 'Buildings'); +// // const buildingsLayerResult = response.results.find(result => result.graphic.layer && result.graphic.layer.title === 'Buildings'); +// if (buildingLayer) { +// const buildingsLayerResult = response.results.find(result => result.layer && result.layer.title === 'Buildings'); +// // const clickedFeature = response.results[0].graphic; + +// // const targetLayer = buildingLayer// buildingsLayerResult.layer; +// // let definition = targetLayer.definitionExpression +// // setBuildDefinition(definition) +// // storeBuildingLayer(targetLayer) +// if (buildingsLayerResult) { + + + +// let definition = buildingLayer.definitionExpression +// setBuildDefinition(definition) + +// const objectId = buildingsLayerResult.graphic.attributes.objectid;// buildingLayer.objectIdField// +// const query = buildingLayer.createQuery(); +// query.objectIds = [objectId]; +// query.outFields = ['*']; +// const queryResponse = await buildingLayer.queryFeatures(query); + +// if (queryResponse.features.length > 0) { +// const fullFeature = queryResponse.features[0]; +// const gcc_gis_id = fullFeature.attributes.gcc_gis_id; +// if (gcc_gis_id) { +// storeclickedFeatureDet([fullFeature.attributes, evt.mapPoint]) + + +// // const defaultContent = buldingPopupContent +// // const popupTemplate = { +// // title: 'Building Info', +// // content: async () => { + +// // let customContent = await buildCustomContent(oldGisId); + +// // return ` +// //
${defaultContent}
+// //
${customContent}
+// // `; +// // } +// // }; + +// // buildingsLayerResult.graphic.layer.popupTemplate = popupTemplate; + +// // jimuMapView.view.popup.open({ +// // features: [clickedFeature], +// // location: evt.mapPoint, +// // fetchFeatures: true +// // }); +// } +// } +// } +// } +// } +// else +// return +// }); + +// // return () => clickHandle.remove(); // Cleanup function to remove the event listener +// // }) +// } + +// } catch (err) { +// console.error('HitTest error:', err); +// } + + +// }; + +// useEffect(() => { +// let cleanup; +// if (buildingLayer) { +// cleanup = handleMapClick(jimuMapView); +// } +// }, [buildingLayer]) + +// useEffect(() => { +// // if (buildingLayer && definitionExpr) +// // buildingLayer.definitionExpression = definitionExpr +// }, [buildingLayer]) + + + + + +// useEffect(() => { +// if (Array.isArray(clickedFeatDet) && clickedFeatDet.length > 0) { +// // const defaultContent = buldingPopupContent + + +// const popupTemplate = { +// title: 'Building Info', +// content: async () => { + +// let customContent = await buildCustomContent(clickedFeatDet); + +// setTimeout(() => { +// document.querySelectorAll('.show-trade-button').forEach(button => { +// button.addEventListener('click', async (e) => { +// const billNo = e.target.getAttribute('data-billno'); +// const detailsRow = document.querySelector(`.trade-details-row[data-billno="${billNo}"]`); +// const detailsCell = detailsRow?.querySelector('.trade-details-cell'); + +// // Prevent multiple appends +// if (detailsRow && detailsCell && detailsCell.innerHTML.trim() === '') { +// const tradeHTML = await getTradeAssessmentHTML(billNo); +// detailsCell.innerHTML = tradeHTML; +// detailsRow.style.display = 'table-row'; +// } +// }); +// }); +// }, 0); + +// return ` +//

Note: The below property details were from GIS.

+//
${customContent}
+ +// `; +// }, +// actions: [], +// }; +// buildingLayer.popupTemplate = popupTemplate; +// // if (definitionExpr) { +// // buildingLayer.definitionExpression = definitionExpr +// // } +// } +// }, [clickedFeatDet]) + + +// async function getPopupContent(buildingLayerSource) { +// const popupTemplate = buildingLayerSource.graphic.layer.popupTemplate; +// storeBuildingPopup(popupTemplate) +// // If a popupTemplate exists, create content based on it +// if (popupTemplate) { +// const content = await popupTemplate.content +// // const content = await popupTemplate.content.map(async (item) => { +// // // Handle any text or html type content +// // if (typeof item === "string") { +// // return item; +// // } else if (item.type === "text") { +// // return `

${item.text}

`; +// // } +// // // You can handle other types (e.g., fields, media) as needed here +// // return ''; +// // }); + +// return content//.join(''); // Join all parts to form the complete content +// } + + +// } +// function generateDirectionUrl(feature) { +// const { x, y, spatialReference } = feature; + +// let latitude, longitude; +// if (spatialReference.wkid === 4326) { +// latitude = y; +// longitude = x; +// } else if (spatialReference.wkid === 102100) { +// // Conversion from Web Mercator to Lat/Lon +// const auxSphereToLatLon = (x, y) => { +// const rMajor = 6378137; +// const shift = Math.PI * rMajor; +// longitude = (x / shift) * 180.0; +// latitude = (y / shift) * 180.0; +// latitude = (180 / Math.PI) * (2 * Math.atan(Math.exp(latitude * Math.PI / 180.0)) - Math.PI / 2.0); +// }; +// auxSphereToLatLon(x, y); +// } +// return "https://www.google.com/maps?q=" +(latitude) + "," +(longitude); +// // return `http://www.google.com/maps/dir/?api=1&destination=${latitude},${longitude}`; +// } + +// const test = async (params: { Zone: string; ward: string; area: string; loc: string; street: string; GISID: string }) => { +// try { +// // Define the API endpoint +// const newAPIEndPoint = "https://erp.chennaicorporation.gov.in/pgr/external/mobileservice?serviceId=common_api&jsonResp=Yes&subService=getPropertyByGis"; + +// // Convert params to query string +// const queryString = new URLSearchParams(params).toString(); +// const requestUrl = `${newAPIEndPoint}&${queryString}`; + +// // Fetch the data using fetch +// const response = await fetch(requestUrl, { +// method: "GET", +// headers: { +// "Content-Type": "application/json", +// }, +// }); + +// if (!response.ok) { +// throw new Error(`HTTP error! Status: ${response.status}`); +// } + +// const data = await response.json(); + +// // Check if data contains valid results +// if (data && data.ListResult && Object.keys(data.ListResult).length > 0) { +// return data.ListResult; // Return the full result +// } + +// return []; // Return an empty array if no matching results +// } catch (err) { +// console.error("Error fetching data:", err); +// return []; +// } +// }; + + + +// const fetchGISIDFetch_new_erp = async (params: { +// Zone: string; +// ward: string; +// area?: string | null; +// loc?: string | null; +// street?: string | null; +// GISID?: string | null; +// }): Promise => { +// try { +// const baseUrl = +// "https://erp.chennaicorporation.gov.in/pgr/external/mobileservice?serviceId=common_api&jsonResp=Yes&subService=getPropertyByGis"; + +// const { GISID, ...queryParams } = params; + +// // Ensure Zone and ward are always included, remove any other null/undefined values +// const filteredParams = Object.fromEntries( +// Object.entries(queryParams).filter( +// ([key, value]) => +// (key === "Zone" || key === "ward") || (value !== null && value !== undefined && value !== "") +// ) +// ); + +// const queryString = new URLSearchParams(filteredParams as Record).toString(); +// const requestUrl = `${baseUrl}&${queryString}`; + +// console.log("Fetching data from:", requestUrl); + +// const controller = new AbortController(); +// const timeout = setTimeout(() => controller.abort(), 90000); // 10 seconds timeout + +// const response = await fetch(requestUrl, { +// method: "GET", +// signal: controller.signal, +// }); + +// clearTimeout(timeout); + +// if (!response.ok) { +// throw new Error(`HTTP Error: ${response.status} ${response.statusText}`); +// } + +// const result = await response.json(); + +// if (!result?.ListResult || !Array.isArray(result.ListResult)) { +// return { error: "Invalid API response structure" }; +// } + +// if (result.ListResult.length === 0) { +// return { error: "No records found" }; +// } + +// // Ensure proper type comparison and collect all matches +// const matchingRecords = result.ListResult.filter( +// (item: any) => String(item.GISID).trim() === String(GISID).trim() +// ); + +// return matchingRecords.length > 0 ? matchingRecords : { error: "GISID not found in results" }; +// } catch (error: any) { +// if (error.name === "AbortError") { +// return { error: "Request timed out" }; +// } +// console.error("Error fetching data:", error.message); +// return { error: error.message }; +// } +// }; + +// async function fetchTradeLicenseDetails(billNumber, clickedFeatDet) { +// try { +// // Normalize zone/ward (ensure leading 'N' and padding) +// const rawZone = (clickedFeatDet?.[0]?.zone_number_mis || clickedFeatDet?.[0]?.zone_number || '').toString(); +// const rawWard = (clickedFeatDet?.[0]?.ward_number_mis || clickedFeatDet?.[0]?.ward_number || '').toString(); + +// const zone = rawZone.startsWith('N') ? rawZone : 'N' + rawZone.padStart(2, '0'); +// const ward = rawWard.startsWith('N') ? rawWard : 'N' + rawWard.padStart(3, '0'); + +// // First get prof/trade data for this bill +// const profResp = await fetch( +// `https://erp.chennaicorporation.gov.in/pgr/external/mobileservice?serviceId=common_api&jsonResp=Yes&subService=getProfTradedata&propid=${encodeURIComponent(billNumber)}` +// ); +// const profData = await profResp.json(); + +// if (!profData?.ListResult || !Array.isArray(profData.ListResult)) { +// return `
No professional/trade data available for bill ${billNumber}
`; +// } + +// // filter only trade license items +// const tradeItems = profData.ListResult.filter(it => it.SOURCE === "TRADELICENSE"); +// const profTaxItems = profData.ListResult.filter(it => it.SOURCE === "PROFTAX"); +// console.log("profItems",profTaxItems) + +// // Track PTNAN used in trade licenses +// let collectedPTNAN = new Set(); +// if (tradeItems.length === 0) { +// return `
No trade-license items found for bill ${billNumber}
`; +// } + +// let html = `
Trade License lookups (zone: ${zone}, ward: ${ward}):
`; + +// // Query getTradeLicensedata for each trade item +// for (const item of tradeItems) { +// const licenseNumber = +// item.LICENSE_NUMBER || +// item.licenseNumber || +// item.LICENSE_NO || +// item.LICENCE_NUMBER || +// item.licence_no || +// item.ID; + +// const url = `https://erp.chennaicorporation.gov.in/pgr/external/mobileservice?serviceId=common_api&jsonResp=Yes&subService=getTradeLicensedata&zone=${encodeURIComponent(zone)}&ward=${encodeURIComponent(ward)}&licenseNumber=${encodeURIComponent(licenseNumber)}`; + +// try { +// const resp = await fetch(url, { method: 'GET' }); +// const json = await resp.json(); + +// html += `
`; +// html += `
Trade licenseNumber: ${licenseNumber}
`; + +// if (json?.ListResult && Array.isArray(json.ListResult) && json.ListResult.length > 0) { +// for (const lic of json.ListResult) { +// // ---- Trade License Details ---- +// html += ` +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
Ward / Zone${lic.WARD} / ${lic.ZONE}
Trade License No${lic.LICENSE_NUMBER}
PT Number${lic.PTNAN}
Establishment${lic.ESTABLISHMENT_NAME}
Trade Nature${lic.TRADE_NAME}
Applicant${lic.APPLICANT_NAME} ${lic.FATHER_NAME ? `(S/o: ${lic.FATHER_NAME})` : ''}
Address${lic.APPLICANT_ADDRESS}
Expiry Date${lic.LIC_EXPIRY_DATE?.split(" ")[0]}
Total Fee₹${lic.TOTAL_FEE}
+// `; + +// // ---- Professional Tax Details ---- +// if (lic.PTNAN) { +// collectedPTNAN.add(lic.PTNAN); +// try { +// const ptUrl = `https://erp.chennaicorporation.gov.in/pgr/external/mobileservice?serviceId=common_api&jsonResp=Yes&subService=getProfessionalTaxData&zone=${encodeURIComponent(lic.ZONE)}&ward=${encodeURIComponent(lic.WARD)}&ptnan=${encodeURIComponent(lic.PTNAN)}`; +// const ptResp = await fetch(ptUrl, { method: "GET" }); +// const ptJson = await ptResp.json(); + +// if (ptJson?.ListResult && Array.isArray(ptJson.ListResult) && ptJson.ListResult.length > 0) { +// for (const pt of ptJson.ListResult) { +// html += ` +//
Professional Tax Details
+// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
PTNAN${pt.PTNAN}
Name${pt.NAME}
Area${pt.AREA}
Address${pt.ADDRESS}
Locality${pt.LOCALITY || "N/A"}
Street${pt.STREET}
Ward / Zone${pt.WARD} / ${pt.ZONE}
Demand₹${pt.DEMAND}
Property Tax No${pt.PROPERTYTAX_NUMBER}
Contact${pt.CONTACT_NUMBER || "N/A"}
Created Date${pt.CREATEDDATE}
+// `; +// } +// } else { +// html += `
No Professional Tax data found for PTNAN ${lic.PTNAN}
`; +// } +// } catch (err) { +// console.error("Error fetching professional tax data", err); +// html += `
Error fetching Professional Tax for PTNAN ${lic.PTNAN}
`; +// } +// } +// } +// } else if (json?.ResultStatus === false && json?.Message) { +// html += `
API returned: ${json.Message}
`; +// } else { +// html += `
No records found for licenseNumber ${licenseNumber}
`; +// } + +// html += `
`; +// } catch (err) { +// console.error('Error fetching trade license data', err); +// html += `
Error fetching trade license for ${licenseNumber}
`; +// } +// } +// // console.log("collectedPTNAN",collectedPTNAN) +// // ✅ SHOW NOT LINKED PROFTAX AT END ONLY +// let remainingPT = profTaxItems.filter(pt => !collectedPTNAN.has(pt.ID)); + +// // ✅ REMAINING PROFTAX → FETCH FULL DETAILS USING API 3 +// // let remainingPT = profTaxItems.filter(pt => !collectedPTNAN.has(pt.ID)); + +// if (remainingPT.length > 0) { +// html += ` +//
+// Other Professional Tax +//
+// `; + +// for (const ptItem of remainingPT) { +// try { +// const ptId = ptItem.ID; + +// const ptUrl = `https://erp.chennaicorporation.gov.in/pgr/external/mobileservice?serviceId=common_api&jsonResp=Yes&subService=getProfessionalTaxData&zone=${encodeURIComponent(zone)}&ward=${encodeURIComponent(ward)}&ptnan=${encodeURIComponent(ptId)}`; + +// const ptResp = await fetch(ptUrl, { method: "GET" }); +// const ptJson = await ptResp.json(); + +// if (ptJson?.ListResult && ptJson.ListResult.length > 0) { +// const pt = ptJson.ListResult[0]; + +// html += ` +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
PTNAN${pt.PTNAN}
Name${pt.NAME}
Area${pt.AREA}
Address${pt.ADDRESS}
Street${pt.STREET}
Ward / Zone${pt.WARD} / ${pt.ZONE}
Demand₹${pt.DEMAND}
Property Tax No${pt.PROPERTYTAX_NUMBER}
+// `; +// } else { +// html += `
No PT data found for ${ptId}
`; +// } +// } catch (err) { +// console.error("Error fetching remaining PT", err); +// html += `
Error fetching PT for ${ptItem.ID}
`; +// } +// } +// } + +// return html; +// } catch (err) { +// console.error('fetchTradeLicenseDetails error', err); +// return `
Error while fetching trade license details
`; +// } +// } + +// async function fetchProfessionalTaxDetails(ptnan, zone, ward) { +// try { +// const url = `https://erp.chennaicorporation.gov.in/pgr/external/mobileservice?serviceId=common_api&jsonResp=Yes&subService=getProfessionalTaxData&zone=${encodeURIComponent(zone)}&ward=${encodeURIComponent(ward)}&ptnan=${encodeURIComponent(ptnan)}`; + +// const resp = await fetch(url, { method: "GET" }); +// const json = await resp.json(); + +// let html = `
Professional Tax Details (zone: ${zone}, ward: ${ward}, PTNAN: ${ptnan}):
`; + +// if (json?.ListResult && Array.isArray(json.ListResult) && json.ListResult.length > 0) { +// for (const item of json.ListResult) { +// html += ` +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
PTNAN${item.PTNAN}
Name${item.NAME}
Area${item.AREA}
Address${item.ADDRESS}
Locality${item.LOCALITY || "N/A"}
Street${item.STREET}
Ward / Zone${item.WARD} / ${item.ZONE}
Demand₹${item.DEMAND}
Property Tax No${item.PROPERTYTAX_NUMBER}
Contact${item.CONTACT_NUMBER || "N/A"}
Created Date${item.CREATEDDATE}
+// `; +// } +// } else if (json?.Message) { +// html += `
API returned: ${json.Message}
`; +// } else { +// html += `
No Professional Tax records found for PTNAN ${ptnan}
`; +// } + +// return html; +// } catch (err) { +// console.error("Error fetching professional tax data", err); +// return `
Error fetching Professional Tax details for PTNAN ${ptnan}
`; +// } +// } + + + +// async function getTradeAssessmentHTML(billNumber) { +// let html = ''; + +// try { +// const profTradeResponse = await fetch( +// `https://erp.chennaicorporation.gov.in/pgr/external/mobileservice?serviceId=common_api&jsonResp=Yes&subService=getProfTradedata&propid=${billNumber}` +// ); +// const profData = await profTradeResponse.json(); + +// if (profData?.ResultStatus === true && Array.isArray(profData.ListResult)) { +// const tradeItems = profData.ListResult.filter(item => item.SOURCE === "TRADELICENSE"); +// const profItems = profData.ListResult.filter(item => item.SOURCE === "PROFTAX"); + + +// } else { +// // html += `
No Professional Trade Assessment data found.
`; +// } +// } catch (err) { +// html += `
Error fetching trade data
`; +// } + +// return html; +// } + + +// async function buildCustomContent(clickedFeatDet) { +// console.log(clickedFeatDet) +// // const billNumbers = await fetchBillNumbers(clickedFeatDet[0].gcc_gis_id); +// // const tradeDetails = await fetchTradeDetails(clickedFeatDet[0].gcc_gis_id); +// // const assessmentCount = await fetchAssessmentCount(clickedFeatDet[0].gcc_gis_id); +// const directionUrl = await generateDirectionUrl(clickedFeatDet[1]); +// // const geocode=getGoogleMapsUrl(clickedFeatDet[1]) +// // const tradeCount = await fetchTradeCount(clickedFeatDet[0].gcc_gis_id); +// // const professionCount = await fetchProfessionCount(clickedFeatDet[0].gcc_gis_id); +// // const maxFloor = await fetchMaxFloor(clickedFeatDet[0].gcc_gis_id); + + + +// // const new_api = await fetchGISIDFetch_new_erp({ +// // Zone: "N02", +// // ward: "N020", +// // area: "MANALI", +// // loc: "VIMALAPURAM", +// // street: "VENUGOPAL STREET", +// // GISID: "G-020-23-01149" +// // }); + + +// const zoneNumber = clickedFeatDet[0].zone_number; +// const formattedZone = "N" + zoneNumber; + +// const wardNumber = clickedFeatDet[0].ward_number; +// const formattedward = "N" + wardNumber; +// const new_api = await fetchGISIDFetch_new_erp({ +// Zone: clickedFeatDet[0].zone_number_mis || formattedZone, +// ward: clickedFeatDet[0].ward_number_mis || formattedward , +// area: clickedFeatDet[0].area_mis, +// loc: clickedFeatDet[0].locality_mis, +// street: clickedFeatDet[0].street_mis, +// GISID: clickedFeatDet[0].gcc_gis_id +// }); +// console.log(new_api); + +// const usage = await fetchSystemPropertyUsage(clickedFeatDet[0].gcc_gis_id); + +// let customContent = ` +// +// +// +// +// +// +// +// + + +//
Zone Number${clickedFeatDet[0].zone_number}
Ward Number${clickedFeatDet[0].ward_number}
Area${clickedFeatDet[0].area_name}
Locality${clickedFeatDet[0].locality}
Road Name${clickedFeatDet[0].road_name}
GIS Id${clickedFeatDet[0].gcc_gis_id}
Locate In MapLocate In Map
+ + + +// `; + +// if (new_api) { +// if (new_api.error) { +// let errorMessage = new_api.error === "Request timed out" +// ? "The ERP API encountered a timeout error." +// : `Error: ${new_api.error}`; + +// customContent += `

${errorMessage}

`; +// } else { +// // Ensure `new_api` is always treated as an array +// const records = Array.isArray(new_api) ? new_api : [new_api]; + +// customContent += `
`; + + +// customContent += ` +//

+// Note: The below property details were fetched from ERP API. +//

+// `; + +// for (const record of records) { +// const billNumber = record.BILL_NUMBER || 'N/A'; + +// customContent += ` +//
+// +// Bill Number: ${billNumber} +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
Bill No:${billNumber}
Active Status:${record.ACTIVE_STATUS || 'N/A'}
Zone:${record.ZONE || 'N/A'}
Ward:${record.WARD || 'N/A'}
Area:${record.AREA || 'N/A'}
Locality:${record.LOCALITY || 'N/A'}
Road Name:${record.STREET || 'N/A'}
House No:${record.HOUSENO || 'N/A'}
Old House No:${record.OLD_HOUSENO || 'N/A'}
Owner Name:${record.OWNER_NAME || 'N/A'}
Property Type:${record.PROPERTY_TYPE || 'N/A'}
Property Usage:${record.USG_NAME || 'N/A'}
Total Built-up Area:${record.TOTAL_BUILTUP_AREA || 'N/A'}
Sital Area:${record.SITAL_AREA || 'N/A'}
Total Demand:${record.TOTAL_DEMAND || 'N/A'}
Total Collection:${record.TOTAL_COLLECTION || 'N/A'}
Total Balance:${record.TOTAL_BALANCE || 'N/A'}
Current Demand:${record.CURRENT_DEMAND || 'N/A'}
Current Collection:${record.CURRENT_COLL || 'N/A'}
Current Balance:${record.CURRENT_BALANCE || 'N/A'}
Arrear Demand:${record.ARREAR_DEMAND || 'N/A'}
Arrear Collection:${record.ARREAR_COLL || 'N/A'}
Arrear Balance:${record.ARREAR_BALANCE || 'N/A'}
Mobile No:${record.MOBILE_NO || 'N/A'}
+// `; + +// if (billNumber !== 'N/A') { +// const tradeHTML = await getTradeAssessmentHTML(billNumber); +// customContent += tradeHTML; +// } +// // NEW: fetch trade license details using zone/ward from clickedFeatDet and license (or ID fallback) +// const tradeLicenseHTML = await fetchTradeLicenseDetails(billNumber, clickedFeatDet); +// customContent += tradeLicenseHTML; + + +// customContent += `
`; +// } + +// customContent += `
`; +// } +// } else { +// customContent += `

No records found

`; +// } + +// return customContent; +// } + +// const clearGraphic = () => { +// jimuMapView?.view.graphics.removeAll(); +// setPopupContent(null); + +// }; +// const handleMapView = (view) => { +// setJimuMapView(view) +// setMapView(view) +// // if (!jimuMapView && view) { + +// // let dispatchItem = { 'view': view } + +// // getAppStore().dispatch( +// // appActions.widgetStatePropChange(props.id, 'sharedData.queryString', dispatchItem) +// // ); +// // alert('dispatched item') +// // } + + +// } +// return ( +//
+// {/* JimuMapViewComponent to integrate map in Experience Builder */} + +// {props.useMapWidgetIds && props.useMapWidgetIds.length === 1 && ( +// + +// )} + + +//
+// ); +// }; + +// export default Popup; + +import React, { useEffect, useState, useRef } from 'react'; +import { JimuMapViewComponent, JimuMapView } from 'jimu-arcgis'; +import { fetchGISIDFetch, fetchBillNoData_New } from '../../../utils/queries'; +import './custom-popup.css'; +import FeatureLayer from '@arcgis/core/layers/FeatureLayer' +import { useMapView } from '../../../utils/mapviewservice'; // Import the context +import { AllWidgetProps, appActions, getAppStore, IMState } from 'jimu-core'; +import { useSelector } from 'react-redux'; +import CustomContent from '@arcgis/core/popup/content/CustomContent'; + +const Popup = (props: AllWidgetProps) => { + const [jimuMapView, setJimuMapView] = useState(null); + const [popupContent, setPopupContent] = useState(null); + const [clickHandle, setClickHandle] = useState(null); // Store event handle + const [buildingPopup, storeBuildingPopup] = useState(null); + const [buldingPopupContent, storePopupContent_Building] = useState(null) + const [buildingLayer, storeBuildingLayer] = useState(null) + const [clickedFeatDet, storeclickedFeatureDet] = useState(null) + const isVisibleRef = useRef(false); + const [definitionExpr, setBuildDefinition] = useState(null) + const [definitionSetActive, flag_definitionSet] = useState(null) + const [isLoading, setIsLoading] = useState(null) + const { setMapView } = useMapView(); + let loadingTimeout; + + const Establishment_TABLE = new FeatureLayer({ + url: "https://gisgcc.chennaicorporation.gov.in/server/rest/services/Revenue/SurveyTables/FeatureServer/1" + }) + const Property_TABLE = new FeatureLayer({ + url: 'https://gisgcc.chennaicorporation.gov.in/server/rest/services/Revenue/Buildings_Revenue_MIS/MapServer/5' + }) + const Professional_TABLE = new FeatureLayer({ + url: "https://gisgcc.chennaicorporation.gov.in/server/rest/services/Revenue/SurveyTables/FeatureServer/3" + }) + const Floor_TABLE = new FeatureLayer({ + url: "https://gisgcc.chennaicorporation.gov.in/server/rest/services/Revenue/SurveyTables/FeatureServer/2" + }) + + const monitoredWidgetIds = ['widget_58', 'widget_59', 'widget_72', 'widget_73', 'widget_66', 'widget_67']; + + const [isVisible, setIsVisible] = useState(false); + + const checkMonitoredWidgets = (): boolean => { + const widgetStates = getAppStore().getState().widgetsRuntimeInfo; + return monitoredWidgetIds.some( + (widgetId) => + widgetStates[widgetId] && widgetStates[widgetId]?.state === 'OPENED' + ); + }; + + const updateDependentWidgetVisibility = (): void => { + const isAnyWidgetOpen = checkMonitoredWidgets(); + setIsVisible(isAnyWidgetOpen); + }; + + useEffect(() => { + const storeUnsubscribe = getAppStore().subscribe(() => { + updateDependentWidgetVisibility(); + }); + + return () => { + storeUnsubscribe(); + }; + }, []); + + useEffect(() => { + let cleanup; + + if (jimuMapView) { + + jimuMapView.view.on('popup-open', () => { + alert('popup open triggered') + console.log('Popup has been opened!'); + }); + jimuMapView.view.on('popup-close', () => { + console.log('Popup has been closed!'); + }); + + if (jimuMapView && !isVisible) { + fetchBuildingLyr() + } + } + }, [jimuMapView]); + + const BuildDefinition_Usage = useSelector( + (state: IMState) => state.widgetsState?.['widget_59']?.state?.building + ); + const BuildDefinition_Type = useSelector( + (state: IMState) => state.widgetsState?.['widget_58']?.state?.building + ); + const BuildDefinition_RevenueFilter = useSelector( + (state: IMState) => state.widgetsState?.['widget_67']?.state?.building + ); + const BuildDefinition_CommonQry = useSelector( + (state: IMState) => state.widgetsState?.['widget_36']?.state?.building + ); + const BuildDefinition_floor = useSelector( + (state: IMState) => state.widgetsState?.['widget_66']?.state?.building + ); + + useEffect(() => { + if (BuildDefinition_Usage) { + setBuildDefinition(BuildDefinition_Usage) + buildingLayer.refresh() + } + }, [BuildDefinition_Usage]) + + useEffect(() => { + if (BuildDefinition_Type) { + setBuildDefinition(BuildDefinition_Type) + buildingLayer.refresh() + } + }, [BuildDefinition_Type]) + + useEffect(() => { + if (BuildDefinition_RevenueFilter) { + setBuildDefinition(BuildDefinition_RevenueFilter) + buildingLayer.refresh() + } + }, [BuildDefinition_RevenueFilter]) + + useEffect(() => { + if (BuildDefinition_CommonQry) { + setBuildDefinition(BuildDefinition_CommonQry) + buildingLayer.refresh() + } + }, [BuildDefinition_CommonQry]) + useEffect(() => { + if (BuildDefinition_floor) { + setBuildDefinition(BuildDefinition_floor) + buildingLayer.refresh() + } + }, [BuildDefinition_floor]) + + useEffect(() => { + if (definitionSetActive) { + if (buildingLayer.definitionExpression !== definitionExpr) { + buildingLayer.when(() => { + buildingLayer.defintionExpression = definitionExpr + }) + } + } + }, [definitionSetActive]) + + useEffect(() => { + let watchHandle + if (buildingLayer && jimuMapView) { + jimuMapView.view.whenLayerView(buildingLayer).then((layerView) => { + watchHandle = layerView.watch('updating', (isUpdating) => { + if (isUpdating) { + if (!isLoading) { + setIsLoading(true); + } + } else { + clearTimeout(loadingTimeout); + loadingTimeout = setTimeout(() => { + setIsLoading(false); + }, 300); + } + + buildingLayer.when(() => { + if (buildingLayer.definitionExpression !== definitionExpr) { + buildingLayer.definitionExpression = definitionExpr; + } + }); + }); + }); + } + + return () => { + if (watchHandle) { + watchHandle.remove(); + } + clearTimeout(loadingTimeout); + }; + + }, [definitionExpr]) + + useEffect(() => { + isVisibleRef.current = isVisible; + }, [isVisible]); + + const fetchSystemPropertyUsage = async (gisId) => { + let response = await fetchBillNoData_New(gisId); + if (response && response.length > 0) { + return response; + } else { + return null + } + }; + async function queryRelatedRecords(table, whereClause, id) { + let query = table.createQuery() + query.where = whereClause + let data = await table.queryFeatures(query) + if (data.features.length > 0) { + return data.features.map(feature => feature.attributes); + } + } + async function fetchTradeDetails(gccGisId) { + const relatedBills = await queryRelatedRecords(Establishment_TABLE, `new_formatted_gis_id = '${gccGisId}'`, 1); + let allTradeDetails = [] + if (relatedBills) { + relatedBills.forEach(bill => { + allTradeDetails.push(`Establishment Name: ${bill.establishment_name}, Trade Nature: ${bill.trade_nature_updated}`); + }); + } + return allTradeDetails.length > 0 ? allTradeDetails.join('
') : "N/A."; + } + + async function fetchBillNumbers(gccGisId) { + const relatedBills = await queryRelatedRecords(Property_TABLE, `new_formatted_gis_id = '${gccGisId}'`, 4); + let allBills = []; + if (relatedBills) { + relatedBills.forEach(bill => { + allBills.push(bill.bill_no); + }); + } + return allBills.length > 0 ? allBills.join('
') : "N/A"; + } + + async function fetchProfessionCount(gccGisId) { + const relatedBills = await queryRelatedRecords(Professional_TABLE, `new_formatted_gis_id = '${gccGisId}'`, 1); + let count + if (relatedBills) { + count = relatedBills.length; + } + return count === undefined || count === 0 ? "N/A" : count; + } + + async function fetchMaxFloor(gccGisId) { + const floors = await queryRelatedRecords(Floor_TABLE, `new_formatted_gis_id = '${gccGisId}'`, 2); + let maxFloor + if (floors) { + maxFloor = Math.max(...floors.map(floor => floor.gcc_floor_id), 0); + } + return maxFloor && maxFloor > 0 ? `${maxFloor}th Floor` : "N/A"; + } + + async function fetchTradeCount(gccGisId) { + const relatedBills = await queryRelatedRecords(Establishment_TABLE, `new_formatted_gis_id = '${gccGisId}'`, 1); + let count + if (relatedBills) { + count = relatedBills.length; + } + return count === undefined || count === 0 ? "N/A" : count; + } + + async function fetchAssessmentCount(gccGisId) { + const relatedBills = await queryRelatedRecords(Property_TABLE, `new_formatted_gis_id = '${gccGisId}'`, 4); + return relatedBills != undefined && relatedBills.length > 0 ? relatedBills.length : "N/A"; + } + + const fetchBuildingLyr = () => { + if (jimuMapView) { + const LandCoverLyr = jimuMapView.view.map.layers.find(layer => layer.title === 'LandCover') + if (!LandCoverLyr) { + console.log('LandCover Layer not found in the web map') + } else { + const buildingLyr_Final = LandCoverLyr.layers.find(layer => layer.title === 'Buildings') + + if (buildingLyr_Final) { + storeBuildingLayer(buildingLyr_Final) + } else { + storeBuildingLayer(null) + console.log('Buildings sublayer not found') + } + } + } + } + + const handleMapClick = (jmv) => { + try { + if (jmv) { + jmv.view.on('click', async (evt) => { + const response = await jmv.view.hitTest(evt); + if (response.results.length > 0) { + if (buildingLayer) { + const buildingsLayerResult = response.results.find(result => result.layer && result.layer.title === 'Buildings'); + if (buildingsLayerResult) { + let definition = buildingLayer.definitionExpression + setBuildDefinition(definition) + + const objectId = buildingsLayerResult.graphic.attributes.objectid; + const query = buildingLayer.createQuery(); + query.objectIds = [objectId]; + query.outFields = ['*']; + const queryResponse = await buildingLayer.queryFeatures(query); + + if (queryResponse.features.length > 0) { + const fullFeature = queryResponse.features[0]; + const gcc_gis_id = fullFeature.attributes.gcc_gis_id; + if (gcc_gis_id) { + storeclickedFeatureDet([fullFeature.attributes, evt.mapPoint]) + } + } + } + } + } + else + return + }); + } + } catch (err) { + console.error('HitTest error:', err); + } + }; + + useEffect(() => { + let cleanup; + if (buildingLayer) { + cleanup = handleMapClick(jimuMapView); + } + }, [buildingLayer]) + + useEffect(() => { + }, [buildingLayer]) + + useEffect(() => { + if (Array.isArray(clickedFeatDet) && clickedFeatDet.length > 0) { + + const popupTemplate = { + + title: 'Building Info', + + content: [ + + new CustomContent({ + + outFields: ['*'], + + creator: async () => { + + const container = + document.createElement('div'); + + // ========================================== + // EXISTING BUILDING CONTENT + // ========================================== + + const existingContent = + await buildCustomContent(clickedFeatDet); + + const existingDiv = + document.createElement('div'); + + existingDiv.innerHTML = + existingContent; + + container.appendChild(existingDiv); + + // ========================================== + // BILL SEARCH CONTROL + // ========================================== + + const zoneNumber = clickedFeatDet[0].zone_number_mis || clickedFeatDet[0].zone_number; + const wardNumber = clickedFeatDet[0].ward_number_mis || clickedFeatDet[0].ward_number; + const area = clickedFeatDet[0].area_mis || clickedFeatDet[0].area_name; + const locality = clickedFeatDet[0].locality_mis || clickedFeatDet[0].locality; + const roadName = clickedFeatDet[0].street_mis || clickedFeatDet[0].road_name; + const gisId = clickedFeatDet[0].gcc_gis_id; + + const searchControl = + createBillSearchControl(zoneNumber, wardNumber, area, locality, roadName, gisId); + + container.appendChild(searchControl); + + return container; + } + + }) + + ], + actions: [] + }; + + buildingLayer.popupTemplate = + popupTemplate; + + } + + }, [clickedFeatDet]); + + async function getPopupContent(buildingLayerSource) { + const popupTemplate = buildingLayerSource.graphic.layer.popupTemplate; + storeBuildingPopup(popupTemplate) + if (popupTemplate) { + const content = await popupTemplate.content + return content + } + } + function generateDirectionUrl(feature) { + const { x, y, spatialReference } = feature; + + let latitude, longitude; + if (spatialReference.wkid === 4326) { + latitude = y; + longitude = x; + } else if (spatialReference.wkid === 102100) { + const auxSphereToLatLon = (x, y) => { + const rMajor = 6378137; + const shift = Math.PI * rMajor; + longitude = (x / shift) * 180.0; + latitude = (y / shift) * 180.0; + latitude = (180 / Math.PI) * (2 * Math.atan(Math.exp(latitude * Math.PI / 180.0)) - Math.PI / 2.0); + }; + auxSphereToLatLon(x, y); + } + return "https://www.google.com/maps?q=" + (latitude) + "," + (longitude); + } + + const test = async (params: { Zone: string; ward: string; area: string; loc: string; street: string; GISID: string }) => { + try { + const newAPIEndPoint = "https://erp.chennaicorporation.gov.in/pgr/external/mobileservice?serviceId=common_api&jsonResp=Yes&subService=getPropertyByGis"; + const queryString = new URLSearchParams(params).toString(); + const requestUrl = `${newAPIEndPoint}&${queryString}`; + + const response = await fetch(requestUrl, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + + if (data && data.ListResult && Object.keys(data.ListResult).length > 0) { + return data.ListResult; + } + + return []; + } catch (err) { + console.error("Error fetching data:", err); + return []; + } + }; + + const fetchGISIDFetch_new_erp = async (params: { + Zone: string; + ward: string; + area?: string | null; + loc?: string | null; + street?: string | null; + GISID?: string | null; + }): Promise => { + try { + const baseUrl = + "https://erp.chennaicorporation.gov.in/pgr/external/mobileservice?serviceId=common_api&jsonResp=Yes&subService=getPropertyByGis"; + + const { GISID, ...queryParams } = params; + + const filteredParams = Object.fromEntries( + Object.entries(queryParams).filter( + ([key, value]) => + (key === "Zone" || key === "ward") || (value !== null && value !== undefined && value !== "") + ) + ); + + const queryString = new URLSearchParams(filteredParams as Record).toString(); + const requestUrl = `${baseUrl}&${queryString}`; + + console.log("Fetching data from:", requestUrl); + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 90000); + + const response = await fetch(requestUrl, { + method: "GET", + signal: controller.signal, + }); + + clearTimeout(timeout); + + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!result?.ListResult || !Array.isArray(result.ListResult)) { + return { error: "Invalid API response structure" }; + } + + if (result.ListResult.length === 0) { + return { error: "No records found" }; + } + + const matchingRecords = result.ListResult.filter( + (item: any) => String(item.GISID).trim() === String(GISID).trim() + ); + + return matchingRecords.length > 0 ? matchingRecords : { error: "GISID not found in results" }; + } catch (error: any) { + if (error.name === "AbortError") { + return { error: "Request timed out" }; + } + console.error("Error fetching data:", error.message); + return { error: error.message }; + } + }; + + async function fetchTradeLicenseDetails(billNumber, clickedFeatDet) { + try { + const rawZone = (clickedFeatDet?.[0]?.zone_number_mis || clickedFeatDet?.[0]?.zone_number || '').toString(); + const rawWard = (clickedFeatDet?.[0]?.ward_number_mis || clickedFeatDet?.[0]?.ward_number || '').toString(); + + const zone = rawZone.startsWith('N') ? rawZone : 'N' + rawZone.padStart(2, '0'); + const ward = rawWard.startsWith('N') ? rawWard : 'N' + rawWard.padStart(3, '0'); + + const profResp = await fetch( + `https://erp.chennaicorporation.gov.in/pgr/external/mobileservice?serviceId=common_api&jsonResp=Yes&subService=getProfTradedata&propid=${encodeURIComponent(billNumber)}` + ); + const profData = await profResp.json(); + + if (!profData?.ListResult || !Array.isArray(profData.ListResult)) { + return `
No professional/trade data available for bill ${billNumber}
`; + } + + const tradeItems = profData.ListResult.filter(it => it.SOURCE === "TRADELICENSE"); + const profTaxItems = profData.ListResult.filter(it => it.SOURCE === "PROFTAX"); + console.log("profItems", profTaxItems) + + let collectedPTNAN = new Set(); + if (tradeItems.length === 0) { + return `
No trade-license items found for bill ${billNumber}
`; + } + + let html = `
Trade License lookups (zone: ${zone}, ward: ${ward}):
`; + + for (const item of tradeItems) { + const licenseNumber = + item.LICENSE_NUMBER || + item.licenseNumber || + item.LICENSE_NO || + item.LICENCE_NUMBER || + item.licence_no || + item.ID; + + const url = `https://erp.chennaicorporation.gov.in/pgr/external/mobileservice?serviceId=common_api&jsonResp=Yes&subService=getTradeLicensedata&zone=${encodeURIComponent(zone)}&ward=${encodeURIComponent(ward)}&licenseNumber=${encodeURIComponent(licenseNumber)}`; + + try { + const resp = await fetch(url, { method: 'GET' }); + const json = await resp.json(); + + html += `
`; + html += `
Trade licenseNumber: ${licenseNumber}
`; + + if (json?.ListResult && Array.isArray(json.ListResult) && json.ListResult.length > 0) { + for (const lic of json.ListResult) { + html += ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Ward / Zone${lic.WARD} / ${lic.ZONE}
Trade License No${lic.LICENSE_NUMBER}
PT Number${lic.PTNAN}
Establishment${lic.ESTABLISHMENT_NAME}
Trade Nature${lic.TRADE_NAME}
Applicant${lic.APPLICANT_NAME} ${lic.FATHER_NAME ? `(S/o: ${lic.FATHER_NAME})` : ''}
Address${lic.APPLICANT_ADDRESS}
Expiry Date${lic.LIC_EXPIRY_DATE?.split(" ")[0]}
Total Fee₹${lic.TOTAL_FEE}
+ `; + + if (lic.PTNAN) { + collectedPTNAN.add(lic.PTNAN); + try { + const ptUrl = `https://erp.chennaicorporation.gov.in/pgr/external/mobileservice?serviceId=common_api&jsonResp=Yes&subService=getProfessionalTaxData&zone=${encodeURIComponent(lic.ZONE)}&ward=${encodeURIComponent(lic.WARD)}&ptnan=${encodeURIComponent(lic.PTNAN)}`; + const ptResp = await fetch(ptUrl, { method: "GET" }); + const ptJson = await ptResp.json(); + + if (ptJson?.ListResult && Array.isArray(ptJson.ListResult) && ptJson.ListResult.length > 0) { + for (const pt of ptJson.ListResult) { + html += ` +
Professional Tax Details
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PTNAN${pt.PTNAN}
Name${pt.NAME}
Area${pt.AREA}
Address${pt.ADDRESS}
Locality${pt.LOCALITY || "N/A"}
Street${pt.STREET}
Ward / Zone${pt.WARD} / ${pt.ZONE}
Demand₹${pt.DEMAND}
Property Tax No${pt.PROPERTYTAX_NUMBER}
Contact${pt.CONTACT_NUMBER || "N/A"}
Created Date${pt.CREATEDDATE}
+ `; + } + } else { + html += `
No Professional Tax data found for PTNAN ${lic.PTNAN}
`; + } + } catch (err) { + console.error("Error fetching professional tax data", err); + html += `
Error fetching Professional Tax for PTNAN ${lic.PTNAN}
`; + } + } + } + } else if (json?.ResultStatus === false && json?.Message) { + html += `
API returned: ${json.Message}
`; + } else { + html += `
No records found for licenseNumber ${licenseNumber}
`; + } + + html += `
`; + } catch (err) { + console.error('Error fetching trade license data', err); + html += `
Error fetching trade license for ${licenseNumber}
`; + } + } + let remainingPT = profTaxItems.filter(pt => !collectedPTNAN.has(pt.ID)); + + if (remainingPT.length > 0) { + html += ` +
+ Other Professional Tax +
+ `; + + for (const ptItem of remainingPT) { + try { + const ptId = ptItem.ID; + + const ptUrl = `https://erp.chennaicorporation.gov.in/pgr/external/mobileservice?serviceId=common_api&jsonResp=Yes&subService=getProfessionalTaxData&zone=${encodeURIComponent(zone)}&ward=${encodeURIComponent(ward)}&ptnan=${encodeURIComponent(ptId)}`; + + const ptResp = await fetch(ptUrl, { method: "GET" }); + const ptJson = await ptResp.json(); + + if (ptJson?.ListResult && ptJson.ListResult.length > 0) { + const pt = ptJson.ListResult[0]; + + html += ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PTNAN${pt.PTNAN}
Name${pt.NAME}
Area${pt.AREA}
Address${pt.ADDRESS}
Street${pt.STREET}
Ward / Zone${pt.WARD} / ${pt.ZONE}
Demand₹${pt.DEMAND}
Property Tax No${pt.PROPERTYTAX_NUMBER}
+ `; + } else { + html += `
No PT data found for ${ptId}
`; + } + } catch (err) { + console.error("Error fetching remaining PT", err); + html += `
Error fetching PT for ${ptItem.ID}
`; + } + } + } + + return html; + } catch (err) { + console.error('fetchTradeLicenseDetails error', err); + return `
Error while fetching trade license details
`; + } + } + + async function fetchProfessionalTaxDetails(ptnan, zone, ward) { + try { + const url = `https://erp.chennaicorporation.gov.in/pgr/external/mobileservice?serviceId=common_api&jsonResp=Yes&subService=getProfessionalTaxData&zone=${encodeURIComponent(zone)}&ward=${encodeURIComponent(ward)}&ptnan=${encodeURIComponent(ptnan)}`; + + const resp = await fetch(url, { method: "GET" }); + const json = await resp.json(); + + let html = `
Professional Tax Details (zone: ${zone}, ward: ${ward}, PTNAN: ${ptnan}):
`; + + if (json?.ListResult && Array.isArray(json.ListResult) && json.ListResult.length > 0) { + for (const item of json.ListResult) { + html += ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PTNAN${item.PTNAN}
Name${item.NAME}
Area${item.AREA}
Address${item.ADDRESS}
Locality${item.LOCALITY || "N/A"}
Street${item.STREET}
Ward / Zone${item.WARD} / ${item.ZONE}
Demand₹${item.DEMAND}
Property Tax No${item.PROPERTYTAX_NUMBER}
Contact${item.CONTACT_NUMBER || "N/A"}
Created Date${item.CREATEDDATE}
+ `; + } + } else if (json?.Message) { + html += `
API returned: ${json.Message}
`; + } else { + html += `
No Professional Tax records found for PTNAN ${ptnan}
`; + } + + return html; + } catch (err) { + console.error("Error fetching professional tax data", err); + return `
Error fetching Professional Tax details for PTNAN ${ptnan}
`; + } + } + + async function getTradeAssessmentHTML(billNumber) { + let html = ''; + + try { + const profTradeResponse = await fetch( + `https://erp.chennaicorporation.gov.in/pgr/external/mobileservice?serviceId=common_api&jsonResp=Yes&subService=getProfTradedata&propid=${billNumber}` + ); + const profData = await profTradeResponse.json(); + + if (profData?.ResultStatus === true && Array.isArray(profData.ListResult)) { + const tradeItems = profData.ListResult.filter(item => item.SOURCE === "TRADELICENSE"); + const profItems = profData.ListResult.filter(item => item.SOURCE === "PROFTAX"); + } else { + } + } catch (err) { + html += `
Error fetching trade data
`; + } + + return html; + } + + // ====================================================== + // BILL NUMBER FORMAT VALIDATION + // Expected: 09-121-01249-000 + // ====================================================== + function validateBillNumber(value: string): boolean { + return /^\d{2}-\d{3}-\d{5}-\d{3}$/.test(value.trim()); + } + + // ====================================================== + // CREATE BILL SEARCH CONTROL + // ====================================================== + function createBillSearchControl( + zoneNumber: string, + wardNumber: string, + area: string, + locality: string, + roadName: string, + gisId: string + ): HTMLElement { + + const container = document.createElement('div'); + + container.style.cssText = ` + margin: 12px 0; + padding: 10px; + border: 1px solid #d9d9d9; + border-radius: 6px; + background: #ffffff; + `; + + const title = document.createElement('div'); + + title.textContent = 'Search by Bill Number'; + + title.style.cssText = ` + font-weight: bold; + margin-bottom: 8px; + color: #444; + font-size: 14px; + `; + + const form = document.createElement('form'); + + form.style.cssText = ` + display: flex; + align-items: center; + gap: 8px; + `; + + const input = document.createElement('input'); + + input.type = 'text'; + input.placeholder = '09-121-01249-000'; + input.maxLength = 16; + + input.style.cssText = ` + flex: 1; + height: 34px; + padding: 6px 10px; + border: 1px solid #bdbdbd; + border-radius: 4px; + font-size: 14px; + box-sizing: border-box; + outline: none; + `; + + const button = document.createElement('button'); + + button.type = 'submit'; + button.textContent = 'Search'; + + button.style.cssText = ` + height: 34px; + padding: 0 16px; + border: none; + border-radius: 4px; + background: #1976d2; + color: white; + font-size: 14px; + cursor: pointer; + `; + + const error = document.createElement('div'); + + error.style.cssText = ` + display: none; + margin-top: 6px; + color: #d32f2f; + font-size: 12px; + `; + + // NEW: results box, where searchBillNumber() renders its output + const resultsBox = document.createElement('div'); + resultsBox.className = 'bill-search-results'; + + // ================================================= + // VALIDATE WHILE TYPING + // ================================================= + // ================================================= +// VALIDATE WHILE TYPING +// ================================================= +input.addEventListener('input', () => { + + const value = input.value.trim(); + + // ================================================= + // IMPORTANT: + // Clear previously searched bill immediately + // whenever the user changes the search text. + // This includes Backspace. + // ================================================= + resultsBox.innerHTML = ''; + + // Reset any previous result/error state + input.style.borderColor = '#bdbdbd'; + + error.style.display = 'none'; + error.textContent = ''; + + // ----------------------------------------------- + // EMPTY SEARCH BOX + // ----------------------------------------------- + if (value === '') { + return; + } + + // ----------------------------------------------- + // VALID BILL NUMBER + // ----------------------------------------------- + if (validateBillNumber(value)) { + + input.style.borderColor = '#4caf50'; + + error.style.display = 'none'; + error.textContent = ''; + + } else { + + // --------------------------------------------- + // INVALID / PARTIAL BILL NUMBER + // --------------------------------------------- + input.style.borderColor = '#d32f2f'; + + error.style.display = 'block'; + + error.textContent = + 'Invalid format. Expected: 09-121-01249-000'; + } + +}); + + // ================================================= + // SEARCH BUTTON + // ================================================= + form.addEventListener('submit', (event) => { + + event.preventDefault(); + + const billNumber = input.value.trim(); + + if (!billNumber) { + input.style.borderColor = '#d32f2f'; + error.style.display = 'block'; + error.textContent = 'Please enter a Bill Number'; + return; + } + + if (!validateBillNumber(billNumber)) { + input.style.borderColor = '#d32f2f'; + error.style.display = 'block'; + error.textContent = + 'Invalid format. Expected: 09-121-01249-000'; + + console.error('Invalid Bill Number:', billNumber); + return; + } + + input.style.borderColor = '#4caf50'; + error.style.display = 'none'; + + console.log('SEARCH BUTTON CLICKED'); + console.log('Bill Number:', billNumber); + + // ---------------------------------- + // REAL SEARCH FUNCTION + // ---------------------------------- + searchBillNumber(billNumber, zoneNumber, wardNumber, area, locality, roadName, gisId, resultsBox); + + }); + + form.appendChild(input); + form.appendChild(button); + + container.appendChild(title); + container.appendChild(form); + container.appendChild(error); + container.appendChild(resultsBox); // NEW + + return container; + } + + // ====================================================== +// CONFIRMATION POPUP: reason for selecting this bill +// ====================================================== +function openReasonConfirmationPopup( + billNumber: string, + gisId: string, + sourceCheckbox: HTMLInputElement +): void { + + const overlay = document.createElement('div'); + overlay.style.cssText = ` + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + `; + + const modal = document.createElement('div'); + modal.style.cssText = ` + background: #fff; + border-radius: 8px; + padding: 18px 20px; + width: 300px; + box-shadow: 0 4px 16px rgba(0,0,0,0.25); + font-size: 14px; + `; + + const title = document.createElement('div'); + title.textContent = `Reason for Bill ${billNumber}`; + title.style.cssText = ` + font-weight: bold; + margin-bottom: 12px; + color: #333; + `; + + const optionsWrap = document.createElement('div'); + optionsWrap.style.cssText = ` + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 16px; + `; + + const reasonName = `reason_${billNumber}_${Date.now()}`; + let selectedReason: string | null = null; + + const makeRadioOption = (label: string, value: string) => { + const row = document.createElement('label'); + row.style.cssText = ` + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + `; + + const radio = document.createElement('input'); + radio.type = 'radio'; + radio.name = reasonName; + radio.value = value; + + radio.addEventListener('change', () => { + if (radio.checked) selectedReason = value; + }); + + const text = document.createElement('span'); + text.textContent = label; + + row.appendChild(radio); + row.appendChild(text); + return row; + }; + + optionsWrap.appendChild(makeRadioOption('Usage Variance', 'USAGE_VARIANCE')); + optionsWrap.appendChild(makeRadioOption('Area Extent', 'AREA_EXTENT')); + + const errorMsg = document.createElement('div'); + errorMsg.style.cssText = ` + display: none; + color: #d32f2f; + font-size: 12px; + margin-bottom: 10px; + `; + errorMsg.textContent = 'Please select a reason.'; + + const buttonRow = document.createElement('div'); + buttonRow.style.cssText = ` + display: flex; + justify-content: flex-end; + gap: 8px; + `; + + const cancelBtn = document.createElement('button'); + cancelBtn.textContent = 'Cancel'; + cancelBtn.style.cssText = ` + padding: 6px 14px; + border: 1px solid #bdbdbd; + border-radius: 4px; + background: #fff; + cursor: pointer; + `; + cancelBtn.addEventListener('click', () => { + sourceCheckbox.checked = false; + document.body.removeChild(overlay); + }); + + const confirmBtn = document.createElement('button'); + confirmBtn.textContent = 'Confirm'; + confirmBtn.style.cssText = ` + padding: 6px 14px; + border: none; + border-radius: 4px; + background: #1976d2; + color: #fff; + cursor: pointer; + `; + confirmBtn.addEventListener('click', async () => { + if (!selectedReason) { + errorMsg.style.display = 'block'; + return; + } + + // TODO: persist selectedReason for (gisId, billNumber) — + // target FeatureLayer/table + field mapping to be decided next. + console.log('Confirmed reason:', { gisId, billNumber, selectedReason }); + + document.body.removeChild(overlay); + }); + + buttonRow.appendChild(cancelBtn); + buttonRow.appendChild(confirmBtn); + + modal.appendChild(title); + modal.appendChild(optionsWrap); + modal.appendChild(errorMsg); + modal.appendChild(buttonRow); + overlay.appendChild(modal); + + document.body.appendChild(overlay); +} + + // ====================================================== + // SEARCH PROPERTY BY BILL NUMBER + // ====================================================== + async function searchBillNumber( + billNumber: string, + zoneNumber: string | number, + wardNumber: string | number, + area: string, + locality: string, + roadName: string, + gisId: string, + resultsBox: HTMLElement + ): Promise { + + console.log("Searching Bill Number:", billNumber); + console.log("Zone:", zoneNumber, "Ward:", wardNumber, "GIS Id:", gisId); + + try { + + const formattedZone = + String(zoneNumber).startsWith("N") ? String(zoneNumber) : `N${zoneNumber}`; + + const formattedWard = + String(wardNumber).startsWith("N") ? String(wardNumber) : `N${wardNumber}`; + + const url = + `https://erp.chennaicorporation.gov.in/pgr/external/mobileservice` + + `?serviceId=common_api` + + `&jsonResp=Yes` + + `&subService=getPropertyByGis` + + `&Zone=${encodeURIComponent(formattedZone)}` + + `&ward=${encodeURIComponent(formattedWard)}`; + + console.log("ERP Bill Search URL:", url); + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + console.log("ERP API Response:", data); + + if (data?.error) { + resultsBox.innerHTML = ` +

${data.error}

+ `; + return; + } + + // ADDED — catches an API-level rejection distinctly from "no matches" + if (data?.ResultStatus === "false" || data?.ResultStatus === false) { + resultsBox.innerHTML = ` +

+ ERP API request failed (${data?.Message || "Unknown error"}). This usually means a token/access issue, not a missing record. +

+ `; + return; + } + + const records = Array.isArray(data?.ListResult) ? data.ListResult : []; + + if (records.length === 0) { + resultsBox.innerHTML = ` +

No property records found.

+ `; + return; + } + + const searchedBill = billNumber.trim().toUpperCase(); + + // Match on BOTH bill number AND GIS ID, so a bill number that + // belongs to a different property is correctly rejected. + const matchedRecord = records.find( + (record: any) => + String(record.BILL_NUMBER || "").trim().toUpperCase() === searchedBill && + String(record.GISID || "").trim().toUpperCase() === String(gisId || "").trim().toUpperCase() + ); + + if (!matchedRecord) { + resultsBox.innerHTML = ` +

+ Bill Number ${billNumber} does not belong to this property + (GIS Id: ${gisId}). +

+ `; + return; + } + + const record = matchedRecord; + const resultBillNumber = record.BILL_NUMBER || "N/A"; + + resultsBox.innerHTML = ` +
+

+ Property details fetched from ERP API. +

+
+ +
+ + Bill Number: ${resultBillNumber} + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Bill No:${resultBillNumber}
Active Status:${record.ACTIVE_STATUS || "N/A"}
Zone:${record.ZONE || "N/A"}
Ward:${record.WARD || "N/A"}
Area:${record.AREA || "N/A"}
Locality:${record.LOCALITY || "N/A"}
Road Name:${record.STREET || "N/A"}
House No:${record.HOUSENO || "N/A"}
Old House No:${record.OLD_HOUSENO || "N/A"}
Owner Name:${record.OWNER_NAME || "N/A"}
Property Type:${record.PROPERTY_TYPE || "N/A"}
Property Usage:${record.USG_NAME || "N/A"}
Total Built-up Area:${record.TOTAL_BUILTUP_AREA || "N/A"}
Sital Area:${record.SITAL_AREA || "N/A"}
Total Demand:${record.TOTAL_DEMAND || "N/A"}
Total Collection:${record.TOTAL_COLLECTION || "N/A"}
Total Balance:${record.TOTAL_BALANCE || "N/A"}
Current Demand:${record.CURRENT_DEMAND || "N/A"}
Current Collection:${record.CURRENT_COLL || "N/A"}
Current Balance:${record.CURRENT_BALANCE || "N/A"}
Arrear Demand:${record.ARREAR_DEMAND || "N/A"}
Arrear Collection:${record.ARREAR_COLL || "N/A"}
Arrear Balance:${record.ARREAR_BALANCE || "N/A"}
Mobile No:${record.MOBILE_NO || "N/A"}
+
+
+ `; + + const billCheckbox = resultsBox.querySelector('.bill-select-checkbox') as HTMLInputElement | null; + if (billCheckbox) { + billCheckbox.addEventListener('click', (e) => e.stopPropagation()); + billCheckbox.addEventListener('change', () => { + if (billCheckbox.checked) { + openReasonConfirmationPopup(resultBillNumber, gisId, billCheckbox); + } + }); + } + + } catch (error) { + + console.error("Bill Number search failed:", error); + + resultsBox.innerHTML = ` +

Failed to fetch property details from ERP API.

+ `; + } + } + + async function buildCustomContent(clickedFeatDet) { + console.log('CONTENT FUNCTION CALLED for GIS ID:', clickedFeatDet?.[0]?.gcc_gis_id); + console.log(clickedFeatDet) + + const directionUrl = await generateDirectionUrl(clickedFeatDet[1]); + + const zoneNumber = clickedFeatDet[0].zone_number; + const formattedZone = "N" + zoneNumber; + + const wardNumber = clickedFeatDet[0].ward_number; + const formattedward = "N" + wardNumber; + const new_api = await fetchGISIDFetch_new_erp({ + Zone: clickedFeatDet[0].zone_number_mis || formattedZone, + ward: clickedFeatDet[0].ward_number_mis || formattedward, + area: clickedFeatDet[0].area_mis, + loc: clickedFeatDet[0].locality_mis, + street: clickedFeatDet[0].street_mis, + GISID: clickedFeatDet[0].gcc_gis_id + }); + console.log(new_api); + + const usage = await fetchSystemPropertyUsage(clickedFeatDet[0].gcc_gis_id); + + let customContent = ` + + + + + + + + + + +
Zone Number${clickedFeatDet[0].zone_number}
Ward Number${clickedFeatDet[0].ward_number}
Area${clickedFeatDet[0].area_name}
Locality${clickedFeatDet[0].locality}
Road Name${clickedFeatDet[0].road_name}
GIS Id${clickedFeatDet[0].gcc_gis_id}
Locate In MapLocate In Map
+ + `; + + return customContent; + } + + const clearGraphic = () => { + jimuMapView?.view.graphics.removeAll(); + setPopupContent(null); + }; + const handleMapView = (view) => { + setJimuMapView(view) + setMapView(view) + } + return ( +
+ {props.useMapWidgetIds && props.useMapWidgetIds.length === 1 && ( + + )} +
+ ); +}; + +export default Popup; + diff --git a/custom-popup/src/setting/setting.tsx b/custom-popup/src/setting/setting.tsx new file mode 100644 index 0000000..3e1d4c8 --- /dev/null +++ b/custom-popup/src/setting/setting.tsx @@ -0,0 +1,18 @@ +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: useMapWidgetIds + }) + } + return
+ +
+} + +export default Setting +