2893 changed files with 284644 additions and 0 deletions
-
3AssetUpdation_Remarks/config.json
-
12AssetUpdation_Remarks/icon.svg
-
29AssetUpdation_Remarks/manifest.json
-
7AssetUpdation_Remarks/src/config.ts
-
637AssetUpdation_Remarks/src/index.tsx
-
8AssetUpdation_Remarks/src/runtime/widget.tsx
-
24AssetUpdation_Remarks/src/styles.css
-
14AssetUpdation_Remarks/tests/simple-widget.test.tsx
-
5Asset_AC_Filter/config.json
-
25Asset_AC_Filter/dist/runtime/assets/zoom-in.svg
-
2Asset_AC_Filter/dist/runtime/widget.js
-
311Asset_AC_Filter/dist/runtime/widget.js.LICENSE.txt
-
1Asset_AC_Filter/dist/setting/setting.js
-
25Asset_AC_Filter/icon.svg
-
19Asset_AC_Filter/manifest.json
-
7Asset_AC_Filter/src/config.ts
-
25Asset_AC_Filter/src/runtime/assets/zoom-in.svg
-
45Asset_AC_Filter/src/runtime/components/LoadingPopup.css
-
23Asset_AC_Filter/src/runtime/components/LoadingPopup.tsx
-
23Asset_AC_Filter/src/runtime/components/LoadingSpinner.jsx
-
123Asset_AC_Filter/src/runtime/simple_widget.css
-
1331Asset_AC_Filter/src/runtime/widget.tsx
-
20Asset_AC_Filter/src/setting/setting.tsx
-
BINAsset_AC_Filter/src/zoom-in.png
-
14Asset_AC_Filter/tests/simple-widget.test.tsx
-
1Asset_Remarks_GIS/config.json
-
47Asset_Remarks_GIS/icon.svg
-
19Asset_Remarks_GIS/manifest.json
-
9Asset_Remarks_GIS/src/config.ts
-
204Asset_Remarks_GIS/src/runtime/components/BufferQueryComponent.tsx
-
150Asset_Remarks_GIS/src/runtime/components/FieldExpressions.tsx
-
9Asset_Remarks_GIS/src/runtime/style.css
-
811Asset_Remarks_GIS/src/runtime/widget.tsx
-
72Asset_Remarks_GIS/src/setting/setting.tsx
-
3Asset_Road_Summary/config.json
-
9Asset_Road_Summary/icon.svg
-
29Asset_Road_Summary/manifest.json
-
7Asset_Road_Summary/src/config.ts
-
317Asset_Road_Summary/src/index.tsx
-
8Asset_Road_Summary/src/runtime/widget.tsx
-
24Asset_Road_Summary/src/styles.css
-
14Asset_Road_Summary/tests/simple-widget.test.tsx
-
3Asset_Road_Summary_view/config.json
-
9Asset_Road_Summary_view/icon.svg
-
29Asset_Road_Summary_view/manifest.json
-
7Asset_Road_Summary_view/src/config.ts
-
242Asset_Road_Summary_view/src/index.tsx
-
8Asset_Road_Summary_view/src/runtime/widget.tsx
-
24Asset_Road_Summary_view/src/styles.css
-
14Asset_Road_Summary_view/tests/simple-widget.test.tsx
-
5Asset_SWD_Filter/config.json
-
25Asset_SWD_Filter/dist/runtime/assets/zoom-in.svg
-
2Asset_SWD_Filter/dist/runtime/widget.js
-
311Asset_SWD_Filter/dist/runtime/widget.js.LICENSE.txt
-
1Asset_SWD_Filter/dist/setting/setting.js
-
3Asset_SWD_Filter/icon.svg
-
19Asset_SWD_Filter/manifest.json
-
7Asset_SWD_Filter/src/config.ts
-
25Asset_SWD_Filter/src/runtime/assets/zoom-in.svg
-
45Asset_SWD_Filter/src/runtime/components/LoadingPopup.css
-
23Asset_SWD_Filter/src/runtime/components/LoadingPopup.tsx
-
23Asset_SWD_Filter/src/runtime/components/LoadingSpinner.jsx
-
123Asset_SWD_Filter/src/runtime/simple_widget.css
-
1274Asset_SWD_Filter/src/runtime/widget.tsx
-
20Asset_SWD_Filter/src/setting/setting.tsx
-
BINAsset_SWD_Filter/src/zoom-in.png
-
14Asset_SWD_Filter/tests/simple-widget.test.tsx
-
3Asset_Summary/config.json
-
4Asset_Summary/icon.svg
-
29Asset_Summary/manifest.json
-
7Asset_Summary/src/config.ts
-
41Asset_Summary/src/index.tsx
-
151Asset_Summary/src/runtime/widget.tsx
-
61Asset_Summary/src/styles.css
-
14Asset_Summary/tests/simple-widget.test.tsx
-
3Asset_Updation_Attachment_status/config.json
-
2Asset_Updation_Attachment_status/icon.svg
-
29Asset_Updation_Attachment_status/manifest.json
-
7Asset_Updation_Attachment_status/src/config.ts
-
898Asset_Updation_Attachment_status/src/index.tsx
-
8Asset_Updation_Attachment_status/src/runtime/widget.tsx
-
24Asset_Updation_Attachment_status/src/styles.css
-
14Asset_Updation_Attachment_status/tests/simple-widget.test.tsx
-
3Asset_Updation_Summary/config.json
-
26Asset_Updation_Summary/icon.svg
-
29Asset_Updation_Summary/manifest.json
-
7Asset_Updation_Summary/src/config.ts
-
381Asset_Updation_Summary/src/index.tsx
-
8Asset_Updation_Summary/src/runtime/widget.tsx
-
24Asset_Updation_Summary/src/styles.css
-
14Asset_Updation_Summary/tests/simple-widget.test.tsx
-
3Attachment_ShopComplex/config.json
-
2Attachment_ShopComplex/icon.svg
-
29Attachment_ShopComplex/manifest.json
-
7Attachment_ShopComplex/src/config.ts
-
482Attachment_ShopComplex/src/index.tsx
-
8Attachment_ShopComplex/src/runtime/widget.tsx
-
24Attachment_ShopComplex/src/styles.css
-
14Attachment_ShopComplex/tests/simple-widget.test.tsx
-
5Attribute_Transfer/config.json
@ -0,0 +1,3 @@ |
|||
{ |
|||
"exampleConfigProperty": "test" |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> |
|||
<svg width="800px" height="800px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
|||
<title>upload-document-note</title> |
|||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
|||
<g id="icon" fill="#000000" transform="translate(64.000000, 42.666667)"> |
|||
<path d="M298.642975,268.433404 L392.812864,362.603293 L362.642975,392.773183 L319.976085,350.117404 L319.976308,447.956959 L277.309641,447.956959 L277.309085,350.116404 L234.642975,392.773183 L204.473085,362.603293 L298.642975,268.433404 Z M234.666667,7.10542736e-15 L341.333333,106.666667 L341.333,280.469 L298.779055,237.915875 L298.666,238.028 L298.666667,124.339779 L216.993555,42.6666667 L42.6666667,42.6666667 L42.6666667,384 L196.184,384 L234.773299,422.589676 L256.093414,401.26939 L256.101,426.666 L1.42108547e-14,426.666667 L1.42108547e-14,7.10542736e-15 L234.666667,7.10542736e-15 Z M213.333333,298.666667 L213.333,323.361 L195.361,341.333 L64,341.333333 L64,298.666667 L213.333333,298.666667 Z M196,85.3333333 L256,145.333333 L124,277.333333 L64,277.333333 L64,217.333333 L196,85.3333333 Z M195.989333,130.581333 L96,230.570667 L96,245.333333 L110.72,245.333333 L210.730667,145.322667 L195.989333,130.581333 Z" id="Combined-Shape"> |
|||
|
|||
</path> |
|||
</g> |
|||
</g> |
|||
</svg> |
|||
@ -0,0 +1,29 @@ |
|||
{ |
|||
"name": "AssetUpdation_Remarks", |
|||
"label": "AssetUpdation_Remarks", |
|||
"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": { |
|||
"hasSettingPage": false |
|||
}, |
|||
"widgets": [ |
|||
{ |
|||
"uri": "AssetUpdation_Remarks", |
|||
"label": "AssetUpdation_Remarks", |
|||
"thumbnail": "thumbnail.png", |
|||
"description": "Summarizes Asset and Road." |
|||
} |
|||
], |
|||
"framework": "react", |
|||
"translatedLocales": ["en"], |
|||
"dataSources": [], |
|||
"elements": [] |
|||
} |
|||
|
|||
|
|||
|
|||
@ -0,0 +1,7 @@ |
|||
import { type ImmutableObject } from 'seamless-immutable' |
|||
|
|||
export interface Config { |
|||
exampleConfigProperty: string |
|||
} |
|||
|
|||
export type IMConfig = ImmutableObject<Config> |
|||
@ -0,0 +1,637 @@ |
|||
import React, { useEffect, useMemo, useState, useCallback, useRef } from "react"; |
|||
import FeatureLayer from "@arcgis/core/layers/FeatureLayer"; |
|||
import { |
|||
Table, |
|||
TableBody, |
|||
TableCell, |
|||
TableContainer, |
|||
TableHead, |
|||
TableRow, |
|||
Paper, |
|||
Typography, |
|||
Grid, |
|||
Button, |
|||
FormControl, |
|||
InputLabel, |
|||
Select, |
|||
MenuItem, |
|||
CircularProgress, |
|||
Alert, |
|||
} from "@mui/material"; |
|||
import { saveAs } from "file-saver"; |
|||
import jsPDF from "jspdf"; |
|||
import "jspdf-autotable"; |
|||
|
|||
// ===================== CONFIGURATION =====================
|
|||
const SUMMARY_LAYER_URL = |
|||
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/gccdeptfield/Summary_Report/FeatureServer/1"; |
|||
|
|||
const DETAIL_LAYER_URL = |
|||
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/Asset_2026_final/FeatureServer/0"; |
|||
|
|||
const PAGE_SIZE = 2000; |
|||
|
|||
// ===================== HELPERS =====================
|
|||
const normalizeText = (value: unknown): string => { |
|||
if (value === null || value === undefined) return ""; |
|||
return String(value).trim(); |
|||
}; |
|||
|
|||
const normalizeCategory = (value: unknown): string => { |
|||
const v = normalizeText(value); |
|||
return v === "" ? "" : v.toUpperCase(); |
|||
}; |
|||
|
|||
const normalizeZone = (value: unknown): string => { |
|||
const v = normalizeText(value); |
|||
if (v === "") return ""; |
|||
const num = parseInt(v, 10); |
|||
if (!Number.isNaN(num)) return num.toString().padStart(2, "0"); |
|||
return v.toUpperCase(); |
|||
}; |
|||
|
|||
const normalizeWard = (value: unknown): string => { |
|||
const v = normalizeText(value); |
|||
if (v === "") return ""; |
|||
const num = parseInt(v, 10); |
|||
if (!Number.isNaN(num)) return num.toString().padStart(3, "0"); |
|||
return v.toUpperCase(); |
|||
}; |
|||
|
|||
const sortAlpha = (values: string[]) => |
|||
[...values].sort((a, b) => |
|||
a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }) |
|||
); |
|||
|
|||
const sortNumericLikeStrings = (values: string[]) => |
|||
[...values].sort((a, b) => { |
|||
const na = parseInt(a, 10); |
|||
const nb = parseInt(b, 10); |
|||
const aNum = Number.isNaN(na) ? Number.MAX_SAFE_INTEGER : na; |
|||
const bNum = Number.isNaN(nb) ? Number.MAX_SAFE_INTEGER : nb; |
|||
if (aNum !== bNum) return aNum - bNum; |
|||
return a.localeCompare(b); |
|||
}); |
|||
|
|||
const joinUniqueRemark = (existing: string, next: string): string => { |
|||
const a = normalizeText(existing); |
|||
const b = normalizeText(next); |
|||
if (!a) return b; |
|||
if (!b) return a; |
|||
if (a.split(" | ").includes(b)) return a; |
|||
return `${a} | ${b}`; |
|||
}; |
|||
|
|||
const buildWhere = (zone?: string, ward?: string): string => { |
|||
const clauses: string[] = ["1=1"]; |
|||
if (zone) clauses.push(`zone = '${zone.replace(/'/g, "''")}'`); |
|||
if (ward) clauses.push(`ward = '${ward.replace(/'/g, "''")}'`); |
|||
return clauses.join(" AND "); |
|||
}; |
|||
|
|||
type CachedZoneData = { |
|||
detailFeatures: any[]; |
|||
summaryFeatures: any[]; |
|||
}; |
|||
|
|||
const CategoryWardSummary: React.FC = () => { |
|||
const [detailLayerFields, setDetailLayerFields] = useState<any[]>([]); |
|||
const [zoneOptions, setZoneOptions] = useState<string[]>([]); |
|||
const [allCategories, setAllCategories] = useState<string[]>([]); |
|||
|
|||
const [selectedZone, setSelectedZone] = useState<string>(""); |
|||
const [selectedWard, setSelectedWard] = useState<string>(""); |
|||
|
|||
const [zoneData, setZoneData] = useState<CachedZoneData>({ |
|||
detailFeatures: [], |
|||
summaryFeatures: [], |
|||
}); |
|||
|
|||
const [loading, setLoading] = useState<boolean>(false); |
|||
const [metaLoading, setMetaLoading] = useState<boolean>(false); |
|||
const [error, setError] = useState<string>(""); |
|||
|
|||
const cacheRef = useRef<Map<string, CachedZoneData>>(new Map()); |
|||
|
|||
const fetchAllFeaturesPaged = async (featureLayer: FeatureLayer, baseQuery: any) => { |
|||
const all: any[] = []; |
|||
let start = 0; |
|||
|
|||
while (true) { |
|||
const q = featureLayer.createQuery(); |
|||
Object.assign(q, baseQuery); |
|||
q.start = start; |
|||
q.num = PAGE_SIZE; |
|||
|
|||
const result = await featureLayer.queryFeatures(q); |
|||
if (!result || !result.features) break; |
|||
|
|||
all.push(...result.features); |
|||
|
|||
if (result.features.length < PAGE_SIZE) break; |
|||
start += PAGE_SIZE; |
|||
} |
|||
|
|||
return all; |
|||
}; |
|||
|
|||
// Load only metadata + category domain + distinct zone list
|
|||
const loadMetadata = useCallback(async () => { |
|||
setMetaLoading(true); |
|||
setError(""); |
|||
|
|||
try { |
|||
const detailLayer = new FeatureLayer({ url: DETAIL_LAYER_URL }); |
|||
const summaryLayer = new FeatureLayer({ url: SUMMARY_LAYER_URL }); |
|||
|
|||
await Promise.all([detailLayer.load(), summaryLayer.load()]); |
|||
setDetailLayerFields(detailLayer.fields || []); |
|||
|
|||
// Categories from the asset layer domain
|
|||
const categoryField = detailLayer.fields?.find( |
|||
(f) => String(f.name).toLowerCase() === "category" |
|||
); |
|||
|
|||
const domainCategories: string[] = []; |
|||
const codedValues = categoryField?.domain?.codedValues || []; |
|||
codedValues.forEach((cv: any) => { |
|||
const value = normalizeCategory(cv?.code ?? cv?.name); |
|||
if (value) domainCategories.push(value); |
|||
}); |
|||
|
|||
setAllCategories(sortAlpha(Array.from(new Set(domainCategories)))); |
|||
|
|||
// Distinct zones from asset layer
|
|||
const zoneQuery = detailLayer.createQuery(); |
|||
zoneQuery.where = "1=1"; |
|||
zoneQuery.returnGeometry = false; |
|||
zoneQuery.outFields = ["zone"]; |
|||
zoneQuery.returnDistinctValues = true; |
|||
|
|||
const zoneResult = await detailLayer.queryFeatures(zoneQuery); |
|||
const zones = Array.from( |
|||
new Set( |
|||
(zoneResult.features || []) |
|||
.map((f: any) => normalizeZone(f.attributes?.zone)) |
|||
.filter(Boolean) |
|||
) |
|||
); |
|||
|
|||
setZoneOptions(sortNumericLikeStrings(zones)); |
|||
} catch (err) { |
|||
console.error("Metadata load error:", err); |
|||
setError("Failed to load metadata."); |
|||
} finally { |
|||
setMetaLoading(false); |
|||
} |
|||
}, []); |
|||
|
|||
useEffect(() => { |
|||
loadMetadata(); |
|||
}, [loadMetadata]); |
|||
|
|||
// Load only selected zone data
|
|||
const loadZoneData = useCallback(async (zone: string) => { |
|||
if (!zone) { |
|||
setZoneData({ detailFeatures: [], summaryFeatures: [] }); |
|||
return; |
|||
} |
|||
|
|||
const cached = cacheRef.current.get(zone); |
|||
if (cached) { |
|||
setZoneData(cached); |
|||
return; |
|||
} |
|||
|
|||
setLoading(true); |
|||
setError(""); |
|||
|
|||
try { |
|||
const summaryLayer = new FeatureLayer({ url: SUMMARY_LAYER_URL }); |
|||
const detailLayer = new FeatureLayer({ url: DETAIL_LAYER_URL }); |
|||
|
|||
const summaryQuery = summaryLayer.createQuery(); |
|||
summaryQuery.where = buildWhere(zone); |
|||
summaryQuery.returnGeometry = false; |
|||
summaryQuery.outFields = ["zone", "category", "remarks_by_gis_expert"]; |
|||
if (summaryLayer.objectIdField) { |
|||
summaryQuery.orderByFields = [summaryLayer.objectIdField]; |
|||
} |
|||
|
|||
const detailQuery = detailLayer.createQuery(); |
|||
detailQuery.where = buildWhere(zone); |
|||
detailQuery.returnGeometry = false; |
|||
detailQuery.outFields = ["zone", "ward", "category"]; |
|||
if (detailLayer.objectIdField) { |
|||
detailQuery.orderByFields = [detailLayer.objectIdField]; |
|||
} |
|||
|
|||
const [summaryResult, detailResult] = await Promise.all([ |
|||
fetchAllFeaturesPaged(summaryLayer, summaryQuery), |
|||
fetchAllFeaturesPaged(detailLayer, detailQuery), |
|||
]); |
|||
|
|||
const payload = { |
|||
detailFeatures: detailResult, |
|||
summaryFeatures: summaryResult, |
|||
}; |
|||
|
|||
cacheRef.current.set(zone, payload); |
|||
setZoneData(payload); |
|||
} catch (err) { |
|||
console.error("Zone load error:", err); |
|||
setError("Failed to load zone data."); |
|||
setZoneData({ detailFeatures: [], summaryFeatures: [] }); |
|||
} finally { |
|||
setLoading(false); |
|||
} |
|||
}, []); |
|||
|
|||
useEffect(() => { |
|||
loadZoneData(selectedZone); |
|||
}, [selectedZone, loadZoneData]); |
|||
|
|||
// Categories shown for the selected zone.
|
|||
// Domain categories stay visible even when count is 0.
|
|||
const mergedCategories = useMemo(() => { |
|||
const set = new Set<string>(); |
|||
|
|||
allCategories.forEach((c) => { |
|||
if (c) set.add(c); |
|||
}); |
|||
|
|||
zoneData.detailFeatures.forEach((feature) => { |
|||
const attrs = feature.attributes || {}; |
|||
const category = normalizeCategory(attrs.category); |
|||
if (category) set.add(category); |
|||
}); |
|||
|
|||
zoneData.summaryFeatures.forEach((feature) => { |
|||
const attrs = feature.attributes || {}; |
|||
const category = normalizeCategory(attrs.category); |
|||
if (category) set.add(category); |
|||
}); |
|||
|
|||
return sortAlpha(Array.from(set)); |
|||
}, [allCategories, zoneData.detailFeatures, zoneData.summaryFeatures]); |
|||
|
|||
// Only wards from the selected zone
|
|||
const wardOptions = useMemo(() => { |
|||
if (!selectedZone) return []; |
|||
|
|||
const wards = new Set<string>(); |
|||
|
|||
zoneData.detailFeatures.forEach((feature) => { |
|||
const attrs = feature.attributes || {}; |
|||
const zone = normalizeZone(attrs.zone); |
|||
const ward = normalizeWard(attrs.ward); |
|||
|
|||
if (zone === selectedZone && ward) { |
|||
wards.add(ward); |
|||
} |
|||
}); |
|||
|
|||
return sortNumericLikeStrings(Array.from(wards)); |
|||
}, [zoneData.detailFeatures, selectedZone]); |
|||
|
|||
const visibleWards = useMemo(() => { |
|||
if (!selectedZone) return []; |
|||
if (selectedWard) return [selectedWard]; |
|||
return wardOptions; |
|||
}, [selectedZone, selectedWard, wardOptions]); |
|||
|
|||
const summaryLookup = useMemo(() => { |
|||
const map = new Map<string, string>(); |
|||
|
|||
zoneData.summaryFeatures.forEach((feature) => { |
|||
const attrs = feature.attributes || {}; |
|||
const zone = normalizeZone(attrs.zone); |
|||
const category = normalizeCategory(attrs.category); |
|||
const remarks = normalizeText(attrs.remarks_by_gis_expert); |
|||
|
|||
if (!zone || !category) return; |
|||
|
|||
const key = `${zone}__${category}`; |
|||
const existing = map.get(key) || ""; |
|||
map.set(key, joinUniqueRemark(existing, remarks)); |
|||
}); |
|||
|
|||
return map; |
|||
}, [zoneData.summaryFeatures]); |
|||
|
|||
const currentRows = useMemo(() => { |
|||
if (!selectedZone) return []; |
|||
|
|||
const map = new Map<string, any>(); |
|||
|
|||
// Pre-create every category row for the selected zone so zero counts show up
|
|||
mergedCategories.forEach((category) => { |
|||
const key = `${selectedZone}__${category}`; |
|||
const row: any = { |
|||
zone: selectedZone, |
|||
category, |
|||
total: 0, |
|||
remarks: summaryLookup.get(key) || "", |
|||
}; |
|||
|
|||
visibleWards.forEach((ward) => { |
|||
row[ward] = 0; |
|||
}); |
|||
|
|||
map.set(key, row); |
|||
}); |
|||
|
|||
// Fill counts from the selected zone only
|
|||
zoneData.detailFeatures.forEach((feature) => { |
|||
const attrs = feature.attributes || {}; |
|||
const zone = normalizeZone(attrs.zone); |
|||
const ward = normalizeWard(attrs.ward); |
|||
const category = normalizeCategory(attrs.category); |
|||
|
|||
if (!zone || !ward || !category) return; |
|||
if (zone !== selectedZone) return; |
|||
if (selectedWard && ward !== selectedWard) return; |
|||
|
|||
const key = `${zone}__${category}`; |
|||
|
|||
if (!map.has(key)) { |
|||
const row: any = { |
|||
zone, |
|||
category, |
|||
total: 0, |
|||
remarks: summaryLookup.get(key) || "", |
|||
}; |
|||
|
|||
visibleWards.forEach((w) => { |
|||
row[w] = 0; |
|||
}); |
|||
|
|||
map.set(key, row); |
|||
} |
|||
|
|||
const row = map.get(key); |
|||
row.total = Number(row.total || 0) + 1; |
|||
|
|||
if (visibleWards.includes(ward)) { |
|||
row[ward] = Number(row[ward] || 0) + 1; |
|||
} |
|||
}); |
|||
|
|||
// Ensure remarks always come from summary layer
|
|||
for (const row of map.values()) { |
|||
const key = `${row.zone}__${row.category}`; |
|||
row.remarks = summaryLookup.get(key) || row.remarks || ""; |
|||
|
|||
visibleWards.forEach((ward) => { |
|||
if (row[ward] === undefined) row[ward] = 0; |
|||
}); |
|||
} |
|||
|
|||
return Array.from(map.values()).sort((a, b) => |
|||
String(a.category).localeCompare(String(b.category), undefined, { |
|||
sensitivity: "base", |
|||
}) |
|||
); |
|||
}, [selectedZone, selectedWard, mergedCategories, summaryLookup, zoneData.detailFeatures, visibleWards]); |
|||
|
|||
const totals = useMemo(() => { |
|||
const acc: any = { total: 0 }; |
|||
visibleWards.forEach((w) => (acc[w] = 0)); |
|||
|
|||
currentRows.forEach((row: any) => { |
|||
visibleWards.forEach((w) => { |
|||
acc[w] += Number(row[w] || 0); |
|||
}); |
|||
acc.total += Number(row.total || 0); |
|||
}); |
|||
|
|||
return acc; |
|||
}, [currentRows, visibleWards]); |
|||
|
|||
const escapeCsvValue = (val: any): string => { |
|||
if (val === null || val === undefined) return ""; |
|||
const s = String(val); |
|||
if (s.includes(",") || s.includes('"') || s.includes("\n")) { |
|||
return `"${s.replace(/"/g, '""')}"`; |
|||
} |
|||
return s; |
|||
}; |
|||
|
|||
const exportToCSV = () => { |
|||
const headers = ["Zone", "Category", ...visibleWards, "Grand Total", "Remarks by GIS Expert"]; |
|||
|
|||
const body = currentRows.map((row: any) => [ |
|||
row.zone, |
|||
row.category, |
|||
...visibleWards.map((w) => row[w] || 0), |
|||
row.total || 0, |
|||
row.remarks || "", |
|||
]); |
|||
|
|||
const totalRow = ["Total", "", ...visibleWards.map((w) => totals[w] || 0), totals.total || 0, ""]; |
|||
|
|||
let csv = headers.map(escapeCsvValue).join(",") + "\n"; |
|||
[...body, totalRow].forEach((row) => { |
|||
csv += row.map(escapeCsvValue).join(",") + "\n"; |
|||
}); |
|||
|
|||
saveAs(new Blob([csv], { type: "text/csv;charset=utf-8;" }), "zone_category_ward_detail.csv"); |
|||
}; |
|||
|
|||
const exportToPDF = () => { |
|||
const doc = new (jsPDF as any)("landscape"); |
|||
|
|||
const headers = [["Zone", "Category", ...visibleWards, "Grand Total", "Remarks by GIS Expert"]]; |
|||
|
|||
const body = currentRows.map((row: any) => [ |
|||
row.zone, |
|||
row.category, |
|||
...visibleWards.map((w) => row[w] || 0), |
|||
row.total || 0, |
|||
row.remarks || "", |
|||
]); |
|||
|
|||
body.push(["Total", "", ...visibleWards.map((w) => totals[w] || 0), totals.total || 0, ""]); |
|||
|
|||
doc.text("Zone / Category / Ward Detail", 14, 14); |
|||
doc.autoTable({ |
|||
head: headers, |
|||
body, |
|||
startY: 20, |
|||
styles: { fontSize: 7, cellWidth: "wrap" }, |
|||
headStyles: { fillColor: [30, 30, 30] }, |
|||
margin: { top: 18, left: 6, right: 6 }, |
|||
}); |
|||
|
|||
doc.save("zone_category_ward_detail.pdf"); |
|||
}; |
|||
|
|||
const handleReset = () => { |
|||
setSelectedZone(""); |
|||
setSelectedWard(""); |
|||
}; |
|||
|
|||
return ( |
|||
<> |
|||
<Typography variant="h6" sx={{ mb: 2 }}> |
|||
Zone / Ward / Category Wise Asset Count |
|||
</Typography> |
|||
|
|||
<Typography variant="body2" sx={{ mb: 2, color: "text.secondary" }}> |
|||
Select a zone first. Only that zone's wards will be loaded and displayed. |
|||
</Typography> |
|||
|
|||
<Grid container spacing={2} sx={{ mb: 2 }} alignItems="center"> |
|||
<Grid item> |
|||
<FormControl sx={{ minWidth: 140 }} size="small"> |
|||
<InputLabel id="zone-select-label">Zone</InputLabel> |
|||
<Select |
|||
labelId="zone-select-label" |
|||
value={selectedZone} |
|||
label="Zone" |
|||
onChange={(e) => { |
|||
setSelectedZone(e.target.value as string); |
|||
setSelectedWard(""); |
|||
}} |
|||
> |
|||
<MenuItem value="">Select Zone</MenuItem> |
|||
{zoneOptions.map((zone) => ( |
|||
<MenuItem key={zone} value={zone}> |
|||
Zone {zone} |
|||
</MenuItem> |
|||
))} |
|||
</Select> |
|||
</FormControl> |
|||
</Grid> |
|||
|
|||
<Grid item> |
|||
<FormControl sx={{ minWidth: 140 }} size="small" disabled={!selectedZone}> |
|||
<InputLabel id="ward-select-label">Ward</InputLabel> |
|||
<Select |
|||
labelId="ward-select-label" |
|||
value={selectedWard} |
|||
label="Ward" |
|||
onChange={(e) => setSelectedWard(e.target.value as string)} |
|||
> |
|||
<MenuItem value="">All Wards</MenuItem> |
|||
{wardOptions.map((ward) => ( |
|||
<MenuItem key={ward} value={ward}> |
|||
{ward} |
|||
</MenuItem> |
|||
))} |
|||
</Select> |
|||
</FormControl> |
|||
</Grid> |
|||
|
|||
<Grid item> |
|||
<Button variant="outlined" onClick={handleReset} sx={{ mr: 1 }}> |
|||
Reset Filters |
|||
</Button> |
|||
<Button variant="contained" color="success" onClick={exportToCSV} sx={{ mr: 1 }}> |
|||
Export CSV |
|||
</Button> |
|||
<Button variant="contained" color="error" onClick={exportToPDF}> |
|||
Export PDF |
|||
</Button> |
|||
</Grid> |
|||
</Grid> |
|||
|
|||
{metaLoading && ( |
|||
<Grid container spacing={2} alignItems="center" sx={{ mb: 1 }}> |
|||
<Grid item> |
|||
<CircularProgress size={20} /> |
|||
</Grid> |
|||
<Grid item> |
|||
<Typography>Loading metadata…</Typography> |
|||
</Grid> |
|||
</Grid> |
|||
)} |
|||
|
|||
{loading && ( |
|||
<Grid container spacing={2} alignItems="center" sx={{ mb: 1 }}> |
|||
<Grid item> |
|||
<CircularProgress size={20} /> |
|||
</Grid> |
|||
<Grid item> |
|||
<Typography>Loading zone data…</Typography> |
|||
</Grid> |
|||
</Grid> |
|||
)} |
|||
|
|||
{error && ( |
|||
<Alert severity="error" sx={{ mb: 2 }}> |
|||
{error} |
|||
</Alert> |
|||
)} |
|||
|
|||
{!selectedZone ? ( |
|||
<Alert severity="info" sx={{ mb: 2 }}> |
|||
Please select a zone to load ward-wise details. |
|||
</Alert> |
|||
) : null} |
|||
|
|||
<TableContainer |
|||
component={Paper} |
|||
sx={{ maxHeight: 560, overflow: "auto", whiteSpace: "nowrap" }} |
|||
> |
|||
<Table stickyHeader size="small"> |
|||
<TableHead> |
|||
<TableRow> |
|||
<TableCell>Zone</TableCell> |
|||
<TableCell>Category</TableCell> |
|||
{visibleWards.map((ward) => ( |
|||
<TableCell key={ward} align="center"> |
|||
{ward} |
|||
</TableCell> |
|||
))} |
|||
<TableCell align="center">Grand Total</TableCell> |
|||
<TableCell>Remarks by GIS Expert</TableCell> |
|||
</TableRow> |
|||
</TableHead> |
|||
|
|||
<TableBody> |
|||
{selectedZone && currentRows.length === 0 && !loading ? ( |
|||
<TableRow> |
|||
<TableCell colSpan={visibleWards.length + 4} align="center"> |
|||
No records to display |
|||
</TableCell> |
|||
</TableRow> |
|||
) : ( |
|||
<> |
|||
{currentRows.map((row: any, idx: number) => ( |
|||
<TableRow key={`${row.zone}_${row.category}_${idx}`}> |
|||
<TableCell>{row.zone}</TableCell> |
|||
<TableCell>{row.category}</TableCell> |
|||
{visibleWards.map((ward) => ( |
|||
<TableCell key={ward} align="center"> |
|||
{row[ward] || 0} |
|||
</TableCell> |
|||
))} |
|||
<TableCell align="center">{row.total || 0}</TableCell> |
|||
<TableCell>{row.remarks || ""}</TableCell> |
|||
</TableRow> |
|||
))} |
|||
|
|||
{selectedZone && currentRows.length > 0 && ( |
|||
<TableRow sx={{ "& td": { fontWeight: "bold" } }}> |
|||
<TableCell>Total</TableCell> |
|||
<TableCell /> |
|||
{visibleWards.map((ward) => ( |
|||
<TableCell key={ward} align="center"> |
|||
{totals[ward] || 0} |
|||
</TableCell> |
|||
))} |
|||
<TableCell align="center">{totals.total || 0}</TableCell> |
|||
<TableCell /> |
|||
</TableRow> |
|||
)} |
|||
</> |
|||
)} |
|||
</TableBody> |
|||
</Table> |
|||
</TableContainer> |
|||
</> |
|||
); |
|||
}; |
|||
|
|||
export default CategoryWardSummary; |
|||
@ -0,0 +1,8 @@ |
|||
import { React, AllWidgetProps } from 'jimu-core'; |
|||
import ZoneAbstractWidget from '../index'; |
|||
|
|||
const Widget = (props: AllWidgetProps<any>) => { |
|||
return <ZoneAbstractWidget {...props} />; |
|||
}; |
|||
|
|||
export default Widget; |
|||
@ -0,0 +1,24 @@ |
|||
.widget-container { |
|||
padding: 15px; |
|||
background: white; |
|||
border-radius: 8px; |
|||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); |
|||
} |
|||
|
|||
h3 { |
|||
font-size: 18px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.export-button { |
|||
margin-top: 10px; |
|||
background-color: #0078d4; |
|||
color: white; |
|||
} |
|||
|
|||
.table-container { |
|||
max-height: 400px; |
|||
overflow-y: auto; |
|||
border: 1px solid #ddd; |
|||
} |
|||
|
|||
@ -0,0 +1,14 @@ |
|||
import { React } from 'jimu-core' |
|||
import _Widget from '../src/runtime/widget' |
|||
import { widgetRender, wrapWidget } from 'jimu-for-test' |
|||
|
|||
const render = widgetRender() |
|||
describe('test simple widget', () => { |
|||
it('simple test', () => { |
|||
const Widget = wrapWidget(_Widget, { |
|||
config: { exampleConfigProperty: 'a' } |
|||
}) |
|||
const { queryByText } = render(<Widget widgetId="Widget_1" />) |
|||
expect(queryByText('exampleConfigProperty: a').tagName).toBe('P') |
|||
}) |
|||
}) |
|||
@ -0,0 +1,5 @@ |
|||
{ |
|||
"exampleConfigProperty": "test", |
|||
"useMapWidget": true |
|||
|
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
<?xml version="1.0" encoding="iso-8859-1"?> |
|||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> |
|||
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" |
|||
viewBox="0 0 488.409 488.409" xml:space="preserve"> |
|||
<circle style="fill:#91C7C9;" cx="160.296" cy="328.409" r="136.8"/> |
|||
<circle style="fill:#B3DEE0;" cx="177.896" cy="346.009" r="119.2"/> |
|||
<path style="fill:#13BCCE;" d="M478.696,10.009c16,16.8,16,43.2-35.2,94.4s-168.8,142.4-184.8,125.6s74.4-133.6,125.6-185.6 |
|||
C435.496-6.791,462.696-6.791,478.696,10.009z"/> |
|||
<path style="fill:#0E94AA;" d="M478.696,10.009c16,16.8,16,43.2-35.2,94.4s-168.8,142.4-184.8,125.6s80.8-124.8,132.8-176.8 |
|||
C442.696,2.009,462.696-6.791,478.696,10.009z"/> |
|||
<path style="fill:#096E89;" d="M478.696,10.009c16,16.8,16,43.2-35.2,94.4s-168.8,142.4-184.8,125.6"/> |
|||
<path style="fill:#F98AC1;" d="M164.296,488.409c-91.2,0-164-72.8-164-164c0-90.4,73.6-164,164-164s164,73.6,164,164 |
|||
C328.296,415.609,254.696,488.409,164.296,488.409z M164.296,200.409c-68,0-124,56-124,124s56,124,124,124s124-56,124-124 |
|||
S232.296,200.409,164.296,200.409z"/> |
|||
<path style="fill:#EF3388;" d="M164.296,488.409c-90.4,0-156-69.6-156-160s72.8-163.2,163.2-163.2s156.8,68.8,156.8,159.2 |
|||
C328.296,415.609,254.696,488.409,164.296,488.409z M164.296,200.409c-68,0-124,56-124,124s56,124,124,124s124-56,124-124 |
|||
S232.296,200.409,164.296,200.409z"/> |
|||
<path style="fill:#BC086B;" d="M306.696,240.409c-3.2,21.6-11.2,43.2-20.8,62.4c1.6,7.2,1.6,14.4,1.6,21.6c0,68-56,124-124,124 |
|||
c-20.8,0-40-4.8-57.6-14.4c-11.2,1.6-22.4,2.4-33.6,2.4c-10.4,0-20-0.8-30.4-1.6c30.4,32.8,72.8,53.6,120.8,53.6 |
|||
c90.4,0,165.6-73.6,165.6-164C329.896,294.009,321.096,265.209,306.696,240.409z"/> |
|||
<path style="fill:#EF3388;" d="M229.096,308.409h-48.8v-48.8c0-8-8-13.6-16-13.6s-16,6.4-16,13.6v48.8h-49.6c-8,0-14.4,8-14.4,16 |
|||
s6.4,16,14.4,16h49.6v49.6c0,8,8,14.4,16,14.4s16-6.4,16-14.4v-49.6h48.8c8,0,14.4-8,14.4-16S237.096,308.409,229.096,308.409z"/> |
|||
<path style="fill:#BC086B;" d="M229.096,308.409h-48.8l0,0l-32,32l0,0v49.6c0,8,8,14.4,16,14.4s16-6.4,16-14.4v-49.6h48.8 |
|||
c8,0,14.4-8,14.4-16S237.096,308.409,229.096,308.409z"/> |
|||
</svg> |
|||
2
Asset_AC_Filter/dist/runtime/widget.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,311 @@ |
|||
/** |
|||
* @license |
|||
Copyright (c) 2008, Adobe Systems Incorporated |
|||
All rights reserved. |
|||
|
|||
Redistribution and use in source and binary forms, with or without |
|||
modification, are permitted provided that the following conditions are |
|||
met: |
|||
|
|||
* Redistributions of source code must retain the above copyright notice, |
|||
this list of conditions and the following disclaimer. |
|||
|
|||
* Redistributions in binary form must reproduce the above copyright |
|||
notice, this list of conditions and the following disclaimer in the |
|||
documentation and/or other materials provided with the distribution. |
|||
|
|||
* Neither the name of Adobe Systems Incorporated nor the names of its |
|||
contributors may be used to endorse or promote products derived from |
|||
this software without specific prior written permission. |
|||
|
|||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS |
|||
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
|||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
|||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
|||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
|||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
|||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
|||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
|||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* |
|||
* Copyright (c) 2014 James Robb, https://github.com/jamesbrobb |
|||
* |
|||
* Permission is hereby granted, free of charge, to any person obtaining |
|||
* a copy of this software and associated documentation files (the |
|||
* "Software"), to deal in the Software without restriction, including |
|||
* without limitation the rights to use, copy, modify, merge, publish, |
|||
* distribute, sublicense, and/or sell copies of the Software, and to |
|||
* permit persons to whom the Software is furnished to do so, subject to |
|||
* the following conditions: |
|||
* |
|||
* The above copyright notice and this permission notice shall be |
|||
* included in all copies or substantial portions of the Software. |
|||
* |
|||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
* ==================================================================== |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* (c) Dean McNamee <dean@gmail.com>, 2013. |
|||
* |
|||
* https://github.com/deanm/omggif |
|||
* |
|||
* Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
* of this software and associated documentation files (the "Software"), to |
|||
* deal in the Software without restriction, including without limitation the |
|||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or |
|||
* sell copies of the Software, and to permit persons to whom the Software is |
|||
* furnished to do so, subject to the following conditions: |
|||
* |
|||
* The above copyright notice and this permission notice shall be included in |
|||
* all copies or substantial portions of the Software. |
|||
* |
|||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
|||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
|||
* IN THE SOFTWARE. |
|||
* |
|||
* omggif is a JavaScript implementation of a GIF 89a encoder and decoder, |
|||
* including animation and compression. It does not rely on any specific |
|||
* underlying system, so should run in the browser, Node, or Plask. |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* Copyright (c) 2014 Steven Spungin (TwelveTone LLC) steven@twelvetone.tv |
|||
* |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* Copyright (c) 2017 Aras Abbasi |
|||
* |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* Copyright (c) 2018 Aras Abbasi |
|||
* |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* Copyright (c) 2019 Aras Abbasi |
|||
* |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* FPDF is released under a permissive license: there is no usage restriction. |
|||
* You may embed it freely in your application (commercial or not), with or |
|||
* without modifications. |
|||
* |
|||
* Reference: http://www.fpdf.org/en/script/script37.php |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* Joseph Myers does not specify a particular license for his work. |
|||
* |
|||
* Author: Joseph Myers |
|||
* Accessed from: http://www.myersdaily.org/joseph/javascript/md5.js |
|||
* |
|||
* Modified by: Owen Leong |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
* Author: Owen Leong (@owenl131) |
|||
* Date: 15 Oct 2020 |
|||
* References: |
|||
* https://www.cs.cmu.edu/~dst/Adobe/Gallery/anon21jul01-pdf-encryption.txt |
|||
* https://github.com/foliojs/pdfkit/blob/master/lib/security.js |
|||
* http://www.fpdf.org/en/script/script37.php |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* Unicode Bidi Engine based on the work of Alex Shensis (@asthensis) |
|||
* MIT License |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* jsPDF fileloading PlugIn |
|||
* Copyright (c) 2018 Aras Abbasi (aras.abbasi@gmail.com) |
|||
* |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* jsPDF filters PlugIn |
|||
* Copyright (c) 2014 Aras Abbasi |
|||
* |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* jsPDF virtual FileSystem functionality |
|||
* |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* A class to parse color values |
|||
* @author Stoyan Stefanov <sstoo@gmail.com> |
|||
* {@link http://www.phpied.com/rgb-color-parser-in-javascript/} |
|||
* @license Use it if you like it |
|||
*/ |
|||
|
|||
/** ==================================================================== |
|||
* @license |
|||
* jsPDF XMP metadata plugin |
|||
* Copyright (c) 2016 Jussi Utunen, u-jussi@suomi24.fi |
|||
* |
|||
* Permission is hereby granted, free of charge, to any person obtaining |
|||
* a copy of this software and associated documentation files (the |
|||
* "Software"), to deal in the Software without restriction, including |
|||
* without limitation the rights to use, copy, modify, merge, publish, |
|||
* distribute, sublicense, and/or sell copies of the Software, and to |
|||
* permit persons to whom the Software is furnished to do so, subject to |
|||
* the following conditions: |
|||
* |
|||
* The above copyright notice and this permission notice shall be |
|||
* included in all copies or substantial portions of the Software. |
|||
* |
|||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
* ==================================================================== |
|||
*/ |
|||
|
|||
/** @license |
|||
* Copyright (c) 2017 Dominik Homberger |
|||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
|||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. |
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
https://webpjs.appspot.com |
|||
WebPRiffParser dominikhlbg@gmail.com |
|||
*/ |
|||
|
|||
/** @license |
|||
* |
|||
* jsPDF - PDF Document creation from JavaScript |
|||
* Version 3.0.1 Built on 2025-03-17T14:19:36.873Z |
|||
* CommitID 00000000 |
|||
* |
|||
* Copyright (c) 2010-2021 James Hall <james@parall.ax>, https://github.com/MrRio/jsPDF |
|||
* 2015-2021 yWorks GmbH, http://www.yworks.com |
|||
* 2015-2021 Lukas Holländer <lukas.hollaender@yworks.com>, https://github.com/HackbrettXXX |
|||
* 2016-2018 Aras Abbasi <aras.abbasi@gmail.com> |
|||
* 2010 Aaron Spike, https://github.com/acspike |
|||
* 2012 Willow Systems Corporation, https://github.com/willowsystems |
|||
* 2012 Pablo Hess, https://github.com/pablohess |
|||
* 2012 Florian Jenett, https://github.com/fjenett |
|||
* 2013 Warren Weckesser, https://github.com/warrenweckesser |
|||
* 2013 Youssef Beddad, https://github.com/lifof |
|||
* 2013 Lee Driscoll, https://github.com/lsdriscoll |
|||
* 2013 Stefan Slonevskiy, https://github.com/stefslon |
|||
* 2013 Jeremy Morel, https://github.com/jmorel |
|||
* 2013 Christoph Hartmann, https://github.com/chris-rock |
|||
* 2014 Juan Pablo Gaviria, https://github.com/juanpgaviria |
|||
* 2014 James Makes, https://github.com/dollaruw |
|||
* 2014 Diego Casorran, https://github.com/diegocr |
|||
* 2014 Steven Spungin, https://github.com/Flamenco |
|||
* 2014 Kenneth Glassey, https://github.com/Gavvers |
|||
* |
|||
* Permission is hereby granted, free of charge, to any person obtaining |
|||
* a copy of this software and associated documentation files (the |
|||
* "Software"), to deal in the Software without restriction, including |
|||
* without limitation the rights to use, copy, modify, merge, publish, |
|||
* distribute, sublicense, and/or sell copies of the Software, and to |
|||
* permit persons to whom the Software is furnished to do so, subject to |
|||
* the following conditions: |
|||
* |
|||
* The above copyright notice and this permission notice shall be |
|||
* included in all copies or substantial portions of the Software. |
|||
* |
|||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
* |
|||
* Contributor(s): |
|||
* siefkenj, ahwolf, rickygu, Midnith, saintclair, eaparango, |
|||
* kim3er, mfo, alnorth, Flamenco |
|||
*/ |
|||
|
|||
/** @license |
|||
* Copyright (c) 2012 Willow Systems Corporation, https://github.com/willowsystems |
|||
* |
|||
* Permission is hereby granted, free of charge, to any person obtaining |
|||
* a copy of this software and associated documentation files (the |
|||
* "Software"), to deal in the Software without restriction, including |
|||
* without limitation the rights to use, copy, modify, merge, publish, |
|||
* distribute, sublicense, and/or sell copies of the Software, and to |
|||
* permit persons to whom the Software is furnished to do so, subject to |
|||
* the following conditions: |
|||
* |
|||
* The above copyright notice and this permission notice shall be |
|||
* included in all copies or substantial portions of the Software. |
|||
* |
|||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
* ==================================================================== |
|||
*/ |
|||
@ -0,0 +1 @@ |
|||
System.register(["jimu-core","jimu-ui/advanced/setting-components"],(function(e,t){var r={},o={};return{setters:[function(e){r.React=e.React},function(e){o.MapWidgetSelector=e.MapWidgetSelector}],execute:function(){e((()=>{var e={79244:e=>{"use strict";e.exports=r},79298:e=>{"use strict";e.exports=o}},t={};function n(r){var o=t[r];if(void 0!==o)return o.exports;var i=t[r]={exports:{}};return e[r](i,i.exports,n),i.exports}n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.p="";var i={};return n.p=window.jimuConfig.baseUrl,(()=>{"use strict";n.r(i),n.d(i,{__set_webpack_public_path__:()=>o,default:()=>r});var e=n(79244),t=n(79298);const r=r=>e.React.createElement("div",{className:"widget-setting-demo"},e.React.createElement(t.MapWidgetSelector,{useMapWidgetIds:r.useMapWidgetIds,onSelect:e=>{console.log("Selected Map Widget IDs:",e),r.onSettingChange({id:r.id,useMapWidgetIds:e})}}));function o(e){n.p=e}})(),i})())}}})); |
|||
@ -0,0 +1,25 @@ |
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="#111827"> |
|||
<path d="M 12,2 L 21,5 L 19,16 L 11,22 L 3,17 Z" |
|||
fill="none" |
|||
stroke="#111827" |
|||
stroke-width="1.5" |
|||
stroke-linejoin="round" /> |
|||
|
|||
<circle cx="12" cy="2" r="1.25" /> |
|||
<circle cx="21" cy="5" r="1.25" /> |
|||
<circle cx="19" cy="16" r="1.25" /> |
|||
<circle cx="11" cy="22" r="1.25" /> |
|||
<circle cx="3" cy="17" r="1.25" /> |
|||
|
|||
<rect x="7" y="14" width="10" height="1.5" rx="0.5" /> |
|||
<rect x="8" y="12.5" width="8" height="1.5" rx="0.25" fill="#ffffff" stroke="#111827" stroke-width="1" /> |
|||
|
|||
<line x1="9" y1="10" x2="9" y2="12.5" stroke="#111827" stroke-width="1" /> |
|||
<line x1="11" y1="10" x2="11" y2="12.5" stroke="#111827" stroke-width="1" /> |
|||
<line x1="13" y1="10" x2="13" y2="12.5" stroke="#111827" stroke-width="1" /> |
|||
<line x1="15" y1="10" x2="15" y2="12.5" stroke="#111827" stroke-width="1" /> |
|||
|
|||
<path d="M 7.5,10 A 4.5,4.5 0 0,1 16.5,10 Z" /> |
|||
|
|||
<line x1="12" y1="5.5" x2="12" y2="3.5" stroke="#111827" stroke-width="1.2" stroke-linecap="round" /> |
|||
</svg> |
|||
@ -0,0 +1,19 @@ |
|||
{ |
|||
"name": "Asset_AC_Filter", |
|||
"label": "Asset_AC_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 |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
import { type ImmutableObject } from 'seamless-immutable' |
|||
|
|||
export interface Config { |
|||
exampleConfigProperty: string |
|||
} |
|||
|
|||
export type IMConfig = ImmutableObject<Config> |
|||
@ -0,0 +1,25 @@ |
|||
<?xml version="1.0" encoding="iso-8859-1"?> |
|||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> |
|||
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" |
|||
viewBox="0 0 488.409 488.409" xml:space="preserve"> |
|||
<circle style="fill:#91C7C9;" cx="160.296" cy="328.409" r="136.8"/> |
|||
<circle style="fill:#B3DEE0;" cx="177.896" cy="346.009" r="119.2"/> |
|||
<path style="fill:#13BCCE;" d="M478.696,10.009c16,16.8,16,43.2-35.2,94.4s-168.8,142.4-184.8,125.6s74.4-133.6,125.6-185.6 |
|||
C435.496-6.791,462.696-6.791,478.696,10.009z"/> |
|||
<path style="fill:#0E94AA;" d="M478.696,10.009c16,16.8,16,43.2-35.2,94.4s-168.8,142.4-184.8,125.6s80.8-124.8,132.8-176.8 |
|||
C442.696,2.009,462.696-6.791,478.696,10.009z"/> |
|||
<path style="fill:#096E89;" d="M478.696,10.009c16,16.8,16,43.2-35.2,94.4s-168.8,142.4-184.8,125.6"/> |
|||
<path style="fill:#F98AC1;" d="M164.296,488.409c-91.2,0-164-72.8-164-164c0-90.4,73.6-164,164-164s164,73.6,164,164 |
|||
C328.296,415.609,254.696,488.409,164.296,488.409z M164.296,200.409c-68,0-124,56-124,124s56,124,124,124s124-56,124-124 |
|||
S232.296,200.409,164.296,200.409z"/> |
|||
<path style="fill:#EF3388;" d="M164.296,488.409c-90.4,0-156-69.6-156-160s72.8-163.2,163.2-163.2s156.8,68.8,156.8,159.2 |
|||
C328.296,415.609,254.696,488.409,164.296,488.409z M164.296,200.409c-68,0-124,56-124,124s56,124,124,124s124-56,124-124 |
|||
S232.296,200.409,164.296,200.409z"/> |
|||
<path style="fill:#BC086B;" d="M306.696,240.409c-3.2,21.6-11.2,43.2-20.8,62.4c1.6,7.2,1.6,14.4,1.6,21.6c0,68-56,124-124,124 |
|||
c-20.8,0-40-4.8-57.6-14.4c-11.2,1.6-22.4,2.4-33.6,2.4c-10.4,0-20-0.8-30.4-1.6c30.4,32.8,72.8,53.6,120.8,53.6 |
|||
c90.4,0,165.6-73.6,165.6-164C329.896,294.009,321.096,265.209,306.696,240.409z"/> |
|||
<path style="fill:#EF3388;" d="M229.096,308.409h-48.8v-48.8c0-8-8-13.6-16-13.6s-16,6.4-16,13.6v48.8h-49.6c-8,0-14.4,8-14.4,16 |
|||
s6.4,16,14.4,16h49.6v49.6c0,8,8,14.4,16,14.4s16-6.4,16-14.4v-49.6h48.8c8,0,14.4-8,14.4-16S237.096,308.409,229.096,308.409z"/> |
|||
<path style="fill:#BC086B;" d="M229.096,308.409h-48.8l0,0l-32,32l0,0v49.6c0,8,8,14.4,16,14.4s16-6.4,16-14.4v-49.6h48.8 |
|||
c8,0,14.4-8,14.4-16S237.096,308.409,229.096,308.409z"/> |
|||
</svg> |
|||
@ -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; |
|||
} |
|||
|
|||
@ -0,0 +1,23 @@ |
|||
import React from 'react'; |
|||
import './LoadingPopup.css'; |
|||
|
|||
interface LoadingPopupProps { |
|||
show: boolean; |
|||
message?: string; |
|||
} |
|||
|
|||
const LoadingPopup: React.FC<LoadingPopupProps> = ({ show, message = "Preparing to download ..." }) => { |
|||
if (!show) return null; |
|||
|
|||
return ( |
|||
<div className="loading-overlay"> |
|||
<div className="loading-box"> |
|||
<h2 className="loading-title">Please Wait..</h2> |
|||
<div className="spinner" /> |
|||
<p className="loading-message">{message}</p> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default LoadingPopup; |
|||
@ -0,0 +1,23 @@ |
|||
import React from 'react'; |
|||
import './LoadingPopup.css'; |
|||
|
|||
interface LoadingPopupProps { |
|||
show: boolean; |
|||
message?: string; |
|||
} |
|||
|
|||
const LoadingPopup: React.FC<LoadingPopupProps> = ({ show, message = "Preparing to download ..." }) => { |
|||
if (!show) return null; |
|||
|
|||
return ( |
|||
<div className="loading-overlay"> |
|||
<div className="loading-box"> |
|||
<h2 className="loading-title">Please Wait..</h2> |
|||
<div className="spinner" /> |
|||
<p className="loading-message">{message}</p> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default LoadingPopup; |
|||
@ -0,0 +1,123 @@ |
|||
.property-widget-parent { |
|||
padding: 0.7rem; |
|||
font-family: "Arial", sans-serif; |
|||
height: 100%; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.property-widget { |
|||
background: #fff; |
|||
border-radius: 6px; |
|||
padding: 0.7rem; |
|||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.08); |
|||
height: 100%; |
|||
max-height: 100%; |
|||
overflow-y: auto; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.widget-title { |
|||
font-size: 1rem; |
|||
font-weight: bold; |
|||
margin-bottom: 0.7rem; |
|||
} |
|||
|
|||
#countDiv { |
|||
background: #f7f7f7; |
|||
border-radius: 4px; |
|||
padding: 4px 8px; /* less padding */ |
|||
margin-bottom: 0.3rem; |
|||
font-size: 0.75rem; /* smaller text */ |
|||
color: #333; |
|||
line-height: 1.2; |
|||
display: inline-block; /* shrink width to content */ |
|||
} |
|||
|
|||
|
|||
|
|||
.property_form { |
|||
display: flex; |
|||
flex-direction: column; |
|||
margin-bottom: 0.7rem; |
|||
} |
|||
|
|||
.property_form label { |
|||
font-weight: 600; |
|||
margin-bottom: 2px; |
|||
font-size: 0.7rem; |
|||
} |
|||
|
|||
.property_form select { |
|||
padding: 4px 6px; |
|||
border: 1px solid #ccc; |
|||
border-radius: 4px; |
|||
font-size: 0.7rem; |
|||
} |
|||
|
|||
button { |
|||
width: 100%; |
|||
padding: 6px 8px; |
|||
margin-top: 0.3rem; |
|||
border-radius: 4px; |
|||
font-weight: bold; |
|||
font-size: 0.7rem; |
|||
border: none; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
button.property_form { |
|||
background-color: #0078d4; |
|||
color: white; |
|||
} |
|||
|
|||
button[property_form]:hover { |
|||
background-color: #005ea0; |
|||
} |
|||
|
|||
button[variant="outlined"] { |
|||
background-color: white; |
|||
color: #0078d4; |
|||
border: 1px solid #0078d4; |
|||
} |
|||
|
|||
button[variant="outlined"]:hover { |
|||
background-color: #f0f0f0; |
|||
} |
|||
|
|||
.attribute-table { |
|||
margin-top: 0.7rem; |
|||
overflow-x: auto; |
|||
} |
|||
|
|||
.attribute-table table { |
|||
table-layout: fixed; |
|||
width: 100%; |
|||
border-collapse: collapse; |
|||
} |
|||
|
|||
.attribute-table th, |
|||
.attribute-table td { |
|||
max-width: 150px; |
|||
padding: 6px; |
|||
border: 1px solid #ddd; |
|||
word-wrap: break-word; |
|||
overflow-wrap: break-word; |
|||
white-space: normal; |
|||
font-size: 0.7rem; |
|||
} |
|||
|
|||
.attribute-table th { |
|||
background-color: #f2f2f2; |
|||
font-weight: bold; |
|||
position: sticky; |
|||
top: 0; |
|||
z-index: 10; |
|||
text-align: left; |
|||
border-bottom: 1px solid #ccc; |
|||
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.attribute-table tr:hover { |
|||
background-color: #f9f9f9; |
|||
cursor: pointer; |
|||
} |
|||
1331
Asset_AC_Filter/src/runtime/widget.tsx
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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<any>) => { |
|||
const onMapWidgetSelected = (useMapWidgetIds: string[]) => { |
|||
console.log("Selected Map Widget IDs:", useMapWidgetIds); |
|||
props.onSettingChange({ |
|||
id: props.id, |
|||
useMapWidgetIds: useMapWidgetIds |
|||
}) |
|||
} |
|||
|
|||
return <div className="widget-setting-demo"> |
|||
<MapWidgetSelector useMapWidgetIds={props.useMapWidgetIds} onSelect={onMapWidgetSelected} /> |
|||
</div> |
|||
} |
|||
|
|||
export default Setting |
|||
|
|||
|
After Width: 512 | Height: 512 | Size: 23 KiB |
@ -0,0 +1,14 @@ |
|||
import { React } from 'jimu-core' |
|||
import _Widget from '../src/runtime/widget' |
|||
import { widgetRender, wrapWidget } from 'jimu-for-test' |
|||
|
|||
const render = widgetRender() |
|||
describe('test simple widget', () => { |
|||
it('simple test', () => { |
|||
const Widget = wrapWidget(_Widget, { |
|||
config: { exampleConfigProperty: 'a' } |
|||
}) |
|||
const { queryByText } = render(<Widget widgetId="Widget_1" />) |
|||
expect(queryByText('exampleConfigProperty: a').tagName).toBe('P') |
|||
}) |
|||
}) |
|||
@ -0,0 +1 @@ |
|||
{} |
|||
@ -0,0 +1,47 @@ |
|||
<?xml version="1.0" encoding="iso-8859-1"?> |
|||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> |
|||
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" |
|||
viewBox="0 0 511.999 511.999" xml:space="preserve"> |
|||
<path style="fill:#3C9684;" d="M275.963,475.985c26.784,1.738,53.728,2.074,80.491-0.208c25.93-2.21,52.325-6.581,76.471-16.629 |
|||
c5.821-2.423,11.467-5.213,16.979-8.272c5.407-2.999,10.779-6.184,15.676-9.973c9.402-7.277,17.321-16.523,22.666-27.172 |
|||
c5.646-11.247,8.406-23.721,9.025-36.241c0.593-11.988-0.916-23.983-7.246-34.409c-11.982-19.735-35.578-29.264-56.794-35.294 |
|||
c-25.158-7.15-51.368-10.368-77.352-12.743c-20.945-1.913-41.953-3.107-62.957-4.15c-9.288-0.462-18.576-0.912-27.864-1.361 |
|||
c-7.372-0.355-14.756-0.308-22.134-0.485c-13.322-0.32-26.642-0.869-39.945-1.648c-26.597-1.559-53.252-3.874-79.543-8.284 |
|||
c-10.88-1.825-21.654-3.888-32.17-7.283c-12.219-3.946-24.111-9.04-35.33-15.289c-21.09-11.746-41.189-28.455-50.479-51.358 |
|||
c-4.73-11.659-6.5-24.832-4.856-37.319c1.564-11.872,6.791-22.898,14.689-31.864c10.398-11.807,24.492-19.384,38.899-25.161 |
|||
c23.855-9.567,49.93-13.778,75.486-15.185c16.103-0.887,32.353-0.721,48.431,0.633c21.926,1.848,43.782,4.437,65.635,6.999 |
|||
c26.699,3.131,53.394,6.581,80.179,8.901c13.315,1.154,26.552,2.947,39.843,4.337c13.285,1.389,26.605,2.489,39.951,3.056 |
|||
c13.23,0.562,26.517,0.684,39.733-0.276c12.787-0.929,25.799-2.851,37.844-7.397c10.325-3.897,20.093-10.136,26.006-19.69 |
|||
c-5.671,11.5-16.563,19.058-28.026,24.075c-12.14,5.312-25.364,8.103-38.431,9.962c-27.073,3.853-54.614,3.773-81.888,2.87 |
|||
c-6.858-0.227-13.717-0.499-20.569-0.876c-6.85-0.377-13.668-1.159-20.482-1.929c-5.166-0.584-10.336-1.126-15.504-1.688 |
|||
c-21.562-2.343-43.097-4.929-64.639-7.446c-21.016-2.456-42.059-5.118-63.169-6.645c-19.578-1.416-39.368-1.3-58.89,0.772 |
|||
c-13.32,1.414-26.655,3.737-39.547,7.41c-9.79,2.789-19.558,6.256-28.59,11.001c-12.781,6.712-24.639,16.055-30.014,29.866 |
|||
c-4.489,11.535-3.778,25.223,0.118,36.82c3.738,11.127,10.935,20.816,19.378,28.845c8.823,8.39,19.156,15.146,29.922,20.767 |
|||
c11.577,6.045,23.872,10.883,36.476,14.302c4.777,1.295,9.633,2.147,14.484,3.095c6.737,1.315,13.538,2.349,20.335,3.308 |
|||
c13.597,1.915,27.266,3.32,40.948,4.458c25.967,2.162,52.037,3.339,78.092,3.606c8.86,0.091,17.722,0.694,26.571,1.134 |
|||
c18.434,0.915,36.867,1.845,55.276,3.198c27.318,2.006,54.774,4.726,81.567,10.594c12.684,2.778,25.294,6.282,37.305,11.259 |
|||
c11.706,4.852,23.046,11.16,32.343,19.864c9.052,8.476,15.821,19.125,19.073,31.137c3.534,13.055,3.038,27.2,0.787,40.437 |
|||
c-2.129,12.516-6.549,24.741-13.363,35.476c-6.746,10.627-15.626,19.694-25.741,27.149c-21.023,15.496-47.443,22.125-72.977,25.477 |
|||
c-27.277,3.579-55.088,3.227-82.442,0.677C302.396,479.951,289.132,478.249,275.963,475.985z"/> |
|||
<path style="fill:#F2C07E;" d="M245.005,471.344c-69.22-23.323-94.851-71.48-96.386-74.465c-0.036-0.06-0.048-0.109-0.073-0.133 |
|||
v-0.012l42.066-18.586l64.181-28.35C254.793,349.798,275.313,402.305,245.005,471.344z"/> |
|||
<path style="fill:#F23488;" d="M215.084,403.634c-3.758,1.656-7.517,3.045-11.239,4.181c-0.834,0.266-1.668,0.508-2.501,0.737 |
|||
c-22.405,6.199-42.924,2.973-50.96-8.556c-0.012,0-0.012-0.012-0.012-0.024c-0.713-1.003-1.317-2.091-1.825-3.227v-0.012 |
|||
L13.345,90.706L55.411,72.12l64.181-28.362l135.201,306.04C262.213,366.571,244.425,390.68,215.084,403.634z"/> |
|||
<path style="fill:#D6A86F;" d="M245.005,471.344c-69.22-23.323-94.851-71.48-96.386-74.465c-0.024-0.048-0.048-0.085-0.073-0.133 |
|||
v-0.012l42.066-18.586l7.819,17.692C200.232,399.912,225.44,458.28,245.005,471.344z"/> |
|||
<path style="fill:#DB2A78;" d="M203.845,407.815c-0.834,0.266-1.668,0.508-2.501,0.737c-22.405,6.199-42.924,2.973-50.96-8.556 |
|||
c-0.012,0-0.012-0.012-0.012-0.024c-0.991-1.643-1.559-2.719-1.752-3.094c-0.024-0.048-0.048-0.085-0.073-0.133v-0.012 |
|||
L13.345,90.706L55.411,72.12l135.201,306.028l7.819,17.692C198.89,396.879,200.848,401.411,203.845,407.815z"/> |
|||
<ellipse transform="matrix(-0.4041 -0.9147 0.9147 -0.4041 31.8246 155.1859)" style="fill:#D6A86F;" cx="66.461" cy="67.227" rx="33.201" ry="58.077"/> |
|||
<g> |
|||
<path style="fill:#3C9684;" d="M47.829,84.521c-3.564,0-6.828-2.1-8.286-5.401c-1.556-3.521-0.717-7.642,2.092-10.275 |
|||
c5.328-4.995,12.202-9.426,19.879-12.818c7.677-3.391,15.583-5.489,22.861-6.065c3.83-0.307,7.449,1.85,9.005,5.373 |
|||
c1.556,3.521,0.717,7.642-2.092,10.275c-5.328,4.995-12.202,9.427-19.879,12.818c-7.677,3.392-15.583,5.489-22.861,6.065 |
|||
C48.308,84.511,48.069,84.521,47.829,84.521z"/> |
|||
<path style="fill:#3C9684;" d="M257.017,435.151c-2.574,11.359-6.429,23.492-12.012,36.193v0.012c-0.012,0-0.036-0.012-0.048-0.012 |
|||
c-12.52-4.23-23.625-9.269-33.402-14.707c0,0,3.517-7.311,10.646-13.752C229.73,436.117,241.295,430.317,257.017,435.151z"/> |
|||
</g> |
|||
<path style="fill:#2B7C67;" d="M245.005,471.344h-0.048c-12.52-4.23-23.625-9.269-33.402-14.707c0,0,3.517-7.311,10.646-13.752 |
|||
C229.573,455.235,237.682,466.449,245.005,471.344z"/> |
|||
</svg> |
|||
@ -0,0 +1,19 @@ |
|||
{ |
|||
"name": "Asset_Remarks_GIS", |
|||
"label": "Asset_Remarks_GIS", |
|||
"type": "widget", |
|||
"version": "1.16.0", |
|||
"exbVersion": "1.16.0", |
|||
"author": "DIVYA LAWRANCE HMS", |
|||
"description": "", |
|||
"copyright": "", |
|||
"license": "http://www.apache.org/licenses/LICENSE-2.0", |
|||
"properties": {}, |
|||
"translatedLocales": [ |
|||
"en" |
|||
], |
|||
"defaultSize": { |
|||
"width": 800, |
|||
"height": 500 |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
import { type ImmutableObject } from 'seamless-immutable' |
|||
|
|||
export interface Config { |
|||
exampleConfigProperty: string |
|||
customizeLayers?: boolean |
|||
selectedLayerIds?: string[] |
|||
} |
|||
|
|||
export type IMConfig = ImmutableObject<Config> |
|||
@ -0,0 +1,204 @@ |
|||
// // BufferStreetLookup.ts
|
|||
// import geometryEngine from "@arcgis/core/geometry/geometryEngine";
|
|||
// import Query from "@arcgis/core/rest/support/Query";
|
|||
// import FeatureLayer from "@arcgis/core/layers/FeatureLayer";
|
|||
|
|||
// const carraigeWayLayer = new FeatureLayer({
|
|||
// url: "https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/Transporations/FeatureServer/1", // replace with your actual layer URL
|
|||
// outFields: ["road_name"]
|
|||
// });
|
|||
// // the below code fetches the road name under buffer
|
|||
// // export async function getStreetNamesFromBuffer(geometry): Promise<string[]> {
|
|||
// // const distances = [50, 100, 500, 1000]; // meters
|
|||
|
|||
// // for (let dist of distances) {
|
|||
// // const buffered = geometryEngine.buffer(geometry, dist, "meters");
|
|||
|
|||
// // const query = new Query({
|
|||
// // geometry: buffered,
|
|||
// // spatialRelationship: "intersects",
|
|||
// // returnGeometry: false,
|
|||
// // outFields: ["road_name"]
|
|||
// // });
|
|||
|
|||
// // try {
|
|||
// // const result = await carraigeWayLayer.queryFeatures(query);
|
|||
// // if (result.features?.length > 0) {
|
|||
// // const names = result.features.map(f => f.attributes.road_name).filter(Boolean);
|
|||
// // return Array.from(new Set(names)); // unique street names
|
|||
// // }
|
|||
// // } catch (err) {
|
|||
// // console.error(`Query failed for ${dist}m buffer`, err);
|
|||
// // }
|
|||
// // }
|
|||
|
|||
// // return []; // fallback if no results
|
|||
// // }
|
|||
// export async function getStreetNamesFromBuffer(geometry): Promise<any[]> {
|
|||
// const distances = [50, 100, 500, 1000]; // meters
|
|||
|
|||
// for (let dist of distances) {
|
|||
// const buffered = geometryEngine.buffer(geometry, dist, "meters");
|
|||
|
|||
// const query = new Query({
|
|||
// geometry: buffered,
|
|||
// spatialRelationship: "intersects",
|
|||
// returnGeometry: false,
|
|||
// outFields: ["road_name", "zone", "ward", "road_id"] // Add all required fields
|
|||
// });
|
|||
|
|||
// try {
|
|||
// const result = await carraigeWayLayer.queryFeatures(query);
|
|||
// if (result.features?.length > 0) {
|
|||
// // Return unique records by road_name
|
|||
// const seen = new Set();
|
|||
// const unique = [];
|
|||
|
|||
// for (const f of result.features) {
|
|||
// const name = f.attributes.road_name;
|
|||
// if (name && !seen.has(name)) {
|
|||
// seen.add(name);
|
|||
// unique.push(f.attributes); // return all attributes
|
|||
// }
|
|||
// }
|
|||
|
|||
// return unique;
|
|||
// }
|
|||
// } catch (err) {
|
|||
// console.error(`Query failed for ${dist}m buffer`, err);
|
|||
// }
|
|||
// }
|
|||
|
|||
// return []; // fallback if no results
|
|||
// }
|
|||
|
|||
|
|||
// import projection from '@arcgis/core/geometry/projection';
|
|||
// import SpatialReference from '@arcgis/core/geometry/SpatialReference';
|
|||
|
|||
// import geometryEngine from "@arcgis/core/geometry/geometryEngine";
|
|||
// import FeatureLayer from "@arcgis/core/layers/FeatureLayer";
|
|||
// import Query from "@arcgis/core/rest/support/Query";
|
|||
// const carraigeWayLayer = new FeatureLayer({
|
|||
// url: "https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/Transporations/FeatureServer/1", // replace with your actual layer URL
|
|||
// outFields: ["road_name"]
|
|||
// });
|
|||
// export async function getStreetNamesFromBuffer(geometry): Promise<any[]> {
|
|||
// // Always load the projection module before using it
|
|||
// await projection.load();
|
|||
|
|||
// // Project input geometry to Web Mercator (meters)
|
|||
// const projected = projection.project(
|
|||
// geometry,
|
|||
// new SpatialReference({ wkid: 3857 }) // Web Mercator uses meters
|
|||
// );
|
|||
|
|||
// const distances = [50, 100, 500, 1000];
|
|||
|
|||
// for (let dist of distances) {
|
|||
// const buffered = geometryEngine.buffer(projected, dist, 'meters');
|
|||
|
|||
// const query = new Query({
|
|||
// geometry: buffered,
|
|||
// spatialRelationship: 'intersects',
|
|||
// returnGeometry: false,
|
|||
// outFields: ['road_name', 'zone', 'ward', 'road_id']
|
|||
// });
|
|||
|
|||
// try {
|
|||
// const result = await carraigeWayLayer.queryFeatures(query);
|
|||
|
|||
// if (result.features?.length > 0) {
|
|||
// const seen = new Set();
|
|||
// const unique = [];
|
|||
|
|||
// for (const f of result.features) {
|
|||
// const name = f.attributes.road_name;
|
|||
// if (name && !seen.has(name)) {
|
|||
// seen.add(name);
|
|||
// unique.push(f.attributes);
|
|||
// }
|
|||
// }
|
|||
|
|||
// return unique;
|
|||
// }
|
|||
// } catch (err) {
|
|||
// console.error(`Query failed for ${dist}m buffer`, err);
|
|||
// }
|
|||
// }
|
|||
|
|||
// return [];
|
|||
// }
|
|||
|
|||
// import buffer from "@arcgis/core/geometry/support/buffer";
|
|||
import * as projection from "@arcgis/core/geometry/projection.js"; |
|||
import SpatialReference from "@arcgis/core/geometry/SpatialReference.js"; |
|||
import * as geometryEngineAsync from "@arcgis/core/geometry/geometryEngineAsync.js"; |
|||
import Query from "@arcgis/core/rest/support/Query.js"; |
|||
import FeatureLayer from "@arcgis/core/layers/FeatureLayer.js"; |
|||
|
|||
// Load your carriageway layer
|
|||
const carriageWayLayer = new FeatureLayer({ |
|||
url: "https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/Transporations/FeatureServer/1", |
|||
outFields: ["road_name", "zone", "ward", "road_id"] |
|||
}); |
|||
|
|||
export async function getStreetNamesFromBuffer(geometry): Promise<any[]> { |
|||
const distances = [10,25,50, 100, 500, 1000]; // in meters
|
|||
const targetSR = new SpatialReference({ wkid: 32644 }); // UTM zone 44N — use your local SR
|
|||
|
|||
let projectedGeom = geometry; |
|||
|
|||
try { |
|||
if (!projection.isLoaded()) { |
|||
await projection.load(); |
|||
} |
|||
|
|||
// ✅ Only project if geometry is not already in a linear spatial reference
|
|||
if (geometry.spatialReference?.isGeographic) { |
|||
projectedGeom = projection.project(geometry, targetSR); |
|||
if (!projectedGeom) { |
|||
console.warn("Projection failed."); |
|||
return []; |
|||
} |
|||
} |
|||
} catch (err) { |
|||
console.error("Projection error:", err); |
|||
return []; |
|||
} |
|||
|
|||
for (const dist of distances) { |
|||
try { |
|||
const buffered = await geometryEngineAsync.buffer(projectedGeom, dist, "meters"); |
|||
|
|||
const query = new Query({ |
|||
geometry: buffered, |
|||
spatialRelationship: "intersects", |
|||
returnGeometry: false, |
|||
outFields: ["road_name", "zone", "ward", "road_id"] |
|||
}); |
|||
|
|||
const result = await carriageWayLayer.queryFeatures(query); |
|||
|
|||
if (result.features?.length > 0) { |
|||
const seen = new Set(); |
|||
const unique = []; |
|||
|
|||
for (const f of result.features) { |
|||
const name = f.attributes.road_name; |
|||
if (name && !seen.has(name)) { |
|||
seen.add(name); |
|||
unique.push(f.attributes); |
|||
} |
|||
} |
|||
|
|||
return unique; |
|||
} |
|||
} catch (err) { |
|||
console.error(`Buffer/query failed at ${dist}m:`, err); |
|||
} |
|||
} |
|||
|
|||
return []; |
|||
} |
|||
|
|||
@ -0,0 +1,150 @@ |
|||
import ExpressionInfo from 'esri/form/ExpressionInfo' |
|||
|
|||
interface Config { |
|||
zoneLayerName?: string |
|||
wardLayerName?: string |
|||
roadLayerName?: string |
|||
buildingsLayerName?: string |
|||
buildingLayerName?: string |
|||
} |
|||
|
|||
export function getExpressions(config: Config) { |
|||
const zoneLayer = config.zoneLayerName || 'ZoneBoundary' |
|||
const wardLayer = config.wardLayerName || 'WardBoundary' |
|||
const roadLayer = config.roadLayerName || 'CarriageWay' |
|||
const buildingsLayer = config.buildingsLayerName || 'Buildings' |
|||
const updationLayer = config.buildingLayerName || 'building_updation_test' |
|||
|
|||
const zoneExpression = new ExpressionInfo({ |
|||
name: 'zone-expression', |
|||
title: 'Zone', |
|||
expression: `var intersectingFeatures = Intersects($feature, FeatureSetByName($map, "${zoneLayer}")); for (var f in intersectingFeatures) { return f.zone; }`, |
|||
returnType: 'string' |
|||
}) |
|||
const remarkexpresssion=new ExpressionInfo({ |
|||
|
|||
name: "always-true", |
|||
expression: "true", // Always evaluates to true
|
|||
returnType: "boolean" |
|||
}) |
|||
const wardExpression = new ExpressionInfo({ |
|||
name: 'ward-expression', |
|||
title: 'Ward', |
|||
expression: `var intersectingFeatures = Intersects($feature, FeatureSetByName($map, "${wardLayer}")); for (var f in intersectingFeatures) { return f.ward_no; }`, |
|||
returnType: 'string' |
|||
}) |
|||
|
|||
const roadIdExpression = new ExpressionInfo({ |
|||
name: 'road-id-expression', |
|||
title: 'Road ID', |
|||
returnType: 'string', |
|||
expression: `var bufferGeom = Buffer($feature, 0.0009, "degree"); var roads = Intersects(FeatureSetByName($map, "${roadLayer}"), bufferGeom); var minDist = Infinity; var nearest = null; for (var r in roads) { var dist = Distance($feature, r, "meters"); if (dist < minDist) { minDist = dist; nearest = r.road_id; } } return nearest;` |
|||
}) |
|||
|
|||
const gisIdExpression = new ExpressionInfo({ |
|||
name: 'gis-id-expression', |
|||
title: 'GIS ID', |
|||
returnType: 'string', |
|||
expression: `
|
|||
var categoryValue = $feature["building_status"]; |
|||
var existingGISID = ""; |
|||
|
|||
if (categoryValue == "Existing Building") { |
|||
var buildings = FeatureSetByName($map, "${buildingsLayer}"); |
|||
var intersecting = Intersects($feature, buildings); |
|||
for (var b in intersecting) { |
|||
return b["gcc_gis_id"]; |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
var updationLayer = FeatureSetByName($map, "${updationLayer}"); |
|||
var intersectingPoints = Intersects($feature, updationLayer); |
|||
var maxSerial = 0; |
|||
for (var pt in intersectingPoints) { |
|||
var id = pt["gis_id"]; |
|||
if (!IsEmpty(id)) { |
|||
var sn = Number(Mid(id, 9, 5)); |
|||
if (sn > maxSerial) { |
|||
maxSerial = sn; |
|||
} |
|||
} |
|||
} |
|||
|
|||
var inputWard = $feature["ward"]; |
|||
if (IsEmpty(inputWard)) { |
|||
var maxWard = 0; |
|||
for (var f in updationLayer) { |
|||
var w = Number(f["ward"]); |
|||
if (w > maxWard) { |
|||
maxWard = w; |
|||
} |
|||
} |
|||
inputWard = maxWard + 1; |
|||
} |
|||
|
|||
var wardText = Text(inputWard, "000"); |
|||
var yearText = Text(Year(Now()) % 100, "00"); |
|||
var serialNum = maxSerial + 1; |
|||
var serialText = Text(serialNum, "00000"); |
|||
var newGISID = "G-" + wardText + "-" + yearText + "-" + serialText; |
|||
|
|||
var exists = Count(Filter(updationLayer, "gis_id = @newGISID")) > 0; |
|||
while (exists) { |
|||
serialNum++; |
|||
serialText = Text(serialNum, "00000"); |
|||
newGISID = "G-" + wardText + "-" + yearText + "-" + serialText; |
|||
exists = Count(Filter(updationLayer, "gis_id = @newGISID")) > 0; |
|||
} |
|||
|
|||
return newGISID; |
|||
`
|
|||
}) |
|||
|
|||
const parentgisidExpression = new ExpressionInfo({ |
|||
name: 'parentgisid-expression', |
|||
title: 'Parent GIS ID', |
|||
returnType: 'string', |
|||
expression: `
|
|||
var buildings = FeatureSetByName($map, "${buildingsLayer}"); |
|||
var intersecting = Intersects($feature, buildings); |
|||
|
|||
var idCounts = {}; |
|||
var maxCount = 0; |
|||
var mostCommonId = null; |
|||
|
|||
for (var b in intersecting) { |
|||
var id = b["gcc_gis_id"]; |
|||
if (IsEmpty(id)) continue; |
|||
|
|||
if (HasKey(idCounts, id)) { |
|||
idCounts[id] += 1; |
|||
} else { |
|||
idCounts[id] = 1; |
|||
} |
|||
|
|||
if (idCounts[id] > maxCount) { |
|||
maxCount = idCounts[id]; |
|||
mostCommonId = id; |
|||
} |
|||
} |
|||
|
|||
return mostCommonId; |
|||
`
|
|||
|
|||
}) |
|||
const formTypeRequiredExpression = { |
|||
name: 'require-form-type', |
|||
expression: '$feature.form_type != null && $feature.form_type != ""' |
|||
} |
|||
|
|||
return { |
|||
zoneExpression, |
|||
remarkexpresssion, |
|||
wardExpression, |
|||
roadIdExpression, |
|||
gisIdExpression, |
|||
parentgisidExpression, |
|||
formTypeRequiredExpression |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
#editorContainer { |
|||
position: absolute; |
|||
/* height: 48%; */ |
|||
/* bottom: 2%; */ |
|||
width: 100%; |
|||
} |
|||
.export-button { |
|||
display: none !important; |
|||
} |
|||
@ -0,0 +1,811 @@ |
|||
// src/runtime/widget.tsx
|
|||
import React, { useEffect, useMemo, useRef, useState } from "react"; |
|||
import { AllWidgetProps } from "jimu-core"; |
|||
import FeatureLayer from "@arcgis/core/layers/FeatureLayer"; |
|||
import Graphic from "@arcgis/core/Graphic"; |
|||
|
|||
type AnyAttrs = Record<string, any>; |
|||
|
|||
type StatusState = { |
|||
type: "idle" | "loading" | "success" | "error"; |
|||
message?: string; |
|||
}; |
|||
|
|||
type CategoryOption = { |
|||
code: string; |
|||
label: string; |
|||
}; |
|||
|
|||
type SummaryRow = { |
|||
zone: string; |
|||
categoryCode: string; |
|||
categoryLabel: string; |
|||
count: number; |
|||
remarks: string; |
|||
}; |
|||
|
|||
const ASSET_POINT_URL = |
|||
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/Asset_2026_final/FeatureServer/0"; |
|||
|
|||
const SUMMARY_LAYER_URL = |
|||
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/gccdeptfield/Summary_Report/FeatureServer/1"; |
|||
|
|||
const ZONE_FIELD = "zone"; |
|||
const CATEGORY_FIELD = "category"; |
|||
const COUNT_FIELD = "total"; |
|||
const REMARKS_FIELD = "remarks_by_gis_expert"; |
|||
|
|||
function escSql(value: string | number | null | undefined) { |
|||
return String(value ?? "").replace(/'/g, "''").trim(); |
|||
} |
|||
|
|||
function normalizeText(value: any) { |
|||
return String(value ?? "").trim(); |
|||
} |
|||
|
|||
function isBlank(value: any) { |
|||
return value === null || value === undefined || String(value).trim() === ""; |
|||
} |
|||
|
|||
function uniqSorted(values: string[]) { |
|||
return Array.from(new Set(values.map((v) => normalizeText(v)).filter((v) => !isBlank(v)))).sort((a, b) => |
|||
a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }) |
|||
); |
|||
} |
|||
|
|||
function htmlEscape(value: any) { |
|||
return String(value ?? "") |
|||
.replace(/&/g, "&") |
|||
.replace(/</g, "<") |
|||
.replace(/>/g, ">") |
|||
.replace(/"/g, """) |
|||
.replace(/'/g, "'"); |
|||
} |
|||
|
|||
function parseApplyEditsResult(res: any) { |
|||
if (!res) return { added: 0, updated: 0, deleted: 0, errors: [] as any[] }; |
|||
|
|||
const addResults = res.addFeatureResults || res.addResults || []; |
|||
const updateResults = res.updateFeatureResults || res.updateResults || []; |
|||
const deleteResults = res.deleteFeatureResults || res.deleteResults || []; |
|||
|
|||
const okAdded = addResults.filter((r: any) => r && (r.success === true || Number.isFinite(r.objectId))).length; |
|||
const okUpdated = updateResults.filter((r: any) => r && (r.success === true || Number.isFinite(r.objectId))).length; |
|||
const okDeleted = deleteResults.filter((r: any) => r && (r.success === true || Number.isFinite(r.objectId))).length; |
|||
|
|||
const errors: any[] = []; |
|||
for (const arr of [addResults, updateResults, deleteResults]) { |
|||
for (const r of arr) { |
|||
if (r && r.error) errors.push(r.error); |
|||
} |
|||
} |
|||
|
|||
return { added: okAdded, updated: okUpdated, deleted: okDeleted, errors }; |
|||
} |
|||
|
|||
function getOidField(layer: FeatureLayer) { |
|||
return layer.objectIdField || "OBJECTID"; |
|||
} |
|||
|
|||
function isNumericFieldType(fieldType?: string) { |
|||
if (!fieldType) return false; |
|||
const t = fieldType.toLowerCase(); |
|||
return t.includes("integer") || t.includes("double") || t.includes("single") || t.includes("small-integer"); |
|||
} |
|||
|
|||
function sqlLiteral(value: string, fieldType?: string) { |
|||
const trimmed = String(value ?? "").trim(); |
|||
if (isNumericFieldType(fieldType)) { |
|||
const n = Number(trimmed); |
|||
if (!Number.isNaN(n)) return String(n); |
|||
} |
|||
return `'${escSql(trimmed)}'`; |
|||
} |
|||
|
|||
function makeWhereEquals(fieldName: string, value: string, fieldType?: string) { |
|||
return `${fieldName} = ${sqlLiteral(value, fieldType)}`; |
|||
} |
|||
|
|||
export default function AssetSummaryWidget(props: AllWidgetProps<any>) { |
|||
const assetLayerRef = useRef<FeatureLayer | null>(null); |
|||
const summaryLayerRef = useRef<FeatureLayer | null>(null); |
|||
const categoryDomainMapRef = useRef<Map<string, string>>(new Map()); |
|||
const categoryFieldTypeRef = useRef<string | undefined>(undefined); |
|||
const zoneFieldTypeRef = useRef<string | undefined>(undefined); |
|||
|
|||
const [selectedZone, setSelectedZone] = useState<string>(""); |
|||
const [zoneOptions, setZoneOptions] = useState<string[]>([]); |
|||
const [categoryOptions, setCategoryOptions] = useState<CategoryOption[]>([]); |
|||
const [selectedCategoryCode, setSelectedCategoryCode] = useState<string>(""); |
|||
const [remarks, setRemarks] = useState<string>(""); |
|||
const [totalCount, setTotalCount] = useState<number>(0); |
|||
const [pendingRows, setPendingRows] = useState<SummaryRow[]>([]); |
|||
const [status, setStatus] = useState<StatusState>({ type: "idle" }); |
|||
|
|||
const selectedCategoryLabel = useMemo(() => { |
|||
return categoryOptions.find((c) => c.code === selectedCategoryCode)?.label || selectedCategoryCode; |
|||
}, [categoryOptions, selectedCategoryCode]); |
|||
|
|||
const currentRow = useMemo<SummaryRow | null>(() => { |
|||
if (!selectedZone || selectedCategoryCode === "") return null; |
|||
return { |
|||
zone: selectedZone, |
|||
categoryCode: selectedCategoryCode, |
|||
categoryLabel: selectedCategoryLabel || selectedCategoryCode, |
|||
count: totalCount, |
|||
remarks: remarks || "", |
|||
}; |
|||
}, [selectedZone, selectedCategoryCode, selectedCategoryLabel, totalCount, remarks]); |
|||
|
|||
useEffect(() => { |
|||
assetLayerRef.current = new FeatureLayer({ url: ASSET_POINT_URL }); |
|||
summaryLayerRef.current = new FeatureLayer({ url: SUMMARY_LAYER_URL }); |
|||
}, []); |
|||
|
|||
useEffect(() => { |
|||
const init = async () => { |
|||
try { |
|||
setStatus({ type: "loading", message: "Loading layer metadata..." }); |
|||
await Promise.all([loadZoneOptions(), loadCategoryOptionsFromDomain()]); |
|||
setStatus({ type: "idle" }); |
|||
} catch (err) { |
|||
console.error("Widget init error:", err); |
|||
setStatus({ type: "error", message: "Failed to initialize widget" }); |
|||
} |
|||
}; |
|||
|
|||
init(); |
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|||
}, [assetLayerRef.current]); |
|||
|
|||
async function safeQueryFeatures(layer: FeatureLayer | null, where: string, outFields: string[] = ["*"]) { |
|||
if (!layer) return null; |
|||
if (!where || !where.trim()) return null; |
|||
|
|||
try { |
|||
const q = layer.createQuery(); |
|||
q.where = where; |
|||
q.outFields = outFields; |
|||
q.returnGeometry = false; |
|||
q.num = 2000; |
|||
return await layer.queryFeatures(q); |
|||
} catch (err) { |
|||
console.error("safeQueryFeatures error:", where, err); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
async function loadZoneOptions() { |
|||
const layer = assetLayerRef.current; |
|||
if (!layer) { |
|||
setZoneOptions([]); |
|||
return []; |
|||
} |
|||
|
|||
try { |
|||
await layer.load(); |
|||
const zoneField = layer.fields?.find((f) => f.name.toLowerCase() === ZONE_FIELD.toLowerCase()); |
|||
zoneFieldTypeRef.current = zoneField?.type; |
|||
|
|||
const q = layer.createQuery(); |
|||
q.where = "1=1"; |
|||
q.outFields = [ZONE_FIELD]; |
|||
q.returnGeometry = false; |
|||
q.returnDistinctValues = true; |
|||
|
|||
const res = await layer.queryFeatures(q); |
|||
const zones: string[] = []; |
|||
|
|||
for (const f of res?.features || []) { |
|||
const z = normalizeText(f.attributes?.[ZONE_FIELD]); |
|||
if (!isBlank(z)) zones.push(z); |
|||
} |
|||
|
|||
const sorted = uniqSorted(zones); |
|||
setZoneOptions(sorted); |
|||
return sorted; |
|||
} catch (err) { |
|||
console.error("loadZoneOptions error:", err); |
|||
setZoneOptions([]); |
|||
return []; |
|||
} |
|||
} |
|||
|
|||
async function loadCategoryOptionsFromDomain() { |
|||
const layer = assetLayerRef.current; |
|||
if (!layer) { |
|||
setCategoryOptions([]); |
|||
return []; |
|||
} |
|||
|
|||
try { |
|||
await layer.load(); |
|||
const categoryField = layer.fields?.find((f) => f.name.toLowerCase() === CATEGORY_FIELD.toLowerCase()); |
|||
categoryFieldTypeRef.current = categoryField?.type; |
|||
|
|||
const map = new Map<string, string>(); |
|||
const codedValues = (categoryField as any)?.domain?.codedValues || []; |
|||
|
|||
for (const cv of codedValues) { |
|||
const rawCode = cv?.code; |
|||
const code = normalizeText(rawCode); |
|||
const label = normalizeText(cv?.name || cv?.label || rawCode); |
|||
|
|||
if (!isBlank(code) || code === "0") { |
|||
map.set(code, label || code); |
|||
} |
|||
} |
|||
|
|||
categoryDomainMapRef.current = map; |
|||
|
|||
const fromDomain = Array.from(map.entries()) |
|||
.map(([code, label]) => ({ code, label })) |
|||
.sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true, sensitivity: "base" })); |
|||
|
|||
if (fromDomain.length) { |
|||
setCategoryOptions(fromDomain); |
|||
return fromDomain; |
|||
} |
|||
|
|||
// Fallback: if no domain exists, load distinct values from asset layer.
|
|||
const q = layer.createQuery(); |
|||
q.where = "1=1"; |
|||
q.outFields = [CATEGORY_FIELD]; |
|||
q.returnGeometry = false; |
|||
q.returnDistinctValues = true; |
|||
|
|||
const res = await layer.queryFeatures(q); |
|||
const options: CategoryOption[] = []; |
|||
for (const f of res?.features || []) { |
|||
const code = normalizeText(f.attributes?.[CATEGORY_FIELD]); |
|||
if (isBlank(code) && code !== "0") continue; |
|||
options.push({ |
|||
code, |
|||
label: code, |
|||
}); |
|||
} |
|||
|
|||
options.sort((a, b) => a.label.localeCompare(b.label, undefined, { numeric: true, sensitivity: "base" })); |
|||
setCategoryOptions(options); |
|||
return options; |
|||
} catch (err) { |
|||
console.error("loadCategoryOptionsFromDomain error:", err); |
|||
setCategoryOptions([]); |
|||
return []; |
|||
} |
|||
} |
|||
|
|||
async function loadCountForZoneCategory(zone: string, categoryCode: string) { |
|||
const layer = assetLayerRef.current; |
|||
if (!layer || isBlank(zone) || categoryCode === "") { |
|||
setTotalCount(0); |
|||
return 0; |
|||
} |
|||
|
|||
try { |
|||
const q = layer.createQuery(); |
|||
q.where = `${makeWhereEquals(ZONE_FIELD, zone, zoneFieldTypeRef.current)} AND ${makeWhereEquals( |
|||
CATEGORY_FIELD, |
|||
categoryCode, |
|||
categoryFieldTypeRef.current |
|||
)}`;
|
|||
q.returnGeometry = false; |
|||
const count = await layer.queryFeatureCount(q); |
|||
setTotalCount(count || 0); |
|||
return count || 0; |
|||
} catch (err) { |
|||
console.error("loadCountForZoneCategory error:", err); |
|||
setTotalCount(0); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
async function loadExistingSummary(zone: string, categoryCode: string) { |
|||
const layer = summaryLayerRef.current; |
|||
if (!layer || isBlank(zone) || categoryCode === "") { |
|||
setRemarks(""); |
|||
return; |
|||
} |
|||
|
|||
const where = |
|||
`${makeWhereEquals(ZONE_FIELD, zone, zoneFieldTypeRef.current)} AND ` + |
|||
`${makeWhereEquals(CATEGORY_FIELD, categoryCode, categoryFieldTypeRef.current)}`; |
|||
|
|||
const res = await safeQueryFeatures(layer, where, ["*"]); |
|||
if (res?.features?.length) { |
|||
const attrs = res.features[0].attributes || {}; |
|||
setRemarks(normalizeText(attrs[REMARKS_FIELD])); |
|||
} else { |
|||
setRemarks(""); |
|||
} |
|||
} |
|||
|
|||
async function handleZoneChange(zone: string) { |
|||
setSelectedZone(zone); |
|||
setSelectedCategoryCode(""); |
|||
setRemarks(""); |
|||
setTotalCount(0); |
|||
|
|||
if (!zone) return; |
|||
|
|||
try { |
|||
setStatus({ type: "loading", message: "Zone selected" }); |
|||
setStatus({ type: "success", message: "Zone loaded" }); |
|||
} catch (err) { |
|||
console.error("handleZoneChange error:", err); |
|||
setStatus({ type: "error", message: "Failed to load zone" }); |
|||
} finally { |
|||
setTimeout(() => setStatus({ type: "idle" }), 900); |
|||
} |
|||
} |
|||
|
|||
async function handleCategoryChange(categoryCode: string) { |
|||
setSelectedCategoryCode(categoryCode); |
|||
setRemarks(""); |
|||
setTotalCount(0); |
|||
|
|||
if (!selectedZone || categoryCode === "") return; |
|||
|
|||
try { |
|||
setStatus({ type: "loading", message: "Calculating count..." }); |
|||
await Promise.all([loadCountForZoneCategory(selectedZone, categoryCode), loadExistingSummary(selectedZone, categoryCode)]); |
|||
setStatus({ type: "success", message: "Category loaded" }); |
|||
} catch (err) { |
|||
console.error("handleCategoryChange error:", err); |
|||
setStatus({ type: "error", message: "Failed to load category" }); |
|||
} finally { |
|||
setTimeout(() => setStatus({ type: "idle" }), 900); |
|||
} |
|||
} |
|||
|
|||
function resetCurrentForm() { |
|||
setSelectedZone(""); |
|||
setSelectedCategoryCode(""); |
|||
setRemarks(""); |
|||
setTotalCount(0); |
|||
setStatus({ type: "idle" }); |
|||
} |
|||
|
|||
function resetAll() { |
|||
resetCurrentForm(); |
|||
setPendingRows([]); |
|||
} |
|||
|
|||
function addCurrentRowToList() { |
|||
if (!currentRow) { |
|||
alert("Please select zone and category."); |
|||
return; |
|||
} |
|||
|
|||
setPendingRows((prev) => { |
|||
const exists = prev.find((r) => r.zone === currentRow.zone && r.categoryCode === currentRow.categoryCode); |
|||
if (exists) { |
|||
return prev.map((r) => |
|||
r.zone === currentRow.zone && r.categoryCode === currentRow.categoryCode ? currentRow : r |
|||
); |
|||
} |
|||
return [...prev, currentRow]; |
|||
}); |
|||
setStatus({ type: "success", message: "Row added to list" }); |
|||
setTimeout(() => setStatus({ type: "idle" }), 900); |
|||
} |
|||
|
|||
function removePendingRow(index: number) { |
|||
setPendingRows((prev) => prev.filter((_, i) => i !== index)); |
|||
} |
|||
|
|||
async function upsertSummaryRow(row: SummaryRow) { |
|||
const layer = summaryLayerRef.current; |
|||
if (!layer) throw new Error("Summary layer is not ready."); |
|||
|
|||
const where = |
|||
`${makeWhereEquals(ZONE_FIELD, row.zone, zoneFieldTypeRef.current)} AND ` + |
|||
`${makeWhereEquals(CATEGORY_FIELD, row.categoryCode, categoryFieldTypeRef.current)}`; |
|||
|
|||
const existing = await safeQueryFeatures(layer, where, ["*"]); |
|||
|
|||
const attrs: AnyAttrs = { |
|||
[ZONE_FIELD]: row.zone, |
|||
[CATEGORY_FIELD]: row.categoryCode, |
|||
[COUNT_FIELD]: String(row.count ?? 0), |
|||
[REMARKS_FIELD]: row.remarks || "", |
|||
}; |
|||
|
|||
let result: any; |
|||
|
|||
if (existing?.features?.length) { |
|||
const found = existing.features[0]; |
|||
const oidField = getOidField(layer); |
|||
attrs[oidField] = found.attributes?.[oidField]; |
|||
|
|||
result = await layer.applyEdits({ |
|||
updateFeatures: [new Graphic({ attributes: attrs })], |
|||
}); |
|||
} else { |
|||
result = await layer.applyEdits({ |
|||
addFeatures: [new Graphic({ attributes: attrs })], |
|||
}); |
|||
} |
|||
|
|||
return parseApplyEditsResult(result); |
|||
} |
|||
|
|||
async function submitCurrent() { |
|||
if (!currentRow) { |
|||
alert("Please select zone and category."); |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
setStatus({ type: "loading", message: "Submitting current row..." }); |
|||
const result = await upsertSummaryRow(currentRow); |
|||
|
|||
if (result.added > 0 || result.updated > 0) { |
|||
setStatus({ type: "success", message: "Current row submitted" }); |
|||
} else { |
|||
setStatus({ type: "error", message: "Submit failed" }); |
|||
} |
|||
} catch (err) { |
|||
console.error("submitCurrent error:", err); |
|||
setStatus({ type: "error", message: "Submit error" }); |
|||
} finally { |
|||
setTimeout(() => setStatus({ type: "idle" }), 1000); |
|||
} |
|||
} |
|||
|
|||
async function submitAll() { |
|||
if (!pendingRows.length) { |
|||
alert("No rows in the list. Add one or more rows first."); |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
setStatus({ type: "loading", message: "Submitting all rows..." }); |
|||
let success = 0; |
|||
let failed = 0; |
|||
|
|||
for (const row of pendingRows) { |
|||
try { |
|||
const result = await upsertSummaryRow(row); |
|||
if (result.added > 0 || result.updated > 0) success += 1; |
|||
else failed += 1; |
|||
} catch (rowErr) { |
|||
console.error("submitAll row error:", rowErr); |
|||
failed += 1; |
|||
} |
|||
} |
|||
|
|||
if (failed === 0) { |
|||
setPendingRows([]); |
|||
setStatus({ type: "success", message: `Submitted ${success} row(s)` }); |
|||
} else { |
|||
setStatus({ type: "error", message: `Submitted ${success}, failed ${failed}` }); |
|||
} |
|||
} catch (err) { |
|||
console.error("submitAll error:", err); |
|||
setStatus({ type: "error", message: "Submit all error" }); |
|||
} finally { |
|||
setTimeout(() => setStatus({ type: "idle" }), 1200); |
|||
} |
|||
} |
|||
|
|||
function rowsForExport() { |
|||
if (pendingRows.length) return pendingRows; |
|||
if (currentRow) return [currentRow]; |
|||
return []; |
|||
} |
|||
|
|||
function downloadBlob(content: BlobPart, filename: string, mimeType: string) { |
|||
const blob = new Blob([content], { type: mimeType }); |
|||
const url = URL.createObjectURL(blob); |
|||
const a = document.createElement("a"); |
|||
a.href = url; |
|||
a.download = filename; |
|||
document.body.appendChild(a); |
|||
a.click(); |
|||
a.remove(); |
|||
URL.revokeObjectURL(url); |
|||
} |
|||
|
|||
function exportExcel() { |
|||
const rows = rowsForExport(); |
|||
if (!rows.length) { |
|||
alert("Nothing to export."); |
|||
return; |
|||
} |
|||
|
|||
const header = ["Zone", "Category", "Category Name", "Count", "Remarks"]; |
|||
const body = rows |
|||
.map( |
|||
(r) => |
|||
`<tr>
|
|||
<td>${htmlEscape(r.zone)}</td> |
|||
<td>${htmlEscape(r.categoryCode)}</td> |
|||
<td>${htmlEscape(r.categoryLabel)}</td> |
|||
<td>${htmlEscape(r.count)}</td> |
|||
<td>${htmlEscape(r.remarks)}</td> |
|||
</tr>`
|
|||
) |
|||
.join(""); |
|||
|
|||
const html = `
|
|||
<html> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
<style> |
|||
body { font-family: Arial, sans-serif; padding: 16px; } |
|||
h2 { margin: 0 0 12px 0; } |
|||
table { border-collapse: collapse; width: 100%; } |
|||
th, td { border: 1px solid #999; padding: 6px 8px; font-size: 12px; vertical-align: top; } |
|||
th { background: #f2f2f2; } |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<h2>Asset Summary</h2> |
|||
<table> |
|||
<thead> |
|||
<tr> |
|||
${header.map((h) => `<th>${h}</th>`).join("")} |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
${body} |
|||
</tbody> |
|||
</table> |
|||
</body> |
|||
</html> |
|||
`.trim();
|
|||
|
|||
downloadBlob( |
|||
`\ufeff${html}`, |
|||
`asset_summary_${new Date().toISOString().slice(0, 10)}.xls`, |
|||
"application/vnd.ms-excel" |
|||
); |
|||
} |
|||
|
|||
function exportPdf() { |
|||
const rows = rowsForExport(); |
|||
if (!rows.length) { |
|||
alert("Nothing to export."); |
|||
return; |
|||
} |
|||
|
|||
const printWindow = window.open("", "_blank", "width=1000,height=720"); |
|||
if (!printWindow) { |
|||
alert("Popup blocked. Please allow popups for PDF export."); |
|||
return; |
|||
} |
|||
|
|||
const htmlRows = rows |
|||
.map( |
|||
(r) => `
|
|||
<tr> |
|||
<td>${htmlEscape(r.zone)}</td> |
|||
<td>${htmlEscape(r.categoryCode)}</td> |
|||
<td>${htmlEscape(r.categoryLabel)}</td> |
|||
<td>${htmlEscape(r.count)}</td> |
|||
<td>${htmlEscape(r.remarks)}</td> |
|||
</tr>`
|
|||
) |
|||
.join(""); |
|||
|
|||
const html = `
|
|||
<html> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
<title>Asset Summary</title> |
|||
<style> |
|||
body { font-family: Arial, sans-serif; padding: 18px; } |
|||
h2 { margin: 0 0 12px 0; } |
|||
.meta { margin-bottom: 12px; font-size: 13px; } |
|||
table { border-collapse: collapse; width: 100%; } |
|||
th, td { border: 1px solid #000; padding: 6px 8px; font-size: 12px; vertical-align: top; } |
|||
th { background: #efefef; } |
|||
@media print { |
|||
body { padding: 0; } |
|||
} |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<h2>Asset Summary</h2> |
|||
<div class="meta">Generated on ${new Date().toLocaleString()}</div> |
|||
<table> |
|||
<thead> |
|||
<tr> |
|||
<th>Zone</th> |
|||
<th>Category</th> |
|||
<th>Category Name</th> |
|||
<th>Count</th> |
|||
<th>Remarks</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
${htmlRows} |
|||
</tbody> |
|||
</table> |
|||
<script> |
|||
window.onload = function () { |
|||
window.focus(); |
|||
window.print(); |
|||
setTimeout(function () { window.close(); }, 250); |
|||
}; |
|||
</script> |
|||
</body> |
|||
</html> |
|||
`.trim();
|
|||
|
|||
printWindow.document.open(); |
|||
printWindow.document.write(html); |
|||
printWindow.document.close(); |
|||
} |
|||
|
|||
async function reloadCountAndRemarks() { |
|||
if (!selectedZone || selectedCategoryCode === "") return; |
|||
await Promise.all([ |
|||
loadCountForZoneCategory(selectedZone, selectedCategoryCode), |
|||
loadExistingSummary(selectedZone, selectedCategoryCode), |
|||
]); |
|||
} |
|||
|
|||
useEffect(() => { |
|||
if (!selectedZone || selectedCategoryCode === "") { |
|||
setTotalCount(0); |
|||
return; |
|||
} |
|||
|
|||
(async () => { |
|||
try { |
|||
setStatus({ type: "loading", message: "Refreshing count..." }); |
|||
await reloadCountAndRemarks(); |
|||
setStatus({ type: "idle" }); |
|||
} catch (err) { |
|||
console.error("reload effect error:", err); |
|||
setStatus({ type: "error", message: "Failed to refresh count" }); |
|||
} |
|||
})(); |
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|||
}, [selectedZone, selectedCategoryCode]); |
|||
|
|||
return ( |
|||
<div style={{ padding: 12, maxWidth: 1100, fontFamily: "Segoe UI, Roboto, Arial" }}> |
|||
<h3 style={{ margin: "0 0 10px 0" }}>Zone - Category Asset Summary</h3> |
|||
|
|||
<div style={{ background: "#fff", padding: 12, borderRadius: 8, border: "1px solid #e8e8e8" }}> |
|||
<div style={{ marginBottom: 10 }}> |
|||
<strong>Status:</strong>{" "} |
|||
{status.type === "idle" |
|||
? "idle" |
|||
: status.type === "loading" |
|||
? `working — ${status.message || ""}` |
|||
: status.message} |
|||
</div> |
|||
|
|||
<div style={{ display: "flex", gap: 8, marginBottom: 12, flexWrap: "wrap" }}> |
|||
<button onClick={addCurrentRowToList} disabled={!selectedZone || selectedCategoryCode === ""}> |
|||
Add Single / Add to List |
|||
</button> |
|||
<button onClick={submitCurrent} disabled={!selectedZone || selectedCategoryCode === ""}> |
|||
Submit Single |
|||
</button> |
|||
<button onClick={submitAll} disabled={!pendingRows.length}> |
|||
Submit Multiple |
|||
</button> |
|||
<button onClick={resetCurrentForm}>Reset Form</button> |
|||
<button onClick={resetAll}>Reset All</button> |
|||
<button onClick={exportExcel}>Export Excel</button> |
|||
<button onClick={exportPdf}>Export PDF</button> |
|||
</div> |
|||
|
|||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10 }}> |
|||
<div> |
|||
<label style={{ fontSize: 12, color: "#444" }}>Zone</label> |
|||
<select |
|||
value={selectedZone} |
|||
onChange={(e) => handleZoneChange(e.target.value)} |
|||
style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }} |
|||
> |
|||
<option value="">-- select zone --</option> |
|||
{zoneOptions.map((z) => ( |
|||
<option key={z} value={z}> |
|||
{z} |
|||
</option> |
|||
))} |
|||
</select> |
|||
</div> |
|||
|
|||
<div> |
|||
<label style={{ fontSize: 12, color: "#444" }}>Category (Domain Name)</label> |
|||
<select |
|||
value={selectedCategoryCode} |
|||
onChange={(e) => handleCategoryChange(e.target.value)} |
|||
disabled={!selectedZone} |
|||
style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }} |
|||
> |
|||
<option value="">-- select category --</option> |
|||
{categoryOptions.map((c) => ( |
|||
<option key={c.code} value={c.code}> |
|||
{c.label} |
|||
</option> |
|||
))} |
|||
</select> |
|||
</div> |
|||
|
|||
<div> |
|||
<label style={{ fontSize: 12, color: "#444" }}>Total Count</label> |
|||
<input |
|||
value={String(totalCount)} |
|||
readOnly |
|||
style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd" }} |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
<div style={{ marginTop: 10 }}> |
|||
<label style={{ display: "block", fontSize: 12, color: "#444", marginBottom: 6 }}> |
|||
Remarks by GIS Expert |
|||
</label> |
|||
<textarea |
|||
value={remarks} |
|||
onChange={(e) => setRemarks(e.target.value)} |
|||
disabled={!selectedZone || selectedCategoryCode === ""} |
|||
rows={4} |
|||
style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid #ddd", resize: "vertical" }} |
|||
placeholder="Enter remarks here" |
|||
/> |
|||
</div> |
|||
|
|||
<div style={{ marginTop: 12 }}> |
|||
<div style={{ fontSize: 13, marginBottom: 6 }}> |
|||
<strong> |
|||
{selectedZone && selectedCategoryCode !== "" |
|||
? `Zone ${selectedZone} | ${selectedCategoryLabel} | Count ${totalCount}` |
|||
: "Select zone and category"} |
|||
</strong> |
|||
</div> |
|||
|
|||
<div style={{ maxHeight: 280, overflow: "auto", border: "1px solid #eee", borderRadius: 6 }}> |
|||
<table style={{ width: "100%", borderCollapse: "collapse" }}> |
|||
<thead> |
|||
<tr style={{ background: "#f7f7f7" }}> |
|||
<th style={{ textAlign: "left", padding: 8, borderBottom: "1px solid #eee" }}>Zone</th> |
|||
<th style={{ textAlign: "left", padding: 8, borderBottom: "1px solid #eee" }}>Category</th> |
|||
<th style={{ textAlign: "left", padding: 8, borderBottom: "1px solid #eee" }}>Category Name</th> |
|||
<th style={{ textAlign: "left", padding: 8, borderBottom: "1px solid #eee" }}>Count</th> |
|||
<th style={{ textAlign: "left", padding: 8, borderBottom: "1px solid #eee" }}>Remarks</th> |
|||
<th style={{ textAlign: "left", padding: 8, borderBottom: "1px solid #eee" }}>Action</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
{pendingRows.length === 0 ? ( |
|||
<tr> |
|||
<td colSpan={6} style={{ padding: 8, color: "#666" }}> |
|||
Add single or multiple rows to build the submission list. |
|||
</td> |
|||
</tr> |
|||
) : ( |
|||
pendingRows.map((row, idx) => ( |
|||
<tr key={`${row.zone}-${row.categoryCode}-${idx}`}> |
|||
<td style={{ padding: 8, borderBottom: "1px solid #f2f2f2" }}>{row.zone}</td> |
|||
<td style={{ padding: 8, borderBottom: "1px solid #f2f2f2" }}>{row.categoryCode}</td> |
|||
<td style={{ padding: 8, borderBottom: "1px solid #f2f2f2" }}>{row.categoryLabel}</td> |
|||
<td style={{ padding: 8, borderBottom: "1px solid #f2f2f2" }}>{row.count}</td> |
|||
<td style={{ padding: 8, borderBottom: "1px solid #f2f2f2" }}>{row.remarks}</td> |
|||
<td style={{ padding: 8, borderBottom: "1px solid #f2f2f2" }}> |
|||
<button onClick={() => removePendingRow(idx)}>Remove</button> |
|||
</td> |
|||
</tr> |
|||
)) |
|||
)} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
|
|||
<div style={{ marginTop: 12, fontSize: 12, color: "#666" }}> |
|||
Category domain values are loaded from the asset layer, including code 0. Zero counts are allowed and can be |
|||
submitted, exported, and kept in the list. |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
import { React } from 'jimu-core' |
|||
import { type AllWidgetSettingProps } from 'jimu-for-builder' |
|||
import { MapWidgetSelector } from 'jimu-ui/advanced/setting-components' |
|||
|
|||
const Setting = (props: AllWidgetSettingProps<any>) => { |
|||
const onMapWidgetSelected = (useMapWidgetIds: string[]) => { |
|||
props.onSettingChange({ |
|||
id: props.id, |
|||
useMapWidgetIds: useMapWidgetIds |
|||
}) |
|||
} |
|||
return <div className="widget-setting-demo"> |
|||
<MapWidgetSelector useMapWidgetIds={props.useMapWidgetIds} onSelect={onMapWidgetSelected} /> |
|||
</div> |
|||
} |
|||
|
|||
export default Setting |
|||
|
|||
// /** @jsx jsx */
|
|||
// import { React, jsx } from 'jimu-core'
|
|||
// import { AllWidgetSettingProps } from 'jimu-for-builder'
|
|||
// import { MapWidgetSelector, DataSourceSelector } from 'jimu-ui/advanced/setting-components'
|
|||
// import { IMUseDataSource } from 'jimu-core'
|
|||
|
|||
// const Setting = (props: AllWidgetSettingProps<any>) => {
|
|||
// const { useMapWidgetIds, useDataSources, config, onSettingChange, id } = props
|
|||
|
|||
// const onMapWidgetSelected = (ids: string[]) => {
|
|||
// onSettingChange({
|
|||
// id,
|
|||
// useMapWidgetIds: ids,
|
|||
// useDataSources: [],
|
|||
// config: {
|
|||
// ...config,
|
|||
// layerDataSource: null
|
|||
// }
|
|||
// })
|
|||
// }
|
|||
|
|||
// const onLayerSelected = (useDataSources: IMUseDataSource[]) => {
|
|||
// onSettingChange({
|
|||
// id,
|
|||
// useDataSources,
|
|||
// config: {
|
|||
// ...config,
|
|||
// layerDataSource: useDataSources?.[0] || null
|
|||
// }
|
|||
// })
|
|||
// }
|
|||
|
|||
// return (
|
|||
// <div className="widget-setting-custom">
|
|||
// <MapWidgetSelector
|
|||
// useMapWidgetIds={useMapWidgetIds}
|
|||
// onSelect={onMapWidgetSelected}
|
|||
// />
|
|||
|
|||
// {useMapWidgetIds?.length > 0 && (
|
|||
// <DataSourceSelector
|
|||
// useMapWidgetIds={useMapWidgetIds}
|
|||
// mustUseMap={true}
|
|||
// types={['FeatureLayer']}
|
|||
// useDataSources={useDataSources}
|
|||
// onChange={onLayerSelected}
|
|||
// multiple={false}
|
|||
// />
|
|||
// )}
|
|||
// </div>
|
|||
// )
|
|||
// }
|
|||
|
|||
// export default Setting
|
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"exampleConfigProperty": "test" |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> |
|||
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" |
|||
width="800px" height="800px" viewBox="0 0 52 52" enable-background="new 0 0 52 52" xml:space="preserve"> |
|||
<path d="M44,4H8C5.8,4,4,5.8,4,8v36c0,2.2,1.8,4,4,4h36c2.2,0,4-1.8,4-4V8C48,5.8,46.2,4,44,4z M12,14 |
|||
c0-0.6,0.4-1,1-1h10c0.6,0,1,0.4,1,1v10c0,0.6-0.4,1-1,1H13c-0.6,0-1-0.4-1-1V14z M36,40c0,0.6-0.4,1-1,1H13c-0.6,0-1-0.4-1-1v-2 |
|||
c0-0.6,0.4-1,1-1h22c0.6,0,1,0.4,1,1V40z M40,32c0,0.6-0.4,1-1,1H13c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1h26c0.6,0,1,0.4,1,1V32z |
|||
M40,24c0,0.6-0.4,1-1,1H29c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1h10c0.6,0,1,0.4,1,1V24z M40,16c0,0.6-0.4,1-1,1H29 |
|||
c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1h10c0.6,0,1,0.4,1,1V16z"/> |
|||
</svg> |
|||
@ -0,0 +1,29 @@ |
|||
{ |
|||
"name": "Asset_Road_Summary", |
|||
"label": "Asset_Road_Summary", |
|||
"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": { |
|||
"hasSettingPage": false |
|||
}, |
|||
"widgets": [ |
|||
{ |
|||
"uri": "Asset_Road_Summary", |
|||
"label": "Asset_Road_Summary", |
|||
"thumbnail": "thumbnail.png", |
|||
"description": "Summarizes Asset and Road." |
|||
} |
|||
], |
|||
"framework": "react", |
|||
"translatedLocales": ["en"], |
|||
"dataSources": [], |
|||
"elements": [] |
|||
} |
|||
|
|||
|
|||
|
|||
@ -0,0 +1,7 @@ |
|||
import { type ImmutableObject } from 'seamless-immutable' |
|||
|
|||
export interface Config { |
|||
exampleConfigProperty: string |
|||
} |
|||
|
|||
export type IMConfig = ImmutableObject<Config> |
|||
@ -0,0 +1,317 @@ |
|||
import React, { useEffect, useState } from 'react'; |
|||
import FeatureLayer from '@arcgis/core/layers/FeatureLayer'; |
|||
import Query from "@arcgis/core/rest/support/Query"; |
|||
import { |
|||
Table, TableBody, TableCell, TableContainer, |
|||
TableHead, TableRow, Paper, Select, MenuItem, Button, Grid |
|||
} from '@mui/material'; |
|||
|
|||
// NOTE: install these two packages in your project for Excel/PDF export
|
|||
// npm i xlsx jspdf jspdf-autotable
|
|||
import * as XLSX from 'xlsx'; |
|||
import jsPDF from "jspdf"; |
|||
import autoTable from "jspdf-autotable"; |
|||
|
|||
// Combined Zone-Ward Asset Summary component
|
|||
// Updated: removed hard-coded allowedCategories. The component now
|
|||
// uses the asset layer's category domain (codedValues) to resolve
|
|||
// category descriptions and includes whatever descriptions exist in the domain.
|
|||
|
|||
const CombinedZoneWardSummary = () => { |
|||
const [zone, setZone] = useState(''); |
|||
const [ward, setWard] = useState(''); |
|||
const [zoneList, setZoneList] = useState([]); |
|||
const [wardList, setWardList] = useState([]); |
|||
const [detailedData, setDetailedData] = useState([]); // { zone, ward, category, count }
|
|||
const [zoneSummary, setZoneSummary] = useState([]); // { zone, category, count }
|
|||
const [categoryDomain, setCategoryDomain] = useState({}); |
|||
|
|||
const assetLayerUrl = "https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/Final_Asset_Location/FeatureServer/0"; |
|||
|
|||
useEffect(() => { |
|||
fetchZoneList(); |
|||
fetchCategoryDomain(); |
|||
}, []); |
|||
|
|||
useEffect(() => { |
|||
if (zone) fetchWardList(zone); |
|||
}, [zone]); |
|||
|
|||
// Fetch distinct zones
|
|||
const fetchZoneList = async () => { |
|||
try { |
|||
const layer = new FeatureLayer({ url: assetLayerUrl }); |
|||
const query = layer.createQuery(); |
|||
query.outFields = ["zone_no"]; |
|||
query.returnDistinctValues = true; |
|||
query.returnGeometry = false; |
|||
const result = await layer.queryFeatures(query); |
|||
const zones = [...new Set(result.features.map(f => f.attributes.zone_no))].sort(); |
|||
setZoneList(zones); |
|||
} catch (err) { |
|||
console.error('fetchZoneList error', err); |
|||
} |
|||
}; |
|||
|
|||
const fetchWardList = async (zoneNo) => { |
|||
try { |
|||
const layer = new FeatureLayer({ url: assetLayerUrl }); |
|||
const query = layer.createQuery(); |
|||
query.where = `zone_no = '${zoneNo}'`; |
|||
query.outFields = ["ward_no"]; |
|||
query.returnDistinctValues = true; |
|||
query.returnGeometry = false; |
|||
const result = await layer.queryFeatures(query); |
|||
const wards = [...new Set(result.features.map(f => f.attributes.ward_no))].sort(); |
|||
setWardList(wards); |
|||
} catch (err) { |
|||
console.error('fetchWardList error', err); |
|||
} |
|||
}; |
|||
|
|||
// Load category domain and set mapping { code: description }
|
|||
// returns the mapping so callers can await it
|
|||
const fetchCategoryDomain = async () => { |
|||
try { |
|||
const layer = new FeatureLayer({ url: assetLayerUrl }); |
|||
await layer.load(); |
|||
const field = layer.fields.find(f => f.name === "category"); |
|||
if (field?.domain?.codedValues) { |
|||
const map = Object.fromEntries(field.domain.codedValues.map(cv => [cv.code, cv.name])); |
|||
setCategoryDomain(map); |
|||
return map; |
|||
} |
|||
setCategoryDomain({}); |
|||
return {}; |
|||
} catch (err) { |
|||
console.error('fetchCategoryDomain error', err); |
|||
setCategoryDomain({}); |
|||
return {}; |
|||
} |
|||
}; |
|||
|
|||
// Ensure category domain is loaded before summarizing so we use descriptions
|
|||
const handleFetch = async () => { |
|||
if (!zone) return; |
|||
await fetchCategoryDomain(); |
|||
await fetchAssetSummaries(zone, ward); |
|||
}; |
|||
|
|||
const handleClear = () => { |
|||
setZone(''); |
|||
setWard(''); |
|||
setWardList([]); |
|||
setDetailedData([]); |
|||
setZoneSummary([]); |
|||
}; |
|||
|
|||
// Build detailed and zone-aggregated summaries using domain descriptions
|
|||
const fetchAssetSummaries = async (zoneNo, wardNo) => { |
|||
try { |
|||
const layer = new FeatureLayer({ url: assetLayerUrl }); |
|||
const query = layer.createQuery(); |
|||
query.where = `zone_no='${zoneNo}'` + (wardNo ? ` AND ward_no='${wardNo}'` : ""); |
|||
query.outFields = ['zone_no', 'ward_no', 'category']; |
|||
query.returnGeometry = false; |
|||
|
|||
const result = await layer.queryFeatures(query); |
|||
|
|||
// counts keyed by "zone|ward|category" and "zone|category"
|
|||
const dCounts = {}; |
|||
const zCounts = {}; |
|||
|
|||
result.features.forEach(({ attributes }) => { |
|||
const zoneVal = attributes.zone_no; |
|||
const wardVal = attributes.ward_no || ''; |
|||
const catCode = attributes.category; |
|||
|
|||
// Resolve to domain description when available; fall back to raw code/string
|
|||
const name = (categoryDomain && categoryDomain.hasOwnProperty(catCode)) ? categoryDomain[catCode] : String(catCode || '').trim(); |
|||
|
|||
// Use whatever description is available in the domain — no hard-coded filter
|
|||
const dKey = `${zoneVal}|${wardVal}|${name}`; |
|||
dCounts[dKey] = (dCounts[dKey] || 0) + 1; |
|||
|
|||
const zKey = `${zoneVal}|${name}`; |
|||
zCounts[zKey] = (zCounts[zKey] || 0) + 1; |
|||
}); |
|||
|
|||
const detailed = Object.entries(dCounts).map(([k, v]) => { |
|||
const [z, w, c] = k.split('|'); |
|||
return { zone: z, ward: w, category: c, count: v }; |
|||
}).sort((a, b) => (a.zone + a.ward + a.category).localeCompare(b.zone + b.ward + b.category)); |
|||
|
|||
const zoneAgg = Object.entries(zCounts).map(([k, v]) => { |
|||
const [z, c] = k.split('|'); |
|||
return { zone: z, category: c, count: v }; |
|||
}).sort((a, b) => (a.zone + a.category).localeCompare(b.zone + b.category)); |
|||
|
|||
setDetailedData(detailed); |
|||
setZoneSummary(zoneAgg); |
|||
} catch (err) { |
|||
console.error('fetchAssetSummaries error', err); |
|||
setDetailedData([]); |
|||
setZoneSummary([]); |
|||
} |
|||
}; |
|||
|
|||
// Export helpers
|
|||
const exportExcel = () => { |
|||
const wb = XLSX.utils.book_new(); |
|||
|
|||
// Detailed sheet
|
|||
const detailedSheetData = [ ["Sl.No", "Zone", "Ward", "Category", "Count"] ]; |
|||
detailedData.forEach((r, i) => detailedSheetData.push([i + 1, r.zone, r.ward, r.category, r.count])); |
|||
const ws1 = XLSX.utils.aoa_to_sheet(detailedSheetData); |
|||
XLSX.utils.book_append_sheet(wb, ws1, 'Detailed'); |
|||
|
|||
// Zone summary sheet
|
|||
const zoneSheetData = [ ["Sl.No", "Zone", "Category", "Count"] ]; |
|||
zoneSummary.forEach((r, i) => zoneSheetData.push([i + 1, r.zone, r.category, r.count])); |
|||
const ws2 = XLSX.utils.aoa_to_sheet(zoneSheetData); |
|||
XLSX.utils.book_append_sheet(wb, ws2, 'ZoneSummary'); |
|||
|
|||
XLSX.writeFile(wb, `Asset_Summary_Zone_${zone || 'All'}.xlsx`); |
|||
}; |
|||
|
|||
const exportPDF = () => { |
|||
const doc = new jsPDF({ |
|||
orientation: "portrait", |
|||
unit: "pt", |
|||
format: "a4" |
|||
}); |
|||
|
|||
// ===== Detailed Table =====
|
|||
const detailedColumns = ["Sl.No", "Zone", "Ward", "Category", "Count"]; |
|||
const detailedRows = detailedData.map((r, i) => [ |
|||
i + 1, |
|||
r.zone, |
|||
r.ward, |
|||
r.category, |
|||
r.count |
|||
]); |
|||
|
|||
doc.setFontSize(14); |
|||
doc.text(`Asset Summary - Detailed (Zone ${zone || "All"})`, 40, 40); |
|||
|
|||
autoTable(doc, { |
|||
head: [detailedColumns], |
|||
body: detailedRows, |
|||
startY: 60, |
|||
styles: { fontSize: 8 }, |
|||
}); |
|||
|
|||
// ===== Zone Summary Table =====
|
|||
doc.addPage(); |
|||
|
|||
const zoneColumns = ["Sl.No", "Zone", "Category", "Count"]; |
|||
const zoneRows = zoneSummary.map((r, i) => [ |
|||
i + 1, |
|||
r.zone, |
|||
r.category, |
|||
r.count |
|||
]); |
|||
|
|||
doc.text(`Asset Summary - Zone Aggregated (Zone ${zone || "All"})`, 40, 40); |
|||
|
|||
autoTable(doc, { |
|||
head: [zoneColumns], |
|||
body: zoneRows, |
|||
startY: 60, |
|||
styles: { fontSize: 8 }, |
|||
}); |
|||
|
|||
doc.save(`Asset_Summary_Zone_${zone || "All"}.pdf`); |
|||
}; |
|||
|
|||
return ( |
|||
<div style={{ padding: '1rem', height: '100%', overflow: 'hidden' }}> |
|||
<h2>Zone-wise Ward Asset Summary (Exports: Excel / PDF)</h2> |
|||
<div style={{ marginBottom: '10px', fontWeight: 600 }}> |
|||
Total Assets (Detailed): {detailedData.reduce((s, r) => s + r.count, 0)} | |
|||
Total Assets (Zone): {zoneSummary.reduce((s, r) => s + r.count, 0)} |
|||
</div> |
|||
<Grid container spacing={2} alignItems="center"> |
|||
<Grid item> |
|||
<Select value={zone} onChange={e => setZone(e.target.value)} displayEmpty style={{ width: 150 }}> |
|||
<MenuItem value="">Select Zone</MenuItem> |
|||
{zoneList.map(z => <MenuItem key={z} value={z}>{z}</MenuItem>)} |
|||
</Select> |
|||
</Grid> |
|||
<Grid item> |
|||
<Select value={ward} onChange={e => setWard(e.target.value)} displayEmpty style={{ width: 150 }} disabled={!zone}> |
|||
<MenuItem value="">All Wards</MenuItem> |
|||
{wardList.map(w => <MenuItem key={w} value={w}>{w}</MenuItem>)} |
|||
</Select> |
|||
</Grid> |
|||
<Grid item> |
|||
<Button variant="contained" color="primary" onClick={handleFetch}>FETCH DATA</Button> |
|||
</Grid> |
|||
<Grid item> |
|||
<Button variant="outlined" color="secondary" onClick={handleClear}>CLEAR</Button> |
|||
</Grid> |
|||
<Grid item> |
|||
<Button variant="contained" onClick={exportExcel} disabled={detailedData.length === 0 && zoneSummary.length === 0}>Export Excel</Button> |
|||
</Grid> |
|||
<Grid item> |
|||
<Button variant="contained" onClick={exportPDF} disabled={detailedData.length === 0 && zoneSummary.length === 0}>Export PDF</Button> |
|||
</Grid> |
|||
</Grid> |
|||
|
|||
{detailedData.length > 0 && ( |
|||
<TableContainer component={Paper} style={{ marginTop: '20px', maxHeight: 300, maxWidth: '100%', overflow: 'auto' }}> |
|||
<Table size="small" stickyHeader> |
|||
<TableHead> |
|||
<TableRow> |
|||
<TableCell><strong>Sl.No</strong></TableCell> |
|||
<TableCell><strong>Zone</strong></TableCell> |
|||
<TableCell><strong>Ward</strong></TableCell> |
|||
<TableCell><strong>Category</strong></TableCell> |
|||
<TableCell><strong>Count</strong></TableCell> |
|||
</TableRow> |
|||
</TableHead> |
|||
<TableBody> |
|||
{detailedData.map((r, i) => ( |
|||
<TableRow key={i}> |
|||
<TableCell>{i + 1}</TableCell> |
|||
<TableCell>{r.zone}</TableCell> |
|||
<TableCell>{r.ward}</TableCell> |
|||
<TableCell>{r.category}</TableCell> |
|||
<TableCell>{r.count}</TableCell> |
|||
</TableRow> |
|||
))} |
|||
</TableBody> |
|||
</Table> |
|||
</TableContainer> |
|||
)} |
|||
|
|||
{zoneSummary.length > 0 && ( |
|||
<TableContainer component={Paper} style={{ marginTop: '20px', maxHeight: 300, maxWidth: '100%', overflow: 'auto' }}> |
|||
<Table size="small" stickyHeader> |
|||
<TableHead> |
|||
<TableRow> |
|||
<TableCell><strong>Sl.No</strong></TableCell> |
|||
<TableCell><strong>Zone</strong></TableCell> |
|||
<TableCell><strong>Category</strong></TableCell> |
|||
<TableCell><strong>Count</strong></TableCell> |
|||
</TableRow> |
|||
</TableHead> |
|||
<TableBody> |
|||
{zoneSummary.map((r, i) => ( |
|||
<TableRow key={i}> |
|||
<TableCell>{i + 1}</TableCell> |
|||
<TableCell>{r.zone}</TableCell> |
|||
<TableCell>{r.category}</TableCell> |
|||
<TableCell>{r.count}</TableCell> |
|||
</TableRow> |
|||
))} |
|||
</TableBody> |
|||
</Table> |
|||
</TableContainer> |
|||
)} |
|||
|
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default CombinedZoneWardSummary; |
|||
@ -0,0 +1,8 @@ |
|||
import { React, AllWidgetProps } from 'jimu-core'; |
|||
import ZoneAbstractWidget from '../index'; |
|||
|
|||
const Widget = (props: AllWidgetProps<any>) => { |
|||
return <ZoneAbstractWidget {...props} />; |
|||
}; |
|||
|
|||
export default Widget; |
|||
@ -0,0 +1,24 @@ |
|||
.widget-container { |
|||
padding: 15px; |
|||
background: white; |
|||
border-radius: 8px; |
|||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); |
|||
} |
|||
|
|||
h3 { |
|||
font-size: 18px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.export-button { |
|||
margin-top: 10px; |
|||
background-color: #0078d4; |
|||
color: white; |
|||
} |
|||
|
|||
.table-container { |
|||
max-height: 400px; |
|||
overflow-y: auto; |
|||
border: 1px solid #ddd; |
|||
} |
|||
|
|||
@ -0,0 +1,14 @@ |
|||
import { React } from 'jimu-core' |
|||
import _Widget from '../src/runtime/widget' |
|||
import { widgetRender, wrapWidget } from 'jimu-for-test' |
|||
|
|||
const render = widgetRender() |
|||
describe('test simple widget', () => { |
|||
it('simple test', () => { |
|||
const Widget = wrapWidget(_Widget, { |
|||
config: { exampleConfigProperty: 'a' } |
|||
}) |
|||
const { queryByText } = render(<Widget widgetId="Widget_1" />) |
|||
expect(queryByText('exampleConfigProperty: a').tagName).toBe('P') |
|||
}) |
|||
}) |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"exampleConfigProperty": "test" |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> |
|||
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" |
|||
width="800px" height="800px" viewBox="0 0 52 52" enable-background="new 0 0 52 52" xml:space="preserve"> |
|||
<path d="M44,4H8C5.8,4,4,5.8,4,8v36c0,2.2,1.8,4,4,4h36c2.2,0,4-1.8,4-4V8C48,5.8,46.2,4,44,4z M12,14 |
|||
c0-0.6,0.4-1,1-1h10c0.6,0,1,0.4,1,1v10c0,0.6-0.4,1-1,1H13c-0.6,0-1-0.4-1-1V14z M36,40c0,0.6-0.4,1-1,1H13c-0.6,0-1-0.4-1-1v-2 |
|||
c0-0.6,0.4-1,1-1h22c0.6,0,1,0.4,1,1V40z M40,32c0,0.6-0.4,1-1,1H13c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1h26c0.6,0,1,0.4,1,1V32z |
|||
M40,24c0,0.6-0.4,1-1,1H29c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1h10c0.6,0,1,0.4,1,1V24z M40,16c0,0.6-0.4,1-1,1H29 |
|||
c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1h10c0.6,0,1,0.4,1,1V16z"/> |
|||
</svg> |
|||
@ -0,0 +1,29 @@ |
|||
{ |
|||
"name": "Asset_Road_Summary_view", |
|||
"label": "Asset_Road_Summary_view", |
|||
"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": { |
|||
"hasSettingPage": false |
|||
}, |
|||
"widgets": [ |
|||
{ |
|||
"uri": "Asset_Road_Summary_view", |
|||
"label": "Asset_Road_Summary_view", |
|||
"thumbnail": "thumbnail.png", |
|||
"description": "Summarizes Asset and Road." |
|||
} |
|||
], |
|||
"framework": "react", |
|||
"translatedLocales": ["en"], |
|||
"dataSources": [], |
|||
"elements": [] |
|||
} |
|||
|
|||
|
|||
|
|||
@ -0,0 +1,7 @@ |
|||
import { type ImmutableObject } from 'seamless-immutable' |
|||
|
|||
export interface Config { |
|||
exampleConfigProperty: string |
|||
} |
|||
|
|||
export type IMConfig = ImmutableObject<Config> |
|||
@ -0,0 +1,242 @@ |
|||
import React, { useEffect, useState } from 'react'; |
|||
import FeatureLayer from '@arcgis/core/layers/FeatureLayer'; |
|||
import Query from "@arcgis/core/rest/support/Query"; |
|||
import { |
|||
Table, TableBody, TableCell, TableContainer, |
|||
TableHead, TableRow, Paper, Select, MenuItem, Button, Grid |
|||
} from '@mui/material'; |
|||
|
|||
// NOTE: install these two packages in your project for Excel/PDF export
|
|||
// npm i xlsx jspdf jspdf-autotable
|
|||
|
|||
|
|||
// Combined Zone-Ward Asset Summary component
|
|||
// Updated: removed hard-coded allowedCategories. The component now
|
|||
// uses the asset layer's category domain (codedValues) to resolve
|
|||
// category descriptions and includes whatever descriptions exist in the domain.
|
|||
|
|||
const CombinedZoneWardSummary = () => { |
|||
const [zone, setZone] = useState(''); |
|||
const [ward, setWard] = useState(''); |
|||
const [zoneList, setZoneList] = useState([]); |
|||
const [wardList, setWardList] = useState([]); |
|||
const [detailedData, setDetailedData] = useState([]); // { zone, ward, category, count }
|
|||
const [zoneSummary, setZoneSummary] = useState([]); // { zone, category, count }
|
|||
const [categoryDomain, setCategoryDomain] = useState({}); |
|||
|
|||
const assetLayerUrl = "https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/Final_Asset_Location/FeatureServer/0"; |
|||
|
|||
useEffect(() => { |
|||
fetchZoneList(); |
|||
fetchCategoryDomain(); |
|||
}, []); |
|||
|
|||
useEffect(() => { |
|||
if (zone) fetchWardList(zone); |
|||
}, [zone]); |
|||
|
|||
// Fetch distinct zones
|
|||
const fetchZoneList = async () => { |
|||
try { |
|||
const layer = new FeatureLayer({ url: assetLayerUrl }); |
|||
const query = layer.createQuery(); |
|||
query.outFields = ["zone_no"]; |
|||
query.returnDistinctValues = true; |
|||
query.returnGeometry = false; |
|||
const result = await layer.queryFeatures(query); |
|||
const zones = [...new Set(result.features.map(f => f.attributes.zone_no))].sort(); |
|||
setZoneList(zones); |
|||
} catch (err) { |
|||
console.error('fetchZoneList error', err); |
|||
} |
|||
}; |
|||
|
|||
const fetchWardList = async (zoneNo) => { |
|||
try { |
|||
const layer = new FeatureLayer({ url: assetLayerUrl }); |
|||
const query = layer.createQuery(); |
|||
query.where = `zone_no = '${zoneNo}'`; |
|||
query.outFields = ["ward_no"]; |
|||
query.returnDistinctValues = true; |
|||
query.returnGeometry = false; |
|||
const result = await layer.queryFeatures(query); |
|||
const wards = [...new Set(result.features.map(f => f.attributes.ward_no))].sort(); |
|||
setWardList(wards); |
|||
} catch (err) { |
|||
console.error('fetchWardList error', err); |
|||
} |
|||
}; |
|||
|
|||
// Load category domain and set mapping { code: description }
|
|||
// returns the mapping so callers can await it
|
|||
const fetchCategoryDomain = async () => { |
|||
try { |
|||
const layer = new FeatureLayer({ url: assetLayerUrl }); |
|||
await layer.load(); |
|||
const field = layer.fields.find(f => f.name === "category"); |
|||
if (field?.domain?.codedValues) { |
|||
const map = Object.fromEntries(field.domain.codedValues.map(cv => [cv.code, cv.name])); |
|||
setCategoryDomain(map); |
|||
return map; |
|||
} |
|||
setCategoryDomain({}); |
|||
return {}; |
|||
} catch (err) { |
|||
console.error('fetchCategoryDomain error', err); |
|||
setCategoryDomain({}); |
|||
return {}; |
|||
} |
|||
}; |
|||
|
|||
// Ensure category domain is loaded before summarizing so we use descriptions
|
|||
const handleFetch = async () => { |
|||
if (!zone) return; |
|||
await fetchCategoryDomain(); |
|||
await fetchAssetSummaries(zone, ward); |
|||
}; |
|||
|
|||
const handleClear = () => { |
|||
setZone(''); |
|||
setWard(''); |
|||
setWardList([]); |
|||
setDetailedData([]); |
|||
setZoneSummary([]); |
|||
}; |
|||
|
|||
// Build detailed and zone-aggregated summaries using domain descriptions
|
|||
const fetchAssetSummaries = async (zoneNo, wardNo) => { |
|||
try { |
|||
const layer = new FeatureLayer({ url: assetLayerUrl }); |
|||
const query = layer.createQuery(); |
|||
query.where = `zone_no='${zoneNo}'` + (wardNo ? ` AND ward_no='${wardNo}'` : ""); |
|||
query.outFields = ['zone_no', 'ward_no', 'category']; |
|||
query.returnGeometry = false; |
|||
|
|||
const result = await layer.queryFeatures(query); |
|||
|
|||
// counts keyed by "zone|ward|category" and "zone|category"
|
|||
const dCounts = {}; |
|||
const zCounts = {}; |
|||
|
|||
result.features.forEach(({ attributes }) => { |
|||
const zoneVal = attributes.zone_no; |
|||
const wardVal = attributes.ward_no || ''; |
|||
const catCode = attributes.category; |
|||
|
|||
// Resolve to domain description when available; fall back to raw code/string
|
|||
const name = (categoryDomain && categoryDomain.hasOwnProperty(catCode)) ? categoryDomain[catCode] : String(catCode || '').trim(); |
|||
|
|||
// Use whatever description is available in the domain — no hard-coded filter
|
|||
const dKey = `${zoneVal}|${wardVal}|${name}`; |
|||
dCounts[dKey] = (dCounts[dKey] || 0) + 1; |
|||
|
|||
const zKey = `${zoneVal}|${name}`; |
|||
zCounts[zKey] = (zCounts[zKey] || 0) + 1; |
|||
}); |
|||
|
|||
const detailed = Object.entries(dCounts).map(([k, v]) => { |
|||
const [z, w, c] = k.split('|'); |
|||
return { zone: z, ward: w, category: c, count: v }; |
|||
}).sort((a, b) => (a.zone + a.ward + a.category).localeCompare(b.zone + b.ward + b.category)); |
|||
|
|||
const zoneAgg = Object.entries(zCounts).map(([k, v]) => { |
|||
const [z, c] = k.split('|'); |
|||
return { zone: z, category: c, count: v }; |
|||
}).sort((a, b) => (a.zone + a.category).localeCompare(b.zone + b.category)); |
|||
|
|||
setDetailedData(detailed); |
|||
setZoneSummary(zoneAgg); |
|||
} catch (err) { |
|||
console.error('fetchAssetSummaries error', err); |
|||
setDetailedData([]); |
|||
setZoneSummary([]); |
|||
} |
|||
}; |
|||
|
|||
|
|||
|
|||
return ( |
|||
<div style={{ padding: '1rem', height: '100%', overflow: 'hidden' }}> |
|||
<h2>Zone-wise Ward Asset Summary </h2> |
|||
<div style={{ marginBottom: '10px', fontWeight: 600 }}> |
|||
Total Assets (Detailed): {detailedData.reduce((s, r) => s + r.count, 0)} | |
|||
Total Assets (Zone): {zoneSummary.reduce((s, r) => s + r.count, 0)} |
|||
</div> |
|||
<Grid container spacing={2} alignItems="center"> |
|||
<Grid item> |
|||
<Select value={zone} onChange={e => setZone(e.target.value)} displayEmpty style={{ width: 150 }}> |
|||
<MenuItem value="">Select Zone</MenuItem> |
|||
{zoneList.map(z => <MenuItem key={z} value={z}>{z}</MenuItem>)} |
|||
</Select> |
|||
</Grid> |
|||
<Grid item> |
|||
<Select value={ward} onChange={e => setWard(e.target.value)} displayEmpty style={{ width: 150 }} disabled={!zone}> |
|||
<MenuItem value="">All Wards</MenuItem> |
|||
{wardList.map(w => <MenuItem key={w} value={w}>{w}</MenuItem>)} |
|||
</Select> |
|||
</Grid> |
|||
<Grid item> |
|||
<Button variant="contained" color="primary" onClick={handleFetch}>FETCH DATA</Button> |
|||
</Grid> |
|||
<Grid item> |
|||
<Button variant="outlined" color="secondary" onClick={handleClear}>CLEAR</Button> |
|||
</Grid> |
|||
|
|||
|
|||
{detailedData.length > 0 && ( |
|||
<TableContainer component={Paper} style={{ marginTop: '20px', maxHeight: 300, maxWidth: '100%', overflow: 'auto' }}> |
|||
<Table size="small" stickyHeader> |
|||
<TableHead> |
|||
<TableRow> |
|||
<TableCell><strong>Sl.No</strong></TableCell> |
|||
<TableCell><strong>Zone</strong></TableCell> |
|||
<TableCell><strong>Ward</strong></TableCell> |
|||
<TableCell><strong>Category</strong></TableCell> |
|||
<TableCell><strong>Count</strong></TableCell> |
|||
</TableRow> |
|||
</TableHead> |
|||
<TableBody> |
|||
{detailedData.map((r, i) => ( |
|||
<TableRow key={i}> |
|||
<TableCell>{i + 1}</TableCell> |
|||
<TableCell>{r.zone}</TableCell> |
|||
<TableCell>{r.ward}</TableCell> |
|||
<TableCell>{r.category}</TableCell> |
|||
<TableCell>{r.count}</TableCell> |
|||
</TableRow> |
|||
))} |
|||
</TableBody> |
|||
</Table> |
|||
</TableContainer> |
|||
)} |
|||
|
|||
{zoneSummary.length > 0 && ( |
|||
<TableContainer component={Paper} style={{ marginTop: '20px', maxHeight: 300, maxWidth: '100%', overflow: 'auto' }}> |
|||
<Table size="small" stickyHeader> |
|||
<TableHead> |
|||
<TableRow> |
|||
<TableCell><strong>Sl.No</strong></TableCell> |
|||
<TableCell><strong>Zone</strong></TableCell> |
|||
<TableCell><strong>Category</strong></TableCell> |
|||
<TableCell><strong>Count</strong></TableCell> |
|||
</TableRow> |
|||
</TableHead> |
|||
<TableBody> |
|||
{zoneSummary.map((r, i) => ( |
|||
<TableRow key={i}> |
|||
<TableCell>{i + 1}</TableCell> |
|||
<TableCell>{r.zone}</TableCell> |
|||
<TableCell>{r.category}</TableCell> |
|||
<TableCell>{r.count}</TableCell> |
|||
</TableRow> |
|||
))} |
|||
</TableBody> |
|||
</Table> |
|||
</TableContainer> |
|||
)} |
|||
|
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default CombinedZoneWardSummary; |
|||
@ -0,0 +1,8 @@ |
|||
import { React, AllWidgetProps } from 'jimu-core'; |
|||
import ZoneAbstractWidget from '../index'; |
|||
|
|||
const Widget = (props: AllWidgetProps<any>) => { |
|||
return <ZoneAbstractWidget {...props} />; |
|||
}; |
|||
|
|||
export default Widget; |
|||
@ -0,0 +1,24 @@ |
|||
.widget-container { |
|||
padding: 15px; |
|||
background: white; |
|||
border-radius: 8px; |
|||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); |
|||
} |
|||
|
|||
h3 { |
|||
font-size: 18px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.export-button { |
|||
margin-top: 10px; |
|||
background-color: #0078d4; |
|||
color: white; |
|||
} |
|||
|
|||
.table-container { |
|||
max-height: 400px; |
|||
overflow-y: auto; |
|||
border: 1px solid #ddd; |
|||
} |
|||
|
|||
@ -0,0 +1,14 @@ |
|||
import { React } from 'jimu-core' |
|||
import _Widget from '../src/runtime/widget' |
|||
import { widgetRender, wrapWidget } from 'jimu-for-test' |
|||
|
|||
const render = widgetRender() |
|||
describe('test simple widget', () => { |
|||
it('simple test', () => { |
|||
const Widget = wrapWidget(_Widget, { |
|||
config: { exampleConfigProperty: 'a' } |
|||
}) |
|||
const { queryByText } = render(<Widget widgetId="Widget_1" />) |
|||
expect(queryByText('exampleConfigProperty: a').tagName).toBe('P') |
|||
}) |
|||
}) |
|||
@ -0,0 +1,5 @@ |
|||
{ |
|||
"exampleConfigProperty": "test", |
|||
"useMapWidget": true |
|||
|
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
<?xml version="1.0" encoding="iso-8859-1"?> |
|||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> |
|||
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" |
|||
viewBox="0 0 488.409 488.409" xml:space="preserve"> |
|||
<circle style="fill:#91C7C9;" cx="160.296" cy="328.409" r="136.8"/> |
|||
<circle style="fill:#B3DEE0;" cx="177.896" cy="346.009" r="119.2"/> |
|||
<path style="fill:#13BCCE;" d="M478.696,10.009c16,16.8,16,43.2-35.2,94.4s-168.8,142.4-184.8,125.6s74.4-133.6,125.6-185.6 |
|||
C435.496-6.791,462.696-6.791,478.696,10.009z"/> |
|||
<path style="fill:#0E94AA;" d="M478.696,10.009c16,16.8,16,43.2-35.2,94.4s-168.8,142.4-184.8,125.6s80.8-124.8,132.8-176.8 |
|||
C442.696,2.009,462.696-6.791,478.696,10.009z"/> |
|||
<path style="fill:#096E89;" d="M478.696,10.009c16,16.8,16,43.2-35.2,94.4s-168.8,142.4-184.8,125.6"/> |
|||
<path style="fill:#F98AC1;" d="M164.296,488.409c-91.2,0-164-72.8-164-164c0-90.4,73.6-164,164-164s164,73.6,164,164 |
|||
C328.296,415.609,254.696,488.409,164.296,488.409z M164.296,200.409c-68,0-124,56-124,124s56,124,124,124s124-56,124-124 |
|||
S232.296,200.409,164.296,200.409z"/> |
|||
<path style="fill:#EF3388;" d="M164.296,488.409c-90.4,0-156-69.6-156-160s72.8-163.2,163.2-163.2s156.8,68.8,156.8,159.2 |
|||
C328.296,415.609,254.696,488.409,164.296,488.409z M164.296,200.409c-68,0-124,56-124,124s56,124,124,124s124-56,124-124 |
|||
S232.296,200.409,164.296,200.409z"/> |
|||
<path style="fill:#BC086B;" d="M306.696,240.409c-3.2,21.6-11.2,43.2-20.8,62.4c1.6,7.2,1.6,14.4,1.6,21.6c0,68-56,124-124,124 |
|||
c-20.8,0-40-4.8-57.6-14.4c-11.2,1.6-22.4,2.4-33.6,2.4c-10.4,0-20-0.8-30.4-1.6c30.4,32.8,72.8,53.6,120.8,53.6 |
|||
c90.4,0,165.6-73.6,165.6-164C329.896,294.009,321.096,265.209,306.696,240.409z"/> |
|||
<path style="fill:#EF3388;" d="M229.096,308.409h-48.8v-48.8c0-8-8-13.6-16-13.6s-16,6.4-16,13.6v48.8h-49.6c-8,0-14.4,8-14.4,16 |
|||
s6.4,16,14.4,16h49.6v49.6c0,8,8,14.4,16,14.4s16-6.4,16-14.4v-49.6h48.8c8,0,14.4-8,14.4-16S237.096,308.409,229.096,308.409z"/> |
|||
<path style="fill:#BC086B;" d="M229.096,308.409h-48.8l0,0l-32,32l0,0v49.6c0,8,8,14.4,16,14.4s16-6.4,16-14.4v-49.6h48.8 |
|||
c8,0,14.4-8,14.4-16S237.096,308.409,229.096,308.409z"/> |
|||
</svg> |
|||
2
Asset_SWD_Filter/dist/runtime/widget.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,311 @@ |
|||
/** |
|||
* @license |
|||
Copyright (c) 2008, Adobe Systems Incorporated |
|||
All rights reserved. |
|||
|
|||
Redistribution and use in source and binary forms, with or without |
|||
modification, are permitted provided that the following conditions are |
|||
met: |
|||
|
|||
* Redistributions of source code must retain the above copyright notice, |
|||
this list of conditions and the following disclaimer. |
|||
|
|||
* Redistributions in binary form must reproduce the above copyright |
|||
notice, this list of conditions and the following disclaimer in the |
|||
documentation and/or other materials provided with the distribution. |
|||
|
|||
* Neither the name of Adobe Systems Incorporated nor the names of its |
|||
contributors may be used to endorse or promote products derived from |
|||
this software without specific prior written permission. |
|||
|
|||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS |
|||
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
|||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
|||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
|||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
|||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
|||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
|||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
|||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* |
|||
* Copyright (c) 2014 James Robb, https://github.com/jamesbrobb |
|||
* |
|||
* Permission is hereby granted, free of charge, to any person obtaining |
|||
* a copy of this software and associated documentation files (the |
|||
* "Software"), to deal in the Software without restriction, including |
|||
* without limitation the rights to use, copy, modify, merge, publish, |
|||
* distribute, sublicense, and/or sell copies of the Software, and to |
|||
* permit persons to whom the Software is furnished to do so, subject to |
|||
* the following conditions: |
|||
* |
|||
* The above copyright notice and this permission notice shall be |
|||
* included in all copies or substantial portions of the Software. |
|||
* |
|||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
* ==================================================================== |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* (c) Dean McNamee <dean@gmail.com>, 2013. |
|||
* |
|||
* https://github.com/deanm/omggif |
|||
* |
|||
* Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
* of this software and associated documentation files (the "Software"), to |
|||
* deal in the Software without restriction, including without limitation the |
|||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or |
|||
* sell copies of the Software, and to permit persons to whom the Software is |
|||
* furnished to do so, subject to the following conditions: |
|||
* |
|||
* The above copyright notice and this permission notice shall be included in |
|||
* all copies or substantial portions of the Software. |
|||
* |
|||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
|||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
|||
* IN THE SOFTWARE. |
|||
* |
|||
* omggif is a JavaScript implementation of a GIF 89a encoder and decoder, |
|||
* including animation and compression. It does not rely on any specific |
|||
* underlying system, so should run in the browser, Node, or Plask. |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* Copyright (c) 2014 Steven Spungin (TwelveTone LLC) steven@twelvetone.tv |
|||
* |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* Copyright (c) 2017 Aras Abbasi |
|||
* |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* Copyright (c) 2018 Aras Abbasi |
|||
* |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* Copyright (c) 2019 Aras Abbasi |
|||
* |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* FPDF is released under a permissive license: there is no usage restriction. |
|||
* You may embed it freely in your application (commercial or not), with or |
|||
* without modifications. |
|||
* |
|||
* Reference: http://www.fpdf.org/en/script/script37.php |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* Joseph Myers does not specify a particular license for his work. |
|||
* |
|||
* Author: Joseph Myers |
|||
* Accessed from: http://www.myersdaily.org/joseph/javascript/md5.js |
|||
* |
|||
* Modified by: Owen Leong |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
* Author: Owen Leong (@owenl131) |
|||
* Date: 15 Oct 2020 |
|||
* References: |
|||
* https://www.cs.cmu.edu/~dst/Adobe/Gallery/anon21jul01-pdf-encryption.txt |
|||
* https://github.com/foliojs/pdfkit/blob/master/lib/security.js |
|||
* http://www.fpdf.org/en/script/script37.php |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* Unicode Bidi Engine based on the work of Alex Shensis (@asthensis) |
|||
* MIT License |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* jsPDF fileloading PlugIn |
|||
* Copyright (c) 2018 Aras Abbasi (aras.abbasi@gmail.com) |
|||
* |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* jsPDF filters PlugIn |
|||
* Copyright (c) 2014 Aras Abbasi |
|||
* |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* @license |
|||
* jsPDF virtual FileSystem functionality |
|||
* |
|||
* Licensed under the MIT License. |
|||
* http://opensource.org/licenses/mit-license |
|||
*/ |
|||
|
|||
/** |
|||
* A class to parse color values |
|||
* @author Stoyan Stefanov <sstoo@gmail.com> |
|||
* {@link http://www.phpied.com/rgb-color-parser-in-javascript/} |
|||
* @license Use it if you like it |
|||
*/ |
|||
|
|||
/** ==================================================================== |
|||
* @license |
|||
* jsPDF XMP metadata plugin |
|||
* Copyright (c) 2016 Jussi Utunen, u-jussi@suomi24.fi |
|||
* |
|||
* Permission is hereby granted, free of charge, to any person obtaining |
|||
* a copy of this software and associated documentation files (the |
|||
* "Software"), to deal in the Software without restriction, including |
|||
* without limitation the rights to use, copy, modify, merge, publish, |
|||
* distribute, sublicense, and/or sell copies of the Software, and to |
|||
* permit persons to whom the Software is furnished to do so, subject to |
|||
* the following conditions: |
|||
* |
|||
* The above copyright notice and this permission notice shall be |
|||
* included in all copies or substantial portions of the Software. |
|||
* |
|||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
* ==================================================================== |
|||
*/ |
|||
|
|||
/** @license |
|||
* Copyright (c) 2017 Dominik Homberger |
|||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
|||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. |
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
https://webpjs.appspot.com |
|||
WebPRiffParser dominikhlbg@gmail.com |
|||
*/ |
|||
|
|||
/** @license |
|||
* |
|||
* jsPDF - PDF Document creation from JavaScript |
|||
* Version 3.0.1 Built on 2025-03-17T14:19:36.873Z |
|||
* CommitID 00000000 |
|||
* |
|||
* Copyright (c) 2010-2021 James Hall <james@parall.ax>, https://github.com/MrRio/jsPDF |
|||
* 2015-2021 yWorks GmbH, http://www.yworks.com |
|||
* 2015-2021 Lukas Holländer <lukas.hollaender@yworks.com>, https://github.com/HackbrettXXX |
|||
* 2016-2018 Aras Abbasi <aras.abbasi@gmail.com> |
|||
* 2010 Aaron Spike, https://github.com/acspike |
|||
* 2012 Willow Systems Corporation, https://github.com/willowsystems |
|||
* 2012 Pablo Hess, https://github.com/pablohess |
|||
* 2012 Florian Jenett, https://github.com/fjenett |
|||
* 2013 Warren Weckesser, https://github.com/warrenweckesser |
|||
* 2013 Youssef Beddad, https://github.com/lifof |
|||
* 2013 Lee Driscoll, https://github.com/lsdriscoll |
|||
* 2013 Stefan Slonevskiy, https://github.com/stefslon |
|||
* 2013 Jeremy Morel, https://github.com/jmorel |
|||
* 2013 Christoph Hartmann, https://github.com/chris-rock |
|||
* 2014 Juan Pablo Gaviria, https://github.com/juanpgaviria |
|||
* 2014 James Makes, https://github.com/dollaruw |
|||
* 2014 Diego Casorran, https://github.com/diegocr |
|||
* 2014 Steven Spungin, https://github.com/Flamenco |
|||
* 2014 Kenneth Glassey, https://github.com/Gavvers |
|||
* |
|||
* Permission is hereby granted, free of charge, to any person obtaining |
|||
* a copy of this software and associated documentation files (the |
|||
* "Software"), to deal in the Software without restriction, including |
|||
* without limitation the rights to use, copy, modify, merge, publish, |
|||
* distribute, sublicense, and/or sell copies of the Software, and to |
|||
* permit persons to whom the Software is furnished to do so, subject to |
|||
* the following conditions: |
|||
* |
|||
* The above copyright notice and this permission notice shall be |
|||
* included in all copies or substantial portions of the Software. |
|||
* |
|||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
* |
|||
* Contributor(s): |
|||
* siefkenj, ahwolf, rickygu, Midnith, saintclair, eaparango, |
|||
* kim3er, mfo, alnorth, Flamenco |
|||
*/ |
|||
|
|||
/** @license |
|||
* Copyright (c) 2012 Willow Systems Corporation, https://github.com/willowsystems |
|||
* |
|||
* Permission is hereby granted, free of charge, to any person obtaining |
|||
* a copy of this software and associated documentation files (the |
|||
* "Software"), to deal in the Software without restriction, including |
|||
* without limitation the rights to use, copy, modify, merge, publish, |
|||
* distribute, sublicense, and/or sell copies of the Software, and to |
|||
* permit persons to whom the Software is furnished to do so, subject to |
|||
* the following conditions: |
|||
* |
|||
* The above copyright notice and this permission notice shall be |
|||
* included in all copies or substantial portions of the Software. |
|||
* |
|||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
* ==================================================================== |
|||
*/ |
|||
@ -0,0 +1 @@ |
|||
System.register(["jimu-core","jimu-ui/advanced/setting-components"],(function(e,t){var r={},o={};return{setters:[function(e){r.React=e.React},function(e){o.MapWidgetSelector=e.MapWidgetSelector}],execute:function(){e((()=>{var e={79244:e=>{"use strict";e.exports=r},79298:e=>{"use strict";e.exports=o}},t={};function n(r){var o=t[r];if(void 0!==o)return o.exports;var i=t[r]={exports:{}};return e[r](i,i.exports,n),i.exports}n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.p="";var i={};return n.p=window.jimuConfig.baseUrl,(()=>{"use strict";n.r(i),n.d(i,{__set_webpack_public_path__:()=>o,default:()=>r});var e=n(79244),t=n(79298);const r=r=>e.React.createElement("div",{className:"widget-setting-demo"},e.React.createElement(t.MapWidgetSelector,{useMapWidgetIds:r.useMapWidgetIds,onSelect:e=>{console.log("Selected Map Widget IDs:",e),r.onSettingChange({id:r.id,useMapWidgetIds:e})}}));function o(e){n.p=e}})(),i})())}}})); |
|||
@ -0,0 +1,3 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> |
|||
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M860.48 259.52a22.72 22.72 0 0 0 22.656-22.656V152c0-17.6-14.4-32-32-32h-640c-17.6 0-32 14.4-32 32v768c0 17.6 14.4 32 32 32h640c17.6 0 32-14.4 32-32v-250.432h-0.064l0.064-0.448a22.656 22.656 0 0 0-45.312 0l0.064 0.448h-0.064V501.12a22.656 22.656 0 0 0 45.312 0V323.52h-0.064a22.592 22.592 0 0 0-45.248 0h-0.064V236.864a22.784 22.784 0 0 0 22.72 22.656z" fill="" /><path d="M837.824 903.616a6.4 6.4 0 0 1-6.4 6.4H230.848a6.4 6.4 0 0 1-6.4-6.4V170.432a6.4 6.4 0 0 1 6.4-6.4h600.576a6.4 6.4 0 0 1 6.4 6.4v733.184z" fill="#FFFF00" /><path d="M285.248 520.576a16 16 0 0 1-32 0V249.152a16 16 0 0 1 32 0v271.424zM285.248 655.552a16 16 0 0 1-32 0v-62.976a16 16 0 0 1 32 0v62.976z" fill="#FFFFFF" /><path d="M328.704 357.44c0 8.832 7.168 16 16 16h283.968a16 16 0 0 0 0-32H344.704a16.064 16.064 0 0 0-16 16zM690.688 341.44a16 16 0 0 0 0 32h36.032a16 16 0 0 0 0-32h-36.032z" fill="#BABA00" /><path d="M328.704 485.12c0 8.832 7.168 16 16 16h381.952a16 16 0 0 0 0-32H344.704a16 16 0 0 0-16 16zM328.704 612.736c0 8.768 7.168 16 16 16h381.952a16 16 0 1 0 0-32H344.704a16 16 0 0 0-16 16z" fill="#BABA00" /><path d="M392.448 141.568c0 12.288-10.112 22.4-22.4 22.4h-0.512a22.528 22.528 0 0 1-22.4-22.4V74.752c0-12.288 10.112-22.4 22.4-22.4h0.512c12.288 0 22.4 10.112 22.4 22.4v66.816zM715.136 141.568c0 12.288-10.112 22.4-22.4 22.4h-0.512a22.528 22.528 0 0 1-22.464-22.4V74.752c0-12.288 10.048-22.4 22.464-22.4h0.512c12.224 0 22.4 10.112 22.4 22.4v66.816z" fill="" /></svg> |
|||
@ -0,0 +1,19 @@ |
|||
{ |
|||
"name": "Asset_SWD_Filter", |
|||
"label": "Asset_SWD_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 |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
import { type ImmutableObject } from 'seamless-immutable' |
|||
|
|||
export interface Config { |
|||
exampleConfigProperty: string |
|||
} |
|||
|
|||
export type IMConfig = ImmutableObject<Config> |
|||
@ -0,0 +1,25 @@ |
|||
<?xml version="1.0" encoding="iso-8859-1"?> |
|||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> |
|||
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" |
|||
viewBox="0 0 488.409 488.409" xml:space="preserve"> |
|||
<circle style="fill:#91C7C9;" cx="160.296" cy="328.409" r="136.8"/> |
|||
<circle style="fill:#B3DEE0;" cx="177.896" cy="346.009" r="119.2"/> |
|||
<path style="fill:#13BCCE;" d="M478.696,10.009c16,16.8,16,43.2-35.2,94.4s-168.8,142.4-184.8,125.6s74.4-133.6,125.6-185.6 |
|||
C435.496-6.791,462.696-6.791,478.696,10.009z"/> |
|||
<path style="fill:#0E94AA;" d="M478.696,10.009c16,16.8,16,43.2-35.2,94.4s-168.8,142.4-184.8,125.6s80.8-124.8,132.8-176.8 |
|||
C442.696,2.009,462.696-6.791,478.696,10.009z"/> |
|||
<path style="fill:#096E89;" d="M478.696,10.009c16,16.8,16,43.2-35.2,94.4s-168.8,142.4-184.8,125.6"/> |
|||
<path style="fill:#F98AC1;" d="M164.296,488.409c-91.2,0-164-72.8-164-164c0-90.4,73.6-164,164-164s164,73.6,164,164 |
|||
C328.296,415.609,254.696,488.409,164.296,488.409z M164.296,200.409c-68,0-124,56-124,124s56,124,124,124s124-56,124-124 |
|||
S232.296,200.409,164.296,200.409z"/> |
|||
<path style="fill:#EF3388;" d="M164.296,488.409c-90.4,0-156-69.6-156-160s72.8-163.2,163.2-163.2s156.8,68.8,156.8,159.2 |
|||
C328.296,415.609,254.696,488.409,164.296,488.409z M164.296,200.409c-68,0-124,56-124,124s56,124,124,124s124-56,124-124 |
|||
S232.296,200.409,164.296,200.409z"/> |
|||
<path style="fill:#BC086B;" d="M306.696,240.409c-3.2,21.6-11.2,43.2-20.8,62.4c1.6,7.2,1.6,14.4,1.6,21.6c0,68-56,124-124,124 |
|||
c-20.8,0-40-4.8-57.6-14.4c-11.2,1.6-22.4,2.4-33.6,2.4c-10.4,0-20-0.8-30.4-1.6c30.4,32.8,72.8,53.6,120.8,53.6 |
|||
c90.4,0,165.6-73.6,165.6-164C329.896,294.009,321.096,265.209,306.696,240.409z"/> |
|||
<path style="fill:#EF3388;" d="M229.096,308.409h-48.8v-48.8c0-8-8-13.6-16-13.6s-16,6.4-16,13.6v48.8h-49.6c-8,0-14.4,8-14.4,16 |
|||
s6.4,16,14.4,16h49.6v49.6c0,8,8,14.4,16,14.4s16-6.4,16-14.4v-49.6h48.8c8,0,14.4-8,14.4-16S237.096,308.409,229.096,308.409z"/> |
|||
<path style="fill:#BC086B;" d="M229.096,308.409h-48.8l0,0l-32,32l0,0v49.6c0,8,8,14.4,16,14.4s16-6.4,16-14.4v-49.6h48.8 |
|||
c8,0,14.4-8,14.4-16S237.096,308.409,229.096,308.409z"/> |
|||
</svg> |
|||
@ -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; |
|||
} |
|||
|
|||
@ -0,0 +1,23 @@ |
|||
import React from 'react'; |
|||
import './LoadingPopup.css'; |
|||
|
|||
interface LoadingPopupProps { |
|||
show: boolean; |
|||
message?: string; |
|||
} |
|||
|
|||
const LoadingPopup: React.FC<LoadingPopupProps> = ({ show, message = "Preparing to download ..." }) => { |
|||
if (!show) return null; |
|||
|
|||
return ( |
|||
<div className="loading-overlay"> |
|||
<div className="loading-box"> |
|||
<h2 className="loading-title">Please Wait..</h2> |
|||
<div className="spinner" /> |
|||
<p className="loading-message">{message}</p> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default LoadingPopup; |
|||
@ -0,0 +1,23 @@ |
|||
import React from 'react'; |
|||
import './LoadingPopup.css'; |
|||
|
|||
interface LoadingPopupProps { |
|||
show: boolean; |
|||
message?: string; |
|||
} |
|||
|
|||
const LoadingPopup: React.FC<LoadingPopupProps> = ({ show, message = "Preparing to download ..." }) => { |
|||
if (!show) return null; |
|||
|
|||
return ( |
|||
<div className="loading-overlay"> |
|||
<div className="loading-box"> |
|||
<h2 className="loading-title">Please Wait..</h2> |
|||
<div className="spinner" /> |
|||
<p className="loading-message">{message}</p> |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default LoadingPopup; |
|||
@ -0,0 +1,123 @@ |
|||
.property-widget-parent { |
|||
padding: 0.7rem; |
|||
font-family: "Arial", sans-serif; |
|||
height: 100%; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.property-widget { |
|||
background: #fff; |
|||
border-radius: 6px; |
|||
padding: 0.7rem; |
|||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.08); |
|||
height: 100%; |
|||
max-height: 100%; |
|||
overflow-y: auto; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.widget-title { |
|||
font-size: 1rem; |
|||
font-weight: bold; |
|||
margin-bottom: 0.7rem; |
|||
} |
|||
|
|||
#countDiv { |
|||
background: #f7f7f7; |
|||
border-radius: 4px; |
|||
padding: 4px 8px; /* less padding */ |
|||
margin-bottom: 0.3rem; |
|||
font-size: 0.75rem; /* smaller text */ |
|||
color: #333; |
|||
line-height: 1.2; |
|||
display: inline-block; /* shrink width to content */ |
|||
} |
|||
|
|||
|
|||
|
|||
.property_form { |
|||
display: flex; |
|||
flex-direction: column; |
|||
margin-bottom: 0.7rem; |
|||
} |
|||
|
|||
.property_form label { |
|||
font-weight: 600; |
|||
margin-bottom: 2px; |
|||
font-size: 0.7rem; |
|||
} |
|||
|
|||
.property_form select { |
|||
padding: 4px 6px; |
|||
border: 1px solid #ccc; |
|||
border-radius: 4px; |
|||
font-size: 0.7rem; |
|||
} |
|||
|
|||
button { |
|||
width: 100%; |
|||
padding: 6px 8px; |
|||
margin-top: 0.3rem; |
|||
border-radius: 4px; |
|||
font-weight: bold; |
|||
font-size: 0.7rem; |
|||
border: none; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
button.property_form { |
|||
background-color: #0078d4; |
|||
color: white; |
|||
} |
|||
|
|||
button[property_form]:hover { |
|||
background-color: #005ea0; |
|||
} |
|||
|
|||
button[variant="outlined"] { |
|||
background-color: white; |
|||
color: #0078d4; |
|||
border: 1px solid #0078d4; |
|||
} |
|||
|
|||
button[variant="outlined"]:hover { |
|||
background-color: #f0f0f0; |
|||
} |
|||
|
|||
.attribute-table { |
|||
margin-top: 0.7rem; |
|||
overflow-x: auto; |
|||
} |
|||
|
|||
.attribute-table table { |
|||
table-layout: fixed; |
|||
width: 100%; |
|||
border-collapse: collapse; |
|||
} |
|||
|
|||
.attribute-table th, |
|||
.attribute-table td { |
|||
max-width: 150px; |
|||
padding: 6px; |
|||
border: 1px solid #ddd; |
|||
word-wrap: break-word; |
|||
overflow-wrap: break-word; |
|||
white-space: normal; |
|||
font-size: 0.7rem; |
|||
} |
|||
|
|||
.attribute-table th { |
|||
background-color: #f2f2f2; |
|||
font-weight: bold; |
|||
position: sticky; |
|||
top: 0; |
|||
z-index: 10; |
|||
text-align: left; |
|||
border-bottom: 1px solid #ccc; |
|||
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.attribute-table tr:hover { |
|||
background-color: #f9f9f9; |
|||
cursor: pointer; |
|||
} |
|||
1274
Asset_SWD_Filter/src/runtime/widget.tsx
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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<any>) => { |
|||
const onMapWidgetSelected = (useMapWidgetIds: string[]) => { |
|||
console.log("Selected Map Widget IDs:", useMapWidgetIds); |
|||
props.onSettingChange({ |
|||
id: props.id, |
|||
useMapWidgetIds: useMapWidgetIds |
|||
}) |
|||
} |
|||
|
|||
return <div className="widget-setting-demo"> |
|||
<MapWidgetSelector useMapWidgetIds={props.useMapWidgetIds} onSelect={onMapWidgetSelected} /> |
|||
</div> |
|||
} |
|||
|
|||
export default Setting |
|||
|
|||
|
After Width: 512 | Height: 512 | Size: 23 KiB |
@ -0,0 +1,14 @@ |
|||
import { React } from 'jimu-core' |
|||
import _Widget from '../src/runtime/widget' |
|||
import { widgetRender, wrapWidget } from 'jimu-for-test' |
|||
|
|||
const render = widgetRender() |
|||
describe('test simple widget', () => { |
|||
it('simple test', () => { |
|||
const Widget = wrapWidget(_Widget, { |
|||
config: { exampleConfigProperty: 'a' } |
|||
}) |
|||
const { queryByText } = render(<Widget widgetId="Widget_1" />) |
|||
expect(queryByText('exampleConfigProperty: a').tagName).toBe('P') |
|||
}) |
|||
}) |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"exampleConfigProperty": "test" |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.1323 10.469L15.1344 8.67087L15.4652 8.40057C15.677 8.22748 15.8568 8.01844 15.9961 7.78298L16.0015 7.77362C16.5302 6.85747 16.3594 5.73361 15.6221 4.9964C14.8849 4.25926 13.761 4.08832 12.8448 4.61713L12.8356 4.62263C12.6001 4.76193 12.3911 4.94163 12.218 5.15344L11.9477 5.48422L10.1496 3.48633L7.33395 6.30197L8.72375 7.43773L8.72456 7.4384C8.83298 7.52762 8.92489 7.63516 8.99614 7.75614C9.26009 8.21606 9.19062 8.77633 8.79417 9.17277C8.3977 9.56925 7.83739 9.63872 7.37747 9.37476C7.2562 9.30333 7.14847 9.2111 7.05912 9.10233L7.05369 9.09587L5.92343 7.7125L2.98242 10.6532L8.32993 16.0008C8.54466 16.2155 8.79959 16.3859 9.08016 16.5021C9.36073 16.6183 9.66144 16.6781 9.96513 16.6781C10.2688 16.6781 10.5695 16.6183 10.8501 16.5021C11.1307 16.3859 11.3856 16.2155 11.6003 16.0008L17.1323 10.469ZM10.4674 15.5782C10.6266 15.5122 10.7713 15.4156 10.8932 15.2937L15.6799 10.5072L13.9779 8.97543L13.9938 8.31147L14.8324 7.62621C14.9533 7.52744 15.0559 7.40814 15.1354 7.27378C15.4309 6.76174 15.3423 6.13078 14.915 5.70353C14.4878 5.27629 13.8567 5.18775 13.3448 5.48325C13.2105 5.56275 13.0912 5.66529 12.9924 5.78617L12.3071 6.6248L11.6431 6.64063L10.1114 4.93873L8.82285 6.22728L9.35855 6.66505C9.4391 6.73123 9.51469 6.80292 9.58482 6.87952C9.68951 6.99389 9.78202 7.11921 9.86066 7.25351C10.3569 8.1132 10.2125 9.16864 9.50128 9.87988C8.79004 10.5911 7.73457 10.7355 6.87486 10.2393C6.74055 10.1606 6.61522 10.0681 6.50085 9.96343C6.42425 9.89331 6.35257 9.81772 6.28639 9.73717L6.28366 9.73392L5.84862 9.20146L4.39667 10.6533L9.03704 15.2937C9.15891 15.4156 9.30361 15.5122 9.46285 15.5782C9.62209 15.6442 9.79276 15.6781 9.96513 15.6781C10.1375 15.6781 10.3082 15.6442 10.4674 15.5782Z" fill="black"/> |
|||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 2C0 0.895431 0.895431 0 2 0H18C19.1046 0 20 0.895431 20 2V18C20 19.1046 19.1046 20 18 20H2C0.895431 20 0 19.1046 0 18V2ZM2 1H18C18.5523 1 19 1.44772 19 2V18C19 18.5523 18.5523 19 18 19H2C1.44772 19 1 18.5523 1 18V2C1 1.44772 1.44772 1 2 1Z" fill="black"/> |
|||
</svg> |
|||
@ -0,0 +1,29 @@ |
|||
{ |
|||
"name": "Asset_Summary", |
|||
"label": "Asset_Summary", |
|||
"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": { |
|||
"hasSettingPage": false |
|||
}, |
|||
"widgets": [ |
|||
{ |
|||
"uri": "Asset_Summary", |
|||
"label": "Asset_Summary", |
|||
"thumbnail": "thumbnail.png", |
|||
"description": "Summarizes Street Light, Highmast, and Pillar Box counts by zone." |
|||
} |
|||
], |
|||
"framework": "react", |
|||
"translatedLocales": ["en"], |
|||
"dataSources": [], |
|||
"elements": [] |
|||
} |
|||
|
|||
|
|||
|
|||
@ -0,0 +1,7 @@ |
|||
import { type ImmutableObject } from 'seamless-immutable' |
|||
|
|||
export interface Config { |
|||
exampleConfigProperty: string |
|||
} |
|||
|
|||
export type IMConfig = ImmutableObject<Config> |
|||
@ -0,0 +1,41 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
|
|||
const featureLayerUrl = "https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/streetlight_Enumeration/FeatureServer/0/query"; |
|||
|
|||
const ZoneWiseStreetLightCount: React.FC = () => { |
|||
const [count, setCount] = useState<number>(0); |
|||
const [zone, setZone] = useState<string>("08"); // Example Zone 08, can be changed
|
|||
|
|||
const fetchCountData = async () => { |
|||
const query = `where=zone%3D%27${zone}%27&returnCountOnly=true&f=geojson`; |
|||
|
|||
try { |
|||
const response = await fetch(`${featureLayerUrl}?${query}`); |
|||
const data = await response.json(); |
|||
|
|||
if (data.error) { |
|||
console.error('API Error:', data.error.message); |
|||
return; |
|||
} |
|||
|
|||
if (data.count) { |
|||
setCount(data.count); // Extract the count from the response
|
|||
} |
|||
} catch (error) { |
|||
console.error("Request Error:", error); |
|||
} |
|||
}; |
|||
|
|||
useEffect(() => { |
|||
fetchCountData(); |
|||
}, [zone]); // Refetch data whenever the zone changes
|
|||
|
|||
return ( |
|||
<div> |
|||
<h3>Zone {zone}</h3> |
|||
<p>Total Count: {count}</p> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default ZoneWiseStreetLightCount; |
|||
@ -0,0 +1,151 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import FeatureLayer from "@arcgis/core/layers/FeatureLayer"; |
|||
import Query from "@arcgis/core/rest/support/Query"; |
|||
import "../styles.css"; |
|||
|
|||
// Feature layer URL
|
|||
const featureLayerUrl = |
|||
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/GCCAsset_Public/FeatureServer/0"; |
|||
|
|||
const ZoneCategoryWiseAssetCount: React.FC = () => { |
|||
const [counts, setCounts] = useState<{ zone_no: string; assetDetails: string }[]>([]); |
|||
const [zone_no, setZone_no] = useState<string>(""); |
|||
const [categoryDomain, setCategoryDomain] = useState<Record<number, string>>({}); |
|||
const [loading, setLoading] = useState<boolean>(false); |
|||
const [error, setError] = useState<string>(""); |
|||
|
|||
// Fetch domain values for 'category'
|
|||
const fetchDomainValues = async () => { |
|||
try { |
|||
const featureLayer = new FeatureLayer({ url: featureLayerUrl }); |
|||
await featureLayer.load(); |
|||
|
|||
const categoryField = featureLayer.fields.find((field) => field.name === "category"); |
|||
|
|||
if (categoryField && categoryField.domain && categoryField.domain.codedValues) { |
|||
const domainMapping: Record<number, string> = {}; |
|||
categoryField.domain.codedValues.forEach((codedValue: { code: number; name: string }) => { |
|||
domainMapping[codedValue.code] = codedValue.name; |
|||
}); |
|||
|
|||
setCategoryDomain(domainMapping); |
|||
fetchZoneCategorySummary(domainMapping); |
|||
} |
|||
} catch (error) { |
|||
console.error("Error fetching domain values:", error); |
|||
} |
|||
}; |
|||
|
|||
// Fetch and aggregate zone-category data
|
|||
const fetchZoneCategorySummary = async (domainMapping: Record<number, string>) => { |
|||
try { |
|||
setLoading(true); |
|||
setError(""); |
|||
|
|||
const featureLayer = new FeatureLayer({ url: featureLayerUrl }); |
|||
|
|||
let whereClause = "1=1"; |
|||
if (zone_no) whereClause = `zone_no='${zone_no}'`; |
|||
|
|||
const query = new Query(); |
|||
query.where = whereClause; |
|||
query.outFields = ["zone_no", "category"]; |
|||
query.returnGeometry = false; |
|||
|
|||
const result = await featureLayer.queryFeatures(query); |
|||
console.log("Total records returned:", result.features.length); |
|||
|
|||
const zoneData: Record<string, Record<string, number>> = {}; |
|||
|
|||
result.features.forEach((feature) => { |
|||
const zone_no = feature.attributes.zone_no; |
|||
const categoryCode = feature.attributes.category; |
|||
const categoryName = domainMapping[categoryCode] || `Unknown (${categoryCode})`; |
|||
|
|||
if (!zoneData[zone_no]) { |
|||
zoneData[zone_no] = {}; |
|||
} |
|||
|
|||
if (!zoneData[zone_no][categoryName]) { |
|||
zoneData[zone_no][categoryName] = 0; |
|||
} |
|||
|
|||
zoneData[zone_no][categoryName] += 1; |
|||
}); |
|||
|
|||
const countsArray = Object.keys(zoneData) |
|||
.sort((a, b) => parseInt(a) - parseInt(b)) |
|||
.map((zone_noKey) => { |
|||
const assetDetails = Object.entries(zoneData[zone_noKey]) |
|||
.map(([category, count]) => `${category} (${count})`) |
|||
.join(", "); |
|||
return { zone_no: zone_noKey, assetDetails }; |
|||
}); |
|||
|
|||
setCounts(countsArray); |
|||
} catch (error) { |
|||
console.error("Error fetching asset category count:", error); |
|||
setError("Failed to fetch data. Please try again later."); |
|||
setCounts([]); |
|||
} finally { |
|||
setLoading(false); |
|||
} |
|||
}; |
|||
|
|||
useEffect(() => { |
|||
fetchDomainValues(); |
|||
}, []); |
|||
|
|||
useEffect(() => { |
|||
if (Object.keys(categoryDomain).length > 0) { |
|||
fetchZoneCategorySummary(categoryDomain); |
|||
} |
|||
}, [zone_no]); |
|||
|
|||
return ( |
|||
<div className="zone-category-wise-asset-container"> |
|||
<h3>Zone and Category Wise - Asset Count</h3> |
|||
|
|||
<label> |
|||
Select Zone No: |
|||
<select |
|||
value={zone_no} |
|||
onChange={(e) => setZone_no(e.target.value)} |
|||
className="zone-no-select" |
|||
> |
|||
<option value="">-- Select Zone No --</option> |
|||
{[...Array(15)].map((_, i) => ( |
|||
<option key={i} value={String(i + 1).padStart(2, "0")}> |
|||
{String(i + 1).padStart(2, "0")} |
|||
</option> |
|||
))} |
|||
</select> |
|||
</label> |
|||
|
|||
{error && <p className="error-message">{error}</p>} |
|||
|
|||
{loading ? ( |
|||
<p>Loading...</p> |
|||
) : ( |
|||
<table className="zone-category-wise-table"> |
|||
<thead> |
|||
<tr> |
|||
<th>Zone No</th> |
|||
<th>Asset Details</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
{counts.map(({ zone_no, assetDetails }, index) => ( |
|||
<tr key={index}> |
|||
<td>{zone_no}</td> |
|||
<td>{assetDetails}</td> |
|||
</tr> |
|||
))} |
|||
</tbody> |
|||
</table> |
|||
)} |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default ZoneCategoryWiseAssetCount; |
|||
@ -0,0 +1,61 @@ |
|||
.widget-container { |
|||
padding: 15px; |
|||
background: white; |
|||
border-radius: 8px; |
|||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); |
|||
} |
|||
|
|||
h3 { |
|||
font-size: 18px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.export-button { |
|||
margin-top: 10px; |
|||
background-color: #0078d4; |
|||
color: white; |
|||
} |
|||
|
|||
.table-container { |
|||
max-height: 400px; |
|||
overflow-y: auto; |
|||
border: 1px solid #ddd; |
|||
} |
|||
/* Zone Wise Streetlight Count */ |
|||
.zone-wise-streetlight-container { |
|||
font-family: Arial, sans-serif; |
|||
margin: 20px; |
|||
} |
|||
|
|||
.zone-select { |
|||
margin-left: 10px; |
|||
padding: 5px; |
|||
font-size: 16px; |
|||
border: 1px solid #ccc; |
|||
border-radius: 4px; |
|||
} |
|||
|
|||
.zone-wise-table { |
|||
border-collapse: collapse; |
|||
width: 100%; |
|||
margin-top: 20px; |
|||
} |
|||
|
|||
.zone-wise-table th, |
|||
.zone-wise-table td { |
|||
border: 1px solid #ddd; |
|||
padding: 8px; |
|||
text-align: left; |
|||
} |
|||
|
|||
.zone-wise-table th { |
|||
background-color: #f2f2f2; |
|||
} |
|||
|
|||
.zone-wise-table tr:nth-child(even) { |
|||
background-color: #f9f9f9; |
|||
} |
|||
|
|||
.zone-wise-table tr:hover { |
|||
background-color: #f1f1f1; |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
import { React } from 'jimu-core' |
|||
import _Widget from '../src/runtime/widget' |
|||
import { widgetRender, wrapWidget } from 'jimu-for-test' |
|||
|
|||
const render = widgetRender() |
|||
describe('test simple widget', () => { |
|||
it('simple test', () => { |
|||
const Widget = wrapWidget(_Widget, { |
|||
config: { exampleConfigProperty: 'a' } |
|||
}) |
|||
const { queryByText } = render(<Widget widgetId="Widget_1" />) |
|||
expect(queryByText('exampleConfigProperty: a').tagName).toBe('P') |
|||
}) |
|||
}) |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"exampleConfigProperty": "test" |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
<?xml version="1.0" ?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> |
|||
<svg width="800px" height="800px" viewBox="0 0 32 32" data-name="Layer 1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:url(#linear-gradient-2);}.cls-3{fill:#f8edeb;}.cls-4{fill:#577590;}</style><linearGradient gradientUnits="userSpaceOnUse" id="linear-gradient" x1="4" x2="25" y1="16" y2="16"><stop offset="0" stop-color="#f9dcc4"/><stop offset="0.01" stop-color="#f8dbc2"/><stop offset="0.12" stop-color="#f3ceb1"/><stop offset="0.26" stop-color="#f0c5a6"/><stop offset="0.46" stop-color="#eebf9f"/><stop offset="1" stop-color="#edbe9d"/></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="linear-gradient-2" x1="7.48" x2="27.52" y1="8.98" y2="29.02"><stop offset="0" stop-color="#f9dcc4"/><stop offset="0.32" stop-color="#f8d9c0"/><stop offset="0.64" stop-color="#f4cfb3"/><stop offset="0.98" stop-color="#eebf9f"/><stop offset="1" stop-color="#edbe9d"/></linearGradient></defs><rect class="cls-1" height="22" rx="2.5" width="21" x="4" y="5"/><rect class="cls-2" height="22" rx="2.5" width="21" x="7" y="8"/><path class="cls-3" d="M20,2a5,5,0,0,0-5,5v5a2,2,0,0,0,3,1.73V15.5a3.5,3.5,0,0,0,7,0V7A5,5,0,0,0,20,2Z"/><path class="cls-4" d="M20,2a5,5,0,0,0-5,5v4.5a.5.5,0,0,0,1,0V7a4,4,0,0,1,8,0v8.5a2.5,2.5,0,0,1-5,0V7a1,1,0,0,1,2,0v8.5a.5.5,0,0,0,1,0V7a2,2,0,0,0-4,0v8.5a3.5,3.5,0,0,0,7,0V7A5,5,0,0,0,20,2Z"/></svg> |
|||
@ -0,0 +1,29 @@ |
|||
{ |
|||
"name": "Asset_Updation_Attachment_status", |
|||
"label": "Asset_Updation_Attachment_status", |
|||
"type": "widget", |
|||
"version": "1.16.0", |
|||
"exbVersion": "1.16.0", |
|||
"author": "Tanii Team 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": "Asset_Updation_Attachment_status", |
|||
"label": "Asset_Updation_Attachment_status", |
|||
"thumbnail": "thumbnail.png", |
|||
"description": "Attachment Available Status." |
|||
} |
|||
], |
|||
"framework": "react", |
|||
"translatedLocales": ["en"], |
|||
"dataSources": [], |
|||
"elements": [] |
|||
} |
|||
|
|||
|
|||
|
|||
@ -0,0 +1,7 @@ |
|||
import { type ImmutableObject } from 'seamless-immutable' |
|||
|
|||
export interface Config { |
|||
exampleConfigProperty: string |
|||
} |
|||
|
|||
export type IMConfig = ImmutableObject<Config> |
|||
@ -0,0 +1,898 @@ |
|||
import React, { useEffect, useMemo, useState } from "react"; |
|||
import FeatureLayer from "@arcgis/core/layers/FeatureLayer"; |
|||
import Graphic from "@arcgis/core/Graphic"; |
|||
import { JimuMapViewComponent, JimuMapView } from "jimu-arcgis"; |
|||
import { type AllWidgetProps } from "jimu-core"; |
|||
import * as XLSX from "xlsx"; |
|||
import jsPDF from "jspdf"; |
|||
import "jspdf-autotable"; |
|||
|
|||
interface SummaryInfo { |
|||
zone: string; |
|||
ward: string; |
|||
category: string; |
|||
totalCount: number; |
|||
withAttachment: number; |
|||
withoutAttachment: number; |
|||
} |
|||
|
|||
interface AssetFeatureInfo { |
|||
objectid: number; |
|||
zone: string; |
|||
ward: string; |
|||
road_name: string; |
|||
category: string; |
|||
asset_code: string; |
|||
asset_type: string; |
|||
name_of_the_asset: string; |
|||
address: string; |
|||
building_status: string; |
|||
status: string; |
|||
ownership: string; |
|||
remarks: string; |
|||
created_date: string; |
|||
geometry?: __esri.Geometry; |
|||
} |
|||
|
|||
const ASSET_URL = |
|||
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/Asset_2026_final/FeatureServer/1"; |
|||
|
|||
const AssetAttachmentStatus = (props: AllWidgetProps<any>) => { |
|||
const [zones, setZones] = useState<string[]>([]); |
|||
const [wards, setWards] = useState<string[]>([]); |
|||
const [categories, setCategories] = useState<string[]>([]); |
|||
|
|||
const [selectedZone, setSelectedZone] = useState(""); |
|||
const [selectedWard, setSelectedWard] = useState(""); |
|||
const [selectedCategory, setSelectedCategory] = useState(""); |
|||
|
|||
const [summary, setSummary] = useState<SummaryInfo | null>(null); |
|||
const [features, setFeatures] = useState<AssetFeatureInfo[]>([]); |
|||
const [loading, setLoading] = useState(false); |
|||
const [fetchingDetails, setFetchingDetails] = useState(false); |
|||
const [fetchProgress, setFetchProgress] = useState({ fetched: 0, total: 0, phase: "" }); |
|||
|
|||
const [jimuMapView, setJimuMapView] = useState<JimuMapView | null>(null); |
|||
const [highlightHandle, setHighlightHandle] = useState<any>(null); |
|||
|
|||
const assetLayer = useMemo(() => { |
|||
return new FeatureLayer({ |
|||
url: ASSET_URL |
|||
}); |
|||
}, []); |
|||
|
|||
const escapeSql = (s: string) => (s ? s.replace(/'/g, "''") : s); |
|||
const safeFileName = (s: string) => (s ? s.replace(/[^a-z0-9_\-]/gi, "_") : "All"); |
|||
const numericSortMaybe = (arr: string[]) => |
|||
arr |
|||
.filter(Boolean) |
|||
.sort((a, b) => { |
|||
const na = Number(a); |
|||
const nb = Number(b); |
|||
if (!isNaN(na) && !isNaN(nb)) return na - nb; |
|||
return String(a).localeCompare(String(b)); |
|||
}); |
|||
|
|||
const alphaSort = (arr: string[]) => |
|||
arr |
|||
.filter(Boolean) |
|||
.sort((a, b) => String(a).trim().localeCompare(String(b).trim(), undefined, { sensitivity: "base" })); |
|||
|
|||
const findAssetLayerInMap = (map: any): FeatureLayer | null => { |
|||
try { |
|||
const targetUrl = ASSET_URL; |
|||
|
|||
const searchLayers = (layers: any): FeatureLayer | null => { |
|||
if (!layers) return null; |
|||
const items = layers.items || []; |
|||
for (const layer of items) { |
|||
if (!layer) continue; |
|||
|
|||
if (layer.url && typeof layer.url === "string" && layer.url.indexOf(targetUrl) !== -1) { |
|||
return layer as FeatureLayer; |
|||
} |
|||
|
|||
if (layer.layers) { |
|||
const found = searchLayers(layer.layers); |
|||
if (found) return found; |
|||
} |
|||
} |
|||
return null; |
|||
}; |
|||
|
|||
return searchLayers(map.layers); |
|||
} catch (e) { |
|||
console.warn("findAssetLayerInMap failed:", e); |
|||
return null; |
|||
} |
|||
}; |
|||
|
|||
useEffect(() => { |
|||
const loadZones = async () => { |
|||
try { |
|||
const q = assetLayer.createQuery(); |
|||
q.outFields = ["zone"]; |
|||
q.where = "zone IS NOT NULL AND zone <> ''"; |
|||
q.returnGeometry = false; |
|||
|
|||
const result = await assetLayer.queryFeatures(q); |
|||
const distinctZones = [...new Set(result.features.map((f) => String(f.attributes.zone || "").trim()).filter(Boolean))]; |
|||
setZones(numericSortMaybe(distinctZones)); |
|||
} catch (err) { |
|||
console.error("loadZones error:", err); |
|||
} |
|||
}; |
|||
|
|||
loadZones(); |
|||
}, [assetLayer]); |
|||
|
|||
useEffect(() => { |
|||
if (!selectedZone) { |
|||
setWards([]); |
|||
return; |
|||
} |
|||
|
|||
const loadWards = async () => { |
|||
try { |
|||
let where = "ward IS NOT NULL AND ward <> ''"; |
|||
if (selectedZone !== "All") where += ` AND zone='${escapeSql(selectedZone)}'`; |
|||
|
|||
const q = assetLayer.createQuery(); |
|||
q.outFields = ["ward"]; |
|||
q.where = where; |
|||
q.returnGeometry = false; |
|||
|
|||
const result = await assetLayer.queryFeatures(q); |
|||
const distinct = [...new Set(result.features.map((f) => String(f.attributes.ward || "").trim()).filter(Boolean))]; |
|||
setWards(numericSortMaybe(distinct)); |
|||
} catch (err) { |
|||
console.error("loadWards error:", err); |
|||
} |
|||
}; |
|||
|
|||
loadWards(); |
|||
}, [selectedZone, assetLayer]); |
|||
|
|||
useEffect(() => { |
|||
if (!selectedZone || !selectedWard) { |
|||
setCategories([]); |
|||
return; |
|||
} |
|||
|
|||
const loadCategories = async () => { |
|||
try { |
|||
let where = "category IS NOT NULL AND category <> ''"; |
|||
if (selectedZone !== "All") where += ` AND zone='${escapeSql(selectedZone)}'`; |
|||
if (selectedWard !== "All") where += ` AND ward='${escapeSql(selectedWard)}'`; |
|||
|
|||
const q = assetLayer.createQuery(); |
|||
q.outFields = ["category"]; |
|||
q.where = where; |
|||
q.returnGeometry = false; |
|||
|
|||
const result = await assetLayer.queryFeatures(q); |
|||
const distinct = [...new Set(result.features.map((f) => String(f.attributes.category || "").trim()).filter(Boolean))]; |
|||
setCategories(alphaSort(distinct)); |
|||
} catch (err) { |
|||
console.error("loadCategories error:", err); |
|||
} |
|||
}; |
|||
|
|||
loadCategories(); |
|||
}, [selectedZone, selectedWard, assetLayer]); |
|||
|
|||
const activeViewChangeHandler = (jmv: JimuMapView) => { |
|||
if (jmv && jmv.view) setJimuMapView(jmv); |
|||
}; |
|||
|
|||
const zoomToFeature = async (feature: AssetFeatureInfo) => { |
|||
try { |
|||
if (!jimuMapView?.view) return; |
|||
|
|||
let geom = feature.geometry as __esri.Geometry | undefined; |
|||
|
|||
if (!geom) { |
|||
const q = assetLayer.createQuery(); |
|||
q.objectIds = [feature.objectid]; |
|||
q.outFields = []; |
|||
q.returnGeometry = true; |
|||
|
|||
const res = await assetLayer.queryFeatures(q); |
|||
if (res.features && res.features.length > 0) { |
|||
geom = res.features[0].geometry; |
|||
setFeatures((prev) => prev.map((pf) => (pf.objectid === feature.objectid ? { ...pf, geometry: geom } : pf))); |
|||
} |
|||
} |
|||
|
|||
if (!geom) return; |
|||
|
|||
if (highlightHandle) { |
|||
try { |
|||
highlightHandle.remove(); |
|||
} catch { |
|||
/* ignore */ |
|||
} |
|||
setHighlightHandle(null); |
|||
} |
|||
|
|||
const assetLayerInMap = findAssetLayerInMap(jimuMapView.view.map); |
|||
if (assetLayerInMap) { |
|||
const layerView = await jimuMapView.view.whenLayerView(assetLayerInMap); |
|||
if (layerView) { |
|||
try { |
|||
const hl = layerView.highlight(feature.objectid); |
|||
setHighlightHandle(hl); |
|||
|
|||
setTimeout(() => { |
|||
try { |
|||
hl.remove(); |
|||
setHighlightHandle(null); |
|||
} catch { |
|||
/* ignore */ |
|||
} |
|||
}, 5000); |
|||
} catch { |
|||
try { |
|||
const graphic = new Graphic({ geometry: geom, attributes: feature as any }); |
|||
const hl = layerView.highlight(graphic); |
|||
setHighlightHandle(hl); |
|||
|
|||
setTimeout(() => { |
|||
try { |
|||
hl.remove(); |
|||
setHighlightHandle(null); |
|||
} catch { |
|||
/* ignore */ |
|||
} |
|||
}, 5000); |
|||
} catch (gErr) { |
|||
console.warn("Highlight failed:", gErr); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
await jimuMapView.view.goTo({ target: geom, zoom: 18 }); |
|||
} catch (err) { |
|||
console.error("Error in zoomToFeature:", err); |
|||
throw err; |
|||
} |
|||
}; |
|||
|
|||
const handleRowZoom = async (feature: AssetFeatureInfo) => { |
|||
try { |
|||
await zoomToFeature(feature); |
|||
} catch (err) { |
|||
console.error("Row zoom failed:", err); |
|||
alert("Zoom failed — check console."); |
|||
} |
|||
}; |
|||
|
|||
const zoomToLayer = async () => { |
|||
if (!jimuMapView?.view) return; |
|||
|
|||
try { |
|||
const map = jimuMapView.view.map; |
|||
const layerInMap = findAssetLayerInMap(map); |
|||
if (!layerInMap) return; |
|||
|
|||
if ((layerInMap as any).fullExtent) { |
|||
const fullExt = (layerInMap as any).fullExtent; |
|||
const padded = fullExt.expand && typeof fullExt.expand === "function" ? fullExt.expand(1.2) : fullExt; |
|||
await jimuMapView.view.goTo({ target: padded }); |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
const q = layerInMap.createQuery(); |
|||
q.where = "1=1"; |
|||
const extentResult: any = await layerInMap.queryExtent(q); |
|||
if (extentResult && extentResult.extent) { |
|||
const ext = extentResult.extent; |
|||
const padded = ext.expand && typeof ext.expand === "function" ? ext.expand(1.2) : ext; |
|||
await jimuMapView.view.goTo({ target: padded }); |
|||
return; |
|||
} |
|||
} catch (qe) { |
|||
console.warn("queryExtent failed:", qe); |
|||
} |
|||
} catch (err) { |
|||
console.error("Error in zoomToLayer:", err); |
|||
} |
|||
}; |
|||
|
|||
const getOidsWithAttachments = async (allObjectIds: number[], initialChunk = 500) => { |
|||
const hasAtt = new Set<number>(); |
|||
|
|||
const tryChunk = async (ids: number[], chunkSize: number) => { |
|||
for (let i = 0; i < ids.length; i += chunkSize) { |
|||
const chunk = ids.slice(i, i + chunkSize); |
|||
try { |
|||
const attachResult = await assetLayer.queryAttachments({ objectIds: chunk }); |
|||
for (const oid of chunk) { |
|||
const arr = (attachResult as any)[String(oid)] || (attachResult as any)[oid]; |
|||
if (arr && arr.length > 0) hasAtt.add(oid); |
|||
} |
|||
setFetchProgress((p) => ({ ...p, fetched: Math.min(p.total, p.fetched + chunk.length) })); |
|||
} catch (err) { |
|||
console.warn(`queryAttachments failed for chunkSize ${chunkSize}`, err); |
|||
if (chunkSize <= 50) throw err; |
|||
await tryChunk(chunk, Math.max(50, Math.floor(chunkSize / 5))); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
setFetchProgress({ fetched: 0, total: allObjectIds.length, phase: "attachments" }); |
|||
await tryChunk(allObjectIds, initialChunk); |
|||
return hasAtt; |
|||
}; |
|||
|
|||
const fetchAttributesForOids = async (oids: number[], attrChunk = 500) => { |
|||
const results: AssetFeatureInfo[] = []; |
|||
const objectIdField = assetLayer.objectIdField || "objectid"; |
|||
setFetchProgress({ fetched: 0, total: oids.length, phase: "attributes" }); |
|||
|
|||
for (let i = 0; i < oids.length; i += attrChunk) { |
|||
const slice = oids.slice(i, i + attrChunk); |
|||
|
|||
try { |
|||
const fq = assetLayer.createQuery(); |
|||
fq.objectIds = slice; |
|||
fq.outFields = [ |
|||
objectIdField, |
|||
"zone", |
|||
"ward", |
|||
"road_name", |
|||
"category", |
|||
"asset_code", |
|||
"asset_type", |
|||
"name_of_the_asset", |
|||
"address", |
|||
"building_status", |
|||
"status", |
|||
"ownership", |
|||
"remarks", |
|||
"created_date" |
|||
]; |
|||
fq.returnGeometry = true; |
|||
|
|||
const res = await assetLayer.queryFeatures(fq); |
|||
|
|||
for (const feat of res.features) { |
|||
const cdVal = feat.attributes.created_date; |
|||
const createdDateStr = cdVal ? new Date(cdVal).toLocaleString() : ""; |
|||
|
|||
results.push({ |
|||
objectid: feat.attributes[objectIdField], |
|||
zone: feat.attributes.zone || "", |
|||
ward: feat.attributes.ward || "", |
|||
road_name: feat.attributes.road_name || "", |
|||
category: feat.attributes.category || "", |
|||
asset_code: feat.attributes.asset_code || "", |
|||
asset_type: feat.attributes.asset_type || "", |
|||
name_of_the_asset: feat.attributes.name_of_the_asset || "", |
|||
address: feat.attributes.address || "", |
|||
building_status: feat.attributes.building_status || "", |
|||
status: feat.attributes.status || "", |
|||
ownership: feat.attributes.ownership || "", |
|||
remarks: feat.attributes.remarks || "", |
|||
created_date: createdDateStr, |
|||
geometry: feat.geometry |
|||
}); |
|||
} |
|||
|
|||
setFetchProgress((p) => ({ ...p, fetched: Math.min(p.total, p.fetched + slice.length) })); |
|||
} catch (err) { |
|||
console.error("Failed to fetch attributes for slice:", err); |
|||
if (attrChunk <= 50) throw err; |
|||
const smaller = Math.max(50, Math.floor(attrChunk / 5)); |
|||
const partial = await fetchAttributesForOids(slice, smaller); |
|||
results.push(...partial); |
|||
} |
|||
} |
|||
|
|||
return results; |
|||
}; |
|||
|
|||
const checkFeatureDetails = async () => { |
|||
setLoading(true); |
|||
setFeatures([]); |
|||
setSummary(null); |
|||
|
|||
try { |
|||
let where = "1=1"; |
|||
if (selectedZone && selectedZone !== "All") where += ` AND zone='${escapeSql(selectedZone)}'`; |
|||
if (selectedWard && selectedWard !== "All") where += ` AND ward='${escapeSql(selectedWard)}'`; |
|||
if (selectedCategory && selectedCategory !== "All") where += ` AND category='${escapeSql(selectedCategory)}'`; |
|||
|
|||
const countQuery = assetLayer.createQuery(); |
|||
countQuery.where = where; |
|||
const totalCount = await assetLayer.queryFeatureCount(countQuery); |
|||
|
|||
const objectIdQuery = assetLayer.createQuery(); |
|||
objectIdQuery.where = where; |
|||
const allObjectIds = await assetLayer.queryObjectIds(objectIdQuery); |
|||
|
|||
if (!allObjectIds || allObjectIds.length === 0) { |
|||
setSummary({ |
|||
zone: selectedZone === "All" ? "All Zones" : selectedZone || "Not selected", |
|||
ward: selectedWard === "All" ? "All Wards" : selectedWard || "Not selected", |
|||
category: selectedCategory === "All" ? "All Categories" : selectedCategory || "Not selected", |
|||
totalCount: 0, |
|||
withAttachment: 0, |
|||
withoutAttachment: 0 |
|||
}); |
|||
setFeatures([]); |
|||
setLoading(false); |
|||
return; |
|||
} |
|||
|
|||
setFetchingDetails(true); |
|||
setFetchProgress({ fetched: 0, total: 0, phase: "" }); |
|||
|
|||
const hasAttachmentSet = await getOidsWithAttachments(allObjectIds, 500); |
|||
const withAttachment = hasAttachmentSet.size; |
|||
const withoutAttachment = allObjectIds.length - withAttachment; |
|||
|
|||
setSummary({ |
|||
zone: selectedZone === "All" ? "All Zones" : selectedZone || "Not selected", |
|||
ward: selectedWard === "All" ? "All Wards" : selectedWard || "Not selected", |
|||
category: selectedCategory === "All" ? "All Categories" : selectedCategory || "Not selected", |
|||
totalCount, |
|||
withAttachment, |
|||
withoutAttachment |
|||
}); |
|||
|
|||
const oidsWithout = allObjectIds.filter((oid) => !hasAttachmentSet.has(oid)); |
|||
if (oidsWithout.length === 0) { |
|||
setFeatures([]); |
|||
setFetchingDetails(false); |
|||
setFetchProgress({ fetched: 0, total: 0, phase: "" }); |
|||
setLoading(false); |
|||
return; |
|||
} |
|||
|
|||
const featureRows = await fetchAttributesForOids(oidsWithout, 500); |
|||
setFeatures(featureRows); |
|||
|
|||
setFetchingDetails(false); |
|||
setFetchProgress({ fetched: 0, total: 0, phase: "" }); |
|||
} catch (err) { |
|||
console.error("Error in checkFeatureDetails:", err); |
|||
alert("Failed to compute counts / fetch details — see console for details."); |
|||
setFetchingDetails(false); |
|||
setFetchProgress({ fetched: 0, total: 0, phase: "" }); |
|||
} finally { |
|||
setLoading(false); |
|||
} |
|||
}; |
|||
|
|||
const resetAll = async () => { |
|||
setSelectedZone(""); |
|||
setSelectedWard(""); |
|||
setSelectedCategory(""); |
|||
setSummary(null); |
|||
setFeatures([]); |
|||
setLoading(false); |
|||
setFetchingDetails(false); |
|||
setFetchProgress({ fetched: 0, total: 0, phase: "" }); |
|||
|
|||
if (highlightHandle) { |
|||
try { |
|||
highlightHandle.remove(); |
|||
} catch { |
|||
/* ignore */ |
|||
} |
|||
setHighlightHandle(null); |
|||
} |
|||
|
|||
if (jimuMapView?.view) { |
|||
try { |
|||
jimuMapView.view.graphics.removeAll(); |
|||
} catch { |
|||
/* ignore */ |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const exportToExcel = () => { |
|||
if (!summary) { |
|||
alert("No report available. Run 'Show Summary & Fetch Details' first."); |
|||
return; |
|||
} |
|||
if (!features || features.length === 0) { |
|||
alert("No feature list available to export."); |
|||
return; |
|||
} |
|||
|
|||
const summarySheetData = [ |
|||
["Zone", summary.zone], |
|||
["Ward", summary.ward], |
|||
["Category", summary.category], |
|||
["Total Features", summary.totalCount], |
|||
["With Attachment", summary.withAttachment], |
|||
["Without Attachment", summary.withoutAttachment] |
|||
]; |
|||
|
|||
const featureData = sortedFeatures.map((f, i) => ({ |
|||
"Sl.No": i + 1, |
|||
ObjectID: f.objectid, |
|||
Zone: f.zone, |
|||
Ward: f.ward, |
|||
Category: f.category, |
|||
"Road Name": f.road_name, |
|||
"Asset Code": f.asset_code, |
|||
"Asset Type": f.asset_type, |
|||
"Asset Name": f.name_of_the_asset, |
|||
Address: f.address, |
|||
Status: f.status, |
|||
Ownership: f.ownership, |
|||
"Building Status": f.building_status, |
|||
Remarks: f.remarks, |
|||
"Created Date": f.created_date |
|||
})); |
|||
|
|||
const wb = XLSX.utils.book_new(); |
|||
const wsSummary = XLSX.utils.aoa_to_sheet(summarySheetData); |
|||
const wsData = XLSX.utils.json_to_sheet(featureData); |
|||
|
|||
if (featureData.length > 0) { |
|||
const headers = Object.keys(featureData[0]); |
|||
const colWidths = headers.map((h) => { |
|||
const maxLen = Math.max(h.length, ...featureData.map((r) => (r[h] ? String(r[h]).length : 0))); |
|||
return { wch: Math.min(Math.max(maxLen + 2, 10), 50) }; |
|||
}); |
|||
(wsData as any)["!cols"] = colWidths; |
|||
} |
|||
|
|||
XLSX.utils.book_append_sheet(wb, wsSummary, "Summary"); |
|||
XLSX.utils.book_append_sheet(wb, wsData, "Without_Attachments"); |
|||
|
|||
const zonePart = summary.zone ? safeFileName(summary.zone) : "AllZones"; |
|||
const wardPart = summary.ward ? safeFileName(summary.ward) : "AllWards"; |
|||
const categoryPart = summary.category ? safeFileName(summary.category) : "AllCategories"; |
|||
const ts = new Date().toISOString().replace(/[:.]/g, "-"); |
|||
|
|||
XLSX.writeFile(wb, `Asset_WithoutAttachments_${zonePart}_${wardPart}_${categoryPart}_${ts}.xlsx`); |
|||
}; |
|||
|
|||
const exportToPDF = () => { |
|||
if (!summary) { |
|||
alert("No report available. Run 'Show Summary & Fetch Details' first."); |
|||
return; |
|||
} |
|||
if (!features || features.length === 0) { |
|||
alert("No feature list available to export."); |
|||
return; |
|||
} |
|||
|
|||
const doc = new jsPDF({ orientation: "landscape", unit: "pt", format: "a4" }); |
|||
doc.setFontSize(14); |
|||
doc.text("Asset - Features Without Attachments", 40, 40); |
|||
|
|||
doc.setFontSize(10); |
|||
doc.text(`Zone: ${summary.zone}`, 40, 65); |
|||
doc.text(`Ward: ${summary.ward}`, 220, 65); |
|||
doc.text(`Category: ${summary.category}`, 400, 65); |
|||
doc.text(`Total: ${summary.totalCount}`, 590, 65); |
|||
doc.text(`Without Attachments: ${summary.withoutAttachment}`, 700, 65); |
|||
|
|||
const head = [[ |
|||
"Sl.No", |
|||
"ObjectID", |
|||
"Zone", |
|||
"Ward", |
|||
"Category", |
|||
"Road Name", |
|||
"Asset Code", |
|||
"Asset Type", |
|||
"Asset Name", |
|||
"Address", |
|||
"Status", |
|||
"Ownership", |
|||
"Building Status", |
|||
"Remarks", |
|||
"Created Date" |
|||
]]; |
|||
|
|||
const body = sortedFeatures.map((f, i) => [ |
|||
i + 1, |
|||
f.objectid, |
|||
f.zone || "", |
|||
f.ward || "", |
|||
f.category || "", |
|||
f.road_name || "", |
|||
f.asset_code || "", |
|||
f.asset_type || "", |
|||
f.name_of_the_asset || "", |
|||
f.address || "", |
|||
f.status || "", |
|||
f.ownership || "", |
|||
f.building_status || "", |
|||
f.remarks || "", |
|||
f.created_date || "" |
|||
]); |
|||
|
|||
(doc as any).autoTable({ |
|||
startY: 85, |
|||
head, |
|||
body, |
|||
styles: { fontSize: 8 }, |
|||
headStyles: { fillColor: [200, 200, 200] }, |
|||
margin: { left: 20, right: 20 } |
|||
}); |
|||
|
|||
const zonePart = summary.zone ? safeFileName(summary.zone) : "AllZones"; |
|||
const wardPart = summary.ward ? safeFileName(summary.ward) : "AllWards"; |
|||
const categoryPart = summary.category ? safeFileName(summary.category) : "AllCategories"; |
|||
const ts = new Date().toISOString().replace(/[:.]/g, "-"); |
|||
|
|||
doc.save(`Asset_WithoutAttachments_${zonePart}_${wardPart}_${categoryPart}_${ts}.pdf`); |
|||
}; |
|||
const sortedFeatures = useMemo(() => { |
|||
return [...features].sort((a, b) => { |
|||
const zoneCmp = String(a.zone || "").trim().localeCompare(String(b.zone || "").trim(), undefined, { |
|||
numeric: true, |
|||
sensitivity: "base" |
|||
}); |
|||
if (zoneCmp !== 0) return zoneCmp; |
|||
|
|||
const wardCmp = String(a.ward || "").trim().localeCompare(String(b.ward || "").trim(), undefined, { |
|||
numeric: true, |
|||
sensitivity: "base" |
|||
}); |
|||
if (wardCmp !== 0) return wardCmp; |
|||
|
|||
return String(a.category || "").trim().localeCompare(String(b.category || "").trim(), undefined, { |
|||
numeric: true, |
|||
sensitivity: "base" |
|||
}); |
|||
}); |
|||
}, [features]); |
|||
|
|||
return ( |
|||
<div className="property-widget-parent"> |
|||
<div className="property-widget"> |
|||
<JimuMapViewComponent |
|||
useMapWidgetId={props.useMapWidgetIds?.[0]} |
|||
onActiveViewChange={activeViewChangeHandler} |
|||
/> |
|||
|
|||
<div> |
|||
<label htmlFor="zoneSelect">Zone: </label> |
|||
<select |
|||
id="zoneSelect" |
|||
value={selectedZone} |
|||
onChange={(e) => { |
|||
setSelectedZone(e.target.value); |
|||
setSelectedWard(""); |
|||
setSelectedCategory(""); |
|||
setSummary(null); |
|||
setFeatures([]); |
|||
}} |
|||
> |
|||
<option value="">-- Select Zone --</option> |
|||
<option value="All">All Zones</option> |
|||
{zones.map((z) => ( |
|||
<option key={z} value={z}> |
|||
{z} |
|||
</option> |
|||
))} |
|||
</select> |
|||
</div> |
|||
|
|||
<div> |
|||
<label htmlFor="wardSelect">Ward: </label> |
|||
<select |
|||
id="wardSelect" |
|||
value={selectedWard} |
|||
onChange={(e) => { |
|||
setSelectedWard(e.target.value); |
|||
setSelectedCategory(""); |
|||
setSummary(null); |
|||
setFeatures([]); |
|||
}} |
|||
disabled={!selectedZone} |
|||
> |
|||
<option value="">-- Select Ward --</option> |
|||
{selectedZone && <option value="All">All Wards</option>} |
|||
{wards.map((w) => ( |
|||
<option key={w} value={w}> |
|||
{w} |
|||
</option> |
|||
))} |
|||
</select> |
|||
</div> |
|||
|
|||
<div> |
|||
<label htmlFor="categorySelect">Category: </label> |
|||
<select |
|||
id="categorySelect" |
|||
value={selectedCategory} |
|||
onChange={(e) => { |
|||
setSelectedCategory(e.target.value); |
|||
setSummary(null); |
|||
setFeatures([]); |
|||
}} |
|||
disabled={!selectedWard} |
|||
> |
|||
<option value="">-- Select Category --</option> |
|||
{selectedWard && <option value="All">All Categories</option>} |
|||
{categories.map((c) => ( |
|||
<option key={c} value={c}> |
|||
{c} |
|||
</option> |
|||
))} |
|||
</select> |
|||
</div> |
|||
|
|||
<div style={{ marginTop: "10px", display: "flex", gap: 8, flexWrap: "wrap" }}> |
|||
<button onClick={checkFeatureDetails} disabled={loading || fetchingDetails || !selectedZone || !selectedWard || !selectedCategory}> |
|||
{loading |
|||
? "Calculating full counts..." |
|||
: fetchingDetails |
|||
? `Fetching... (${fetchProgress.phase} ${fetchProgress.fetched}/${fetchProgress.total})` |
|||
: "Show Summary & Fetch Details"} |
|||
</button> |
|||
|
|||
<button onClick={resetAll} disabled={loading || fetchingDetails} type="button"> |
|||
♻️ Reset |
|||
</button> |
|||
|
|||
<button onClick={zoomToLayer} disabled={!jimuMapView?.view} type="button"> |
|||
🔎 Zoom to Layer |
|||
</button> |
|||
|
|||
<button onClick={exportToExcel} disabled={!features || features.length === 0} type="button"> |
|||
⬇️ Export Excel |
|||
</button> |
|||
|
|||
<button onClick={exportToPDF} disabled={!features || features.length === 0} type="button"> |
|||
📄 Export PDF |
|||
</button> |
|||
</div> |
|||
|
|||
{summary && ( |
|||
<div style={{ marginTop: "15px", border: "1px solid #ccc", padding: "10px" }}> |
|||
<p><strong>Zone:</strong> {summary.zone}</p> |
|||
<p><strong>Ward:</strong> {summary.ward}</p> |
|||
<p><strong>Category:</strong> {summary.category}</p> |
|||
<p><strong>Total Features:</strong> {summary.totalCount}</p> |
|||
<p><strong>With Attachment:</strong> ✅ {summary.withAttachment}</p> |
|||
<p><strong>Without Attachment:</strong> ❌ {summary.withoutAttachment}</p> |
|||
|
|||
{fetchingDetails && ( |
|||
<p style={{ color: "#0074D9" }}> |
|||
Fetching details... Phase: {fetchProgress.phase} — {fetchProgress.fetched}/{fetchProgress.total} |
|||
</p> |
|||
)} |
|||
|
|||
{!fetchingDetails && summary.withoutAttachment > 0 && ( |
|||
<p style={{ color: "#666", marginTop: 8 }}> |
|||
Details fetched for {features.length} asset(s) without attachments. You can export the list using Excel or PDF. |
|||
</p> |
|||
)} |
|||
|
|||
{summary.withoutAttachment === 0 && <p style={{ color: "#0A0" }}>All assets have attachments.</p>} |
|||
</div> |
|||
)} |
|||
|
|||
{features.length > 0 && ( |
|||
<div style={{ marginTop: "15px" }}> |
|||
<h4>Assets Without Attachments: ({features.length})</h4> |
|||
<div |
|||
style={{ |
|||
maxHeight: "500px", |
|||
overflow: "auto", |
|||
position: "relative", |
|||
border: "1px solid #ccc", |
|||
borderRadius: "4px" |
|||
}} |
|||
> |
|||
<div style={{ overflowX: "auto", position: "relative" }}> |
|||
<table |
|||
style={{ |
|||
borderCollapse: "collapse", |
|||
width: "100%", |
|||
minWidth: "1400px" |
|||
}} |
|||
border={1} |
|||
> |
|||
<thead |
|||
style={{ |
|||
position: "sticky", |
|||
top: 0, |
|||
zIndex: 10, |
|||
backgroundColor: "#f5f5f5", |
|||
boxShadow: "0 2px 2px -1px rgba(0,0,0,0.1)" |
|||
}} |
|||
> |
|||
<tr> |
|||
<th style={{ padding: "8px", textAlign: "left", backgroundColor: "#f5f5f5", whiteSpace: "nowrap" }}>Sl.no</th> |
|||
<th style={{ padding: "8px", textAlign: "left", backgroundColor: "#f5f5f5", whiteSpace: "nowrap" }}>ObjectID</th> |
|||
<th style={{ padding: "8px", textAlign: "left", backgroundColor: "#f5f5f5", whiteSpace: "nowrap" }}>Zone</th> |
|||
<th style={{ padding: "8px", textAlign: "left", backgroundColor: "#f5f5f5", whiteSpace: "nowrap" }}>Ward</th> |
|||
<th style={{ padding: "8px", textAlign: "left", backgroundColor: "#f5f5f5", whiteSpace: "nowrap" }}>Category</th> |
|||
<th style={{ padding: "8px", textAlign: "left", backgroundColor: "#f5f5f5", whiteSpace: "nowrap" }}>Road Name</th> |
|||
<th style={{ padding: "8px", textAlign: "left", backgroundColor: "#f5f5f5", whiteSpace: "nowrap" }}>Asset Code</th> |
|||
<th style={{ padding: "8px", textAlign: "left", backgroundColor: "#f5f5f5", whiteSpace: "nowrap" }}>Action</th> |
|||
</tr> |
|||
</thead> |
|||
|
|||
<tbody> |
|||
{sortedFeatures.map((f, index) => ( |
|||
<tr |
|||
key={f.objectid} |
|||
style={{ |
|||
backgroundColor: index % 2 === 0 ? "#fff" : "#f9f9f9", |
|||
cursor: "pointer" |
|||
}} |
|||
onClick={() => { |
|||
void handleRowZoom(f); |
|||
}} |
|||
onDoubleClick={() => { |
|||
void handleRowZoom(f); |
|||
}} |
|||
title="Click row to zoom" |
|||
> |
|||
<td style={{ padding: "8px", whiteSpace: "nowrap" }}>{index + 1}</td> |
|||
<td style={{ padding: "8px", whiteSpace: "nowrap" }}>{f.objectid}</td> |
|||
<td style={{ padding: "8px", whiteSpace: "nowrap" }}>{f.zone}</td> |
|||
<td style={{ padding: "8px", whiteSpace: "nowrap" }}>{f.ward}</td> |
|||
<td style={{ padding: "8px", whiteSpace: "nowrap" }}>{f.category}</td> |
|||
<td style={{ padding: "8px", whiteSpace: "nowrap" }}>{f.road_name}</td> |
|||
<td style={{ padding: "8px", whiteSpace: "nowrap" }}>{f.asset_code}</td> |
|||
<td style={{ padding: "8px", whiteSpace: "nowrap" }}> |
|||
<button |
|||
title="Zoom to feature" |
|||
onClick={async (e) => { |
|||
e.stopPropagation(); |
|||
await handleRowZoom(f); |
|||
}} |
|||
disabled={!jimuMapView?.view} |
|||
style={{ |
|||
padding: "4px 8px", |
|||
background: "#007acc", |
|||
color: "white", |
|||
border: "none", |
|||
borderRadius: "3px", |
|||
cursor: "pointer" |
|||
}} |
|||
> |
|||
📍 |
|||
</button> |
|||
</td> |
|||
</tr> |
|||
))} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
|
|||
<div |
|||
style={{ |
|||
marginTop: "10px", |
|||
fontSize: "0.9em", |
|||
color: "#666", |
|||
display: "flex", |
|||
justifyContent: "space-between", |
|||
alignItems: "center" |
|||
}} |
|||
> |
|||
<span>Scroll vertically and horizontally to view all data</span> |
|||
<span>Total rows: {features.length}</span> |
|||
</div> |
|||
</div> |
|||
)} |
|||
|
|||
{summary && features.length === 0 && summary.totalCount > 0 && ( |
|||
<p style={{ marginTop: "10px" }}>All assets have attachments or the detailed list could not be fetched. ✅</p> |
|||
)} |
|||
|
|||
{summary && summary.totalCount === 0 && <p style={{ marginTop: "10px" }}>No assets found for the selected criteria.</p>} |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default AssetAttachmentStatus; |
|||
@ -0,0 +1,8 @@ |
|||
import { React, AllWidgetProps } from 'jimu-core'; |
|||
import ZoneAbstractWidget from '../index'; |
|||
|
|||
const Widget = (props: AllWidgetProps<any>) => { |
|||
return <ZoneAbstractWidget {...props} />; |
|||
}; |
|||
|
|||
export default Widget; |
|||
@ -0,0 +1,24 @@ |
|||
.widget-container { |
|||
padding: 15px; |
|||
background: white; |
|||
border-radius: 8px; |
|||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); |
|||
} |
|||
|
|||
h3 { |
|||
font-size: 18px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.export-button { |
|||
margin-top: 10px; |
|||
background-color: #0078d4; |
|||
color: white; |
|||
} |
|||
|
|||
.table-container { |
|||
max-height: 400px; |
|||
overflow-y: auto; |
|||
border: 1px solid #ddd; |
|||
} |
|||
|
|||
@ -0,0 +1,14 @@ |
|||
import { React } from 'jimu-core' |
|||
import _Widget from '../src/runtime/widget' |
|||
import { widgetRender, wrapWidget } from 'jimu-for-test' |
|||
|
|||
const render = widgetRender() |
|||
describe('test simple widget', () => { |
|||
it('simple test', () => { |
|||
const Widget = wrapWidget(_Widget, { |
|||
config: { exampleConfigProperty: 'a' } |
|||
}) |
|||
const { queryByText } = render(<Widget widgetId="Widget_1" />) |
|||
expect(queryByText('exampleConfigProperty: a').tagName).toBe('P') |
|||
}) |
|||
}) |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"exampleConfigProperty": "test" |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
<?xml version="1.0" encoding="iso-8859-1"?> |
|||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> |
|||
<svg height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" |
|||
viewBox="0 0 179.004 179.004" xml:space="preserve"> |
|||
<g> |
|||
<path style="fill:#010002;" d="M127.124,127.773c0,0.77,0,1.504,0,2.22c4.075,1.671,7.769,2.685,10.472,1.939 |
|||
c5.955-1.635,10.705-8.169,15.293-14.488c3.574-4.905,6.951-9.517,11.116-12.22c0-30.419,0-70.212,0-83.286 |
|||
c-0.018,0.012-0.036,0.024-0.054,0.036c-0.519-4.344-15.365-4.571-18.396-4.571c-3.025,0-17.871,0.227-18.402,4.571 |
|||
c-0.012-0.012-0.03-0.024-0.042-0.036c0,12.059,0,46.858,0,76.048c6.128,0.37,15.943,2.387,17.531,13.3 |
|||
c0.537,3.61-0.316,6.922-2.446,9.553C139.064,124.73,133.556,126.675,127.124,127.773z M130.221,22.213 |
|||
c1.575-0.835,6.808-2.047,15.335-2.047c8.539,0,13.778,1.211,15.335,2.047c-1.569,0.829-6.802,2.041-15.335,2.041 |
|||
C137.029,24.248,131.796,23.036,130.221,22.213z M114.242,98.326c1.456-0.095,2.948-0.185,4.439-0.269c0-20.544,0-42.979,0-54.304 |
|||
c-0.018,0.012-0.036,0.012-0.054,0.024c-0.519-4.332-15.365-4.559-18.39-4.559c-3.025,0-17.877,0.227-18.396,4.559 |
|||
c-0.012-0.012-0.036-0.012-0.048-0.024c0,10.663,0,31.195,0,50.706c0.853,0.197,1.701,0.4,2.566,0.615 |
|||
c5.967,1.438,12.131,2.924,18.121,3.485C105.948,98.893,110.071,98.589,114.242,98.326z M84.903,44.004 |
|||
c1.575-0.841,6.82-2.035,15.341-2.035c8.533,0,13.783,1.193,15.335,2.035c-1.563,0.841-6.808,2.047-15.335,2.047 |
|||
C91.723,46.051,86.478,44.845,84.903,44.004z M73.375,92.646c0-8.926,0-12.703,0-27.096c-0.018,0.018-0.036,0.024-0.054,0.036 |
|||
c-0.519-4.338-15.365-4.571-18.396-4.571c-3.019,0-17.877,0.233-18.396,4.571c-0.006-0.012-0.03-0.018-0.042-0.036 |
|||
c0,17.93,0,19.392,0,34.876c6.981-4.266,14.404-7.584,22.316-8.592C63.398,91.25,68.285,91.727,73.375,92.646z M54.931,67.848 |
|||
c-8.533,0-13.778-1.205-15.335-2.053c1.575-0.829,6.814-2.029,15.335-2.029c8.539,0,13.778,1.199,15.341,2.029 |
|||
C68.703,66.642,63.464,67.848,54.931,67.848z M0,131.639c15.812-6.689,35.962-32.436,59.37-35.443 |
|||
c12.16-1.569,27.489,5.334,42.711,6.749c13.39,1.247,36.058-5.854,38.218,8.992c1.88,12.859-20.806,12.16-35.968,13.497 |
|||
c11.856,2.363,24.661,13.449,34.435,10.758c12.59-3.479,19.13-22.083,27.907-27.477c9.929-6.104,13.777-2.166,11.85,5.49 |
|||
c-4.791,19.082-13.008,36.708-29.22,42.705c-42.299,15.645-87.886-14.452-119.809,4.242L0,131.639z"/> |
|||
</g> |
|||
</svg> |
|||
@ -0,0 +1,29 @@ |
|||
{ |
|||
"name": "Asset_Updation_Summary", |
|||
"label": "Asset_Updation_Summary", |
|||
"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": { |
|||
"hasSettingPage": false |
|||
}, |
|||
"widgets": [ |
|||
{ |
|||
"uri": "Asset_Updation_Summary", |
|||
"label": "Asset_Updation_Summary", |
|||
"thumbnail": "thumbnail.png", |
|||
"description": "Summarizes Asset and Road." |
|||
} |
|||
], |
|||
"framework": "react", |
|||
"translatedLocales": ["en"], |
|||
"dataSources": [], |
|||
"elements": [] |
|||
} |
|||
|
|||
|
|||
|
|||
@ -0,0 +1,7 @@ |
|||
import { type ImmutableObject } from 'seamless-immutable' |
|||
|
|||
export interface Config { |
|||
exampleConfigProperty: string |
|||
} |
|||
|
|||
export type IMConfig = ImmutableObject<Config> |
|||
@ -0,0 +1,381 @@ |
|||
import React, { useEffect, useState } from 'react'; |
|||
import FeatureLayer from '@arcgis/core/layers/FeatureLayer'; |
|||
import { |
|||
Table, TableBody, TableCell, TableContainer, |
|||
TableHead, TableRow, Paper, Select, MenuItem, Button, Grid |
|||
} from '@mui/material'; |
|||
|
|||
import * as XLSX from 'xlsx'; |
|||
import jsPDF from "jspdf"; |
|||
import autoTable from "jspdf-autotable"; |
|||
|
|||
const CombinedZoneWardSummary = () => { |
|||
const [zone, setZone] = useState(''); |
|||
const [ward, setWard] = useState(''); |
|||
const [zoneList, setZoneList] = useState<string[]>([]); |
|||
const [wardList, setWardList] = useState<string[]>([]); |
|||
|
|||
// 1) Zone + Ward + Category wise
|
|||
const [detailedData, setDetailedData] = useState<any[]>([]); |
|||
|
|||
// 2) Zone total count
|
|||
const [zoneTotalSummary, setZoneTotalSummary] = useState<any[]>([]); |
|||
|
|||
// 3) Ward total count
|
|||
const [wardTotalSummary, setWardTotalSummary] = useState<any[]>([]); |
|||
|
|||
// 4) Zone + Category wise count
|
|||
const [zoneCategorySummary, setZoneCategorySummary] = useState<any[]>([]); |
|||
|
|||
const assetLayerUrl = |
|||
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/Asset_2026_final/FeatureServer/0"; |
|||
|
|||
useEffect(() => { |
|||
fetchZoneList(); |
|||
}, []); |
|||
|
|||
useEffect(() => { |
|||
if (zone) fetchWardList(zone); |
|||
else setWardList([]); |
|||
}, [zone]); |
|||
|
|||
const fetchZoneList = async () => { |
|||
try { |
|||
const layer = new FeatureLayer({ url: assetLayerUrl }); |
|||
const query = layer.createQuery(); |
|||
query.outFields = ["zone"]; |
|||
query.returnDistinctValues = true; |
|||
query.returnGeometry = false; |
|||
|
|||
const result = await layer.queryFeatures(query); |
|||
const zones = [...new Set(result.features.map(f => f.attributes.zone).filter(Boolean))].sort(); |
|||
setZoneList(zones as string[]); |
|||
} catch (err) { |
|||
console.error('fetchZoneList error', err); |
|||
} |
|||
}; |
|||
|
|||
const fetchWardList = async (zoneNo: string) => { |
|||
try { |
|||
const layer = new FeatureLayer({ url: assetLayerUrl }); |
|||
const query = layer.createQuery(); |
|||
query.where = `zone = '${zoneNo}'`; |
|||
query.outFields = ["ward"]; |
|||
query.returnDistinctValues = true; |
|||
query.returnGeometry = false; |
|||
|
|||
const result = await layer.queryFeatures(query); |
|||
const wards = [...new Set(result.features.map(f => f.attributes.ward).filter(Boolean))].sort(); |
|||
setWardList(wards as string[]); |
|||
} catch (err) { |
|||
console.error('fetchWardList error', err); |
|||
} |
|||
}; |
|||
|
|||
const handleFetch = async () => { |
|||
if (!zone) return; |
|||
await fetchAssetSummaries(zone, ward); |
|||
}; |
|||
|
|||
const handleClear = () => { |
|||
setZone(''); |
|||
setWard(''); |
|||
setWardList([]); |
|||
setDetailedData([]); |
|||
setZoneTotalSummary([]); |
|||
setWardTotalSummary([]); |
|||
setZoneCategorySummary([]); |
|||
}; |
|||
|
|||
const fetchAssetSummaries = async (zoneNo: string, wardNo: string) => { |
|||
try { |
|||
const layer = new FeatureLayer({ url: assetLayerUrl }); |
|||
const query = layer.createQuery(); |
|||
query.where = `zone='${zoneNo}'` + (wardNo ? ` AND ward='${wardNo}'` : ""); |
|||
query.outFields = ['zone', 'ward', 'category']; |
|||
query.returnGeometry = false; |
|||
|
|||
const result = await layer.queryFeatures(query); |
|||
|
|||
const detailedMap: Record<string, number> = {}; |
|||
const zoneMap: Record<string, number> = {}; |
|||
const wardMap: Record<string, number> = {}; |
|||
const zoneCategoryMap: Record<string, number> = {}; |
|||
|
|||
result.features.forEach(({ attributes }) => { |
|||
const z = String(attributes.zone || '').trim(); |
|||
const w = String(attributes.ward || '').trim(); |
|||
const c = String(attributes.category || '').trim(); |
|||
|
|||
const detailedKey = `${z}|${w}|${c}`; |
|||
const zoneKey = `${z}`; |
|||
const wardKey = `${z}|${w}`; |
|||
const zoneCategoryKey = `${z}|${c}`; |
|||
|
|||
detailedMap[detailedKey] = (detailedMap[detailedKey] || 0) + 1; |
|||
zoneMap[zoneKey] = (zoneMap[zoneKey] || 0) + 1; |
|||
wardMap[wardKey] = (wardMap[wardKey] || 0) + 1; |
|||
zoneCategoryMap[zoneCategoryKey] = (zoneCategoryMap[zoneCategoryKey] || 0) + 1; |
|||
}); |
|||
|
|||
const detailed = Object.entries(detailedMap) |
|||
.map(([key, count]) => { |
|||
const [z, w, c] = key.split('|'); |
|||
return { zone: z, ward: w, category: c, count }; |
|||
}) |
|||
.sort((a, b) => (a.zone + a.ward + a.category).localeCompare(b.zone + b.ward + b.category)); |
|||
|
|||
const zoneSummary = Object.entries(zoneMap) |
|||
.map(([zone, count]) => ({ zone, count })) |
|||
.sort((a, b) => a.zone.localeCompare(b.zone)); |
|||
|
|||
const wardSummary = Object.entries(wardMap) |
|||
.map(([key, count]) => { |
|||
const [z, w] = key.split('|'); |
|||
return { zone: z, ward: w, count }; |
|||
}) |
|||
.sort((a, b) => (a.zone + a.ward).localeCompare(b.zone + b.ward)); |
|||
|
|||
const zoneCategory = Object.entries(zoneCategoryMap) |
|||
.map(([key, count]) => { |
|||
const [z, c] = key.split('|'); |
|||
return { zone: z, category: c, count }; |
|||
}) |
|||
.sort((a, b) => (a.zone + a.category).localeCompare(b.zone + b.category)); |
|||
|
|||
setDetailedData(detailed); |
|||
setZoneTotalSummary(zoneSummary); |
|||
setWardTotalSummary(wardSummary); |
|||
setZoneCategorySummary(zoneCategory); |
|||
} catch (err) { |
|||
console.error('fetchAssetSummaries error', err); |
|||
setDetailedData([]); |
|||
setZoneTotalSummary([]); |
|||
setWardTotalSummary([]); |
|||
setZoneCategorySummary([]); |
|||
} |
|||
}; |
|||
|
|||
const exportExcel = () => { |
|||
const wb = XLSX.utils.book_new(); |
|||
|
|||
const detailedSheetData = [["Sl.No", "Zone", "Ward", "Category", "Count"]]; |
|||
detailedData.forEach((r, i) => detailedSheetData.push([i + 1, r.zone, r.ward, r.category, r.count])); |
|||
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(detailedSheetData), 'ZoneWardCategory'); |
|||
|
|||
const zoneCategorySheetData = [["Sl.No", "Zone", "Category", "Total Count"]]; |
|||
zoneCategorySummary.forEach((r, i) => zoneCategorySheetData.push([i + 1, r.zone, r.category, r.count])); |
|||
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(zoneCategorySheetData), 'ZoneCategory'); |
|||
|
|||
const zoneSheetData = [["Sl.No", "Zone", "Total Count"]]; |
|||
zoneTotalSummary.forEach((r, i) => zoneSheetData.push([i + 1, r.zone, r.count])); |
|||
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(zoneSheetData), 'ZoneTotal'); |
|||
|
|||
const wardSheetData = [["Sl.No", "Zone", "Ward", "Total Count"]]; |
|||
wardTotalSummary.forEach((r, i) => wardSheetData.push([i + 1, r.zone, r.ward, r.count])); |
|||
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(wardSheetData), 'WardTotal'); |
|||
|
|||
XLSX.writeFile(wb, `Asset_Summary_${zone || 'All'}.xlsx`); |
|||
}; |
|||
|
|||
const exportPDF = () => { |
|||
const doc = new jsPDF({ orientation: "portrait", unit: "pt", format: "a4" }); |
|||
|
|||
doc.setFontSize(14); |
|||
doc.text(`Zone + Ward + Category Wise Count`, 40, 40); |
|||
autoTable(doc, { |
|||
head: [["Sl.No", "Zone", "Ward", "Category", "Count"]], |
|||
body: detailedData.map((r, i) => [i + 1, r.zone, r.ward, r.category, r.count]), |
|||
startY: 60, |
|||
styles: { fontSize: 8 }, |
|||
}); |
|||
|
|||
doc.addPage(); |
|||
doc.text(`Zone + Category Wise Count`, 40, 40); |
|||
autoTable(doc, { |
|||
head: [["Sl.No", "Zone", "Category", "Total Count"]], |
|||
body: zoneCategorySummary.map((r, i) => [i + 1, r.zone, r.category, r.count]), |
|||
startY: 60, |
|||
styles: { fontSize: 8 }, |
|||
}); |
|||
|
|||
doc.addPage(); |
|||
doc.text(`Zone Total Count`, 40, 40); |
|||
autoTable(doc, { |
|||
head: [["Sl.No", "Zone", "Total Count"]], |
|||
body: zoneTotalSummary.map((r, i) => [i + 1, r.zone, r.count]), |
|||
startY: 60, |
|||
styles: { fontSize: 8 }, |
|||
}); |
|||
|
|||
doc.addPage(); |
|||
doc.text(`Ward Total Count`, 40, 40); |
|||
autoTable(doc, { |
|||
head: [["Sl.No", "Zone", "Ward", "Total Count"]], |
|||
body: wardTotalSummary.map((r, i) => [i + 1, r.zone, r.ward, r.count]), |
|||
startY: 60, |
|||
styles: { fontSize: 8 }, |
|||
}); |
|||
|
|||
doc.save(`Asset_Summary_${zone || 'All'}.pdf`); |
|||
}; |
|||
|
|||
return ( |
|||
<div style={{ padding: '1rem', height: '100%', overflow: 'hidden' }}> |
|||
<h2>Zone / Ward / Category Summary</h2> |
|||
|
|||
<div style={{ marginBottom: '10px', fontWeight: 600 }}> |
|||
Total Assets: {detailedData.reduce((s, r) => s + r.count, 0)} |
|||
</div> |
|||
|
|||
<Grid container spacing={2} alignItems="center"> |
|||
<Grid item> |
|||
<Select |
|||
value={zone} |
|||
onChange={e => setZone(e.target.value)} |
|||
displayEmpty |
|||
style={{ width: 150 }} |
|||
> |
|||
<MenuItem value="">Select Zone</MenuItem> |
|||
{zoneList.map(z => <MenuItem key={z} value={z}>{z}</MenuItem>)} |
|||
</Select> |
|||
</Grid> |
|||
|
|||
<Grid item> |
|||
<Select |
|||
value={ward} |
|||
onChange={e => setWard(e.target.value)} |
|||
displayEmpty |
|||
style={{ width: 150 }} |
|||
disabled={!zone} |
|||
> |
|||
<MenuItem value="">All Wards</MenuItem> |
|||
{wardList.map(w => <MenuItem key={w} value={w}>{w}</MenuItem>)} |
|||
</Select> |
|||
</Grid> |
|||
|
|||
<Grid item> |
|||
<Button variant="contained" onClick={handleFetch}>FETCH DATA</Button> |
|||
</Grid> |
|||
|
|||
<Grid item> |
|||
<Button variant="outlined" onClick={handleClear}>CLEAR</Button> |
|||
</Grid> |
|||
|
|||
<Grid item> |
|||
<Button variant="contained" onClick={exportExcel} disabled={detailedData.length === 0}> |
|||
Export Excel |
|||
</Button> |
|||
</Grid> |
|||
|
|||
<Grid item> |
|||
<Button variant="contained" onClick={exportPDF} disabled={detailedData.length === 0}> |
|||
Export PDF |
|||
</Button> |
|||
</Grid> |
|||
</Grid> |
|||
|
|||
{detailedData.length > 0 && ( |
|||
<TableContainer component={Paper} style={{ marginTop: '20px', maxHeight: 300, overflow: 'auto' }}> |
|||
<Table size="small" stickyHeader> |
|||
<TableHead> |
|||
<TableRow> |
|||
<TableCell><strong>Sl.No</strong></TableCell> |
|||
<TableCell><strong>Zone</strong></TableCell> |
|||
<TableCell><strong>Ward</strong></TableCell> |
|||
<TableCell><strong>Category</strong></TableCell> |
|||
<TableCell><strong>Count</strong></TableCell> |
|||
</TableRow> |
|||
</TableHead> |
|||
<TableBody> |
|||
{detailedData.map((r, i) => ( |
|||
<TableRow key={i}> |
|||
<TableCell>{i + 1}</TableCell> |
|||
<TableCell>{r.zone}</TableCell> |
|||
<TableCell>{r.ward}</TableCell> |
|||
<TableCell>{r.category}</TableCell> |
|||
<TableCell>{r.count}</TableCell> |
|||
</TableRow> |
|||
))} |
|||
</TableBody> |
|||
</Table> |
|||
</TableContainer> |
|||
)} |
|||
|
|||
{zoneCategorySummary.length > 0 && ( |
|||
<TableContainer component={Paper} style={{ marginTop: '20px', maxHeight: 300, overflow: 'auto' }}> |
|||
<Table size="small" stickyHeader> |
|||
<TableHead> |
|||
<TableRow> |
|||
<TableCell><strong>Sl.No</strong></TableCell> |
|||
<TableCell><strong>Zone</strong></TableCell> |
|||
<TableCell><strong>Category</strong></TableCell> |
|||
<TableCell><strong>Total Count</strong></TableCell> |
|||
</TableRow> |
|||
</TableHead> |
|||
<TableBody> |
|||
{zoneCategorySummary.map((r, i) => ( |
|||
<TableRow key={i}> |
|||
<TableCell>{i + 1}</TableCell> |
|||
<TableCell>{r.zone}</TableCell> |
|||
<TableCell>{r.category}</TableCell> |
|||
<TableCell>{r.count}</TableCell> |
|||
</TableRow> |
|||
))} |
|||
</TableBody> |
|||
</Table> |
|||
</TableContainer> |
|||
)} |
|||
|
|||
{zoneTotalSummary.length > 0 && ( |
|||
<TableContainer component={Paper} style={{ marginTop: '20px', maxHeight: 300, overflow: 'auto' }}> |
|||
<Table size="small" stickyHeader> |
|||
<TableHead> |
|||
<TableRow> |
|||
<TableCell><strong>Sl.No</strong></TableCell> |
|||
<TableCell><strong>Zone</strong></TableCell> |
|||
<TableCell><strong>Total Count</strong></TableCell> |
|||
</TableRow> |
|||
</TableHead> |
|||
<TableBody> |
|||
{zoneTotalSummary.map((r, i) => ( |
|||
<TableRow key={i}> |
|||
<TableCell>{i + 1}</TableCell> |
|||
<TableCell>{r.zone}</TableCell> |
|||
<TableCell>{r.count}</TableCell> |
|||
</TableRow> |
|||
))} |
|||
</TableBody> |
|||
</Table> |
|||
</TableContainer> |
|||
)} |
|||
|
|||
{wardTotalSummary.length > 0 && ( |
|||
<TableContainer component={Paper} style={{ marginTop: '20px', maxHeight: 300, overflow: 'auto' }}> |
|||
<Table size="small" stickyHeader> |
|||
<TableHead> |
|||
<TableRow> |
|||
<TableCell><strong>Sl.No</strong></TableCell> |
|||
<TableCell><strong>Zone</strong></TableCell> |
|||
<TableCell><strong>Ward</strong></TableCell> |
|||
<TableCell><strong>Total Count</strong></TableCell> |
|||
</TableRow> |
|||
</TableHead> |
|||
<TableBody> |
|||
{wardTotalSummary.map((r, i) => ( |
|||
<TableRow key={i}> |
|||
<TableCell>{i + 1}</TableCell> |
|||
<TableCell>{r.zone}</TableCell> |
|||
<TableCell>{r.ward}</TableCell> |
|||
<TableCell>{r.count}</TableCell> |
|||
</TableRow> |
|||
))} |
|||
</TableBody> |
|||
</Table> |
|||
</TableContainer> |
|||
)} |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default CombinedZoneWardSummary; |
|||
@ -0,0 +1,8 @@ |
|||
import { React, AllWidgetProps } from 'jimu-core'; |
|||
import ZoneAbstractWidget from '../index'; |
|||
|
|||
const Widget = (props: AllWidgetProps<any>) => { |
|||
return <ZoneAbstractWidget {...props} />; |
|||
}; |
|||
|
|||
export default Widget; |
|||
@ -0,0 +1,24 @@ |
|||
.widget-container { |
|||
padding: 15px; |
|||
background: white; |
|||
border-radius: 8px; |
|||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); |
|||
} |
|||
|
|||
h3 { |
|||
font-size: 18px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.export-button { |
|||
margin-top: 10px; |
|||
background-color: #0078d4; |
|||
color: white; |
|||
} |
|||
|
|||
.table-container { |
|||
max-height: 400px; |
|||
overflow-y: auto; |
|||
border: 1px solid #ddd; |
|||
} |
|||
|
|||
@ -0,0 +1,14 @@ |
|||
import { React } from 'jimu-core' |
|||
import _Widget from '../src/runtime/widget' |
|||
import { widgetRender, wrapWidget } from 'jimu-for-test' |
|||
|
|||
const render = widgetRender() |
|||
describe('test simple widget', () => { |
|||
it('simple test', () => { |
|||
const Widget = wrapWidget(_Widget, { |
|||
config: { exampleConfigProperty: 'a' } |
|||
}) |
|||
const { queryByText } = render(<Widget widgetId="Widget_1" />) |
|||
expect(queryByText('exampleConfigProperty: a').tagName).toBe('P') |
|||
}) |
|||
}) |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"exampleConfigProperty": "test" |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
<?xml version="1.0" ?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> |
|||
<svg width="800px" height="800px" viewBox="0 0 32 32" data-name="Layer 1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:url(#linear-gradient-2);}.cls-3{fill:#f8edeb;}.cls-4{fill:#577590;}</style><linearGradient gradientUnits="userSpaceOnUse" id="linear-gradient" x1="4" x2="25" y1="16" y2="16"><stop offset="0" stop-color="#f9dcc4"/><stop offset="0.01" stop-color="#f8dbc2"/><stop offset="0.12" stop-color="#f3ceb1"/><stop offset="0.26" stop-color="#f0c5a6"/><stop offset="0.46" stop-color="#eebf9f"/><stop offset="1" stop-color="#edbe9d"/></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="linear-gradient-2" x1="7.48" x2="27.52" y1="8.98" y2="29.02"><stop offset="0" stop-color="#f9dcc4"/><stop offset="0.32" stop-color="#f8d9c0"/><stop offset="0.64" stop-color="#f4cfb3"/><stop offset="0.98" stop-color="#eebf9f"/><stop offset="1" stop-color="#edbe9d"/></linearGradient></defs><rect class="cls-1" height="22" rx="2.5" width="21" x="4" y="5"/><rect class="cls-2" height="22" rx="2.5" width="21" x="7" y="8"/><path class="cls-3" d="M20,2a5,5,0,0,0-5,5v5a2,2,0,0,0,3,1.73V15.5a3.5,3.5,0,0,0,7,0V7A5,5,0,0,0,20,2Z"/><path class="cls-4" d="M20,2a5,5,0,0,0-5,5v4.5a.5.5,0,0,0,1,0V7a4,4,0,0,1,8,0v8.5a2.5,2.5,0,0,1-5,0V7a1,1,0,0,1,2,0v8.5a.5.5,0,0,0,1,0V7a2,2,0,0,0-4,0v8.5a3.5,3.5,0,0,0,7,0V7A5,5,0,0,0,20,2Z"/></svg> |
|||
@ -0,0 +1,29 @@ |
|||
{ |
|||
"name": "Attachment_ShopComplex", |
|||
"label": "Attachment_ShopComplex", |
|||
"type": "widget", |
|||
"version": "1.16.0", |
|||
"exbVersion": "1.16.0", |
|||
"author": "Tanii Team 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": "Attachment_ShopComplex", |
|||
"label": "Attachment_ShopComplex", |
|||
"thumbnail": "thumbnail.png", |
|||
"description": "Attachment Available Status." |
|||
} |
|||
], |
|||
"framework": "react", |
|||
"translatedLocales": ["en"], |
|||
"dataSources": [], |
|||
"elements": [] |
|||
} |
|||
|
|||
|
|||
|
|||
@ -0,0 +1,7 @@ |
|||
import { type ImmutableObject } from 'seamless-immutable' |
|||
|
|||
export interface Config { |
|||
exampleConfigProperty: string |
|||
} |
|||
|
|||
export type IMConfig = ImmutableObject<Config> |
|||
@ -0,0 +1,482 @@ |
|||
import React, { useState, useEffect, useMemo } from "react"; |
|||
import FeatureLayer from "@arcgis/core/layers/FeatureLayer"; |
|||
import { JimuMapViewComponent } from "jimu-arcgis"; |
|||
import { type AllWidgetProps } from "jimu-core"; |
|||
|
|||
import * as XLSX from "xlsx"; |
|||
import { saveAs } from "file-saver"; |
|||
import jsPDF from "jspdf"; |
|||
import autoTable from "jspdf-autotable"; |
|||
|
|||
interface SummaryInfo { |
|||
zone: string; |
|||
ward: string; |
|||
totalCount: number; |
|||
attachedCount: number; |
|||
withoutAttachmentCount: number; |
|||
} |
|||
|
|||
interface FeatureInfo { |
|||
objectid: number; |
|||
zone_no: string; |
|||
ward_no: string; |
|||
shopping_complex_name?: string; |
|||
shopping_complex_no?: string; |
|||
road_name?: string; |
|||
address?: string; |
|||
category?: string; |
|||
attachmentCount: number; |
|||
attachmentStatus: "Attached" | "No Attachment"; |
|||
} |
|||
|
|||
const SHOPPING_COMPLEX_URL = |
|||
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/gccdeptfield/Shopping_Complex_Shop/FeatureServer/1"; |
|||
|
|||
const chunkArray = <T,>(arr: T[], size: number): T[][] => |
|||
arr.reduce((acc: T[][], val, i) => { |
|||
const idx = Math.floor(i / size); |
|||
acc[idx] = acc[idx] || []; |
|||
acc[idx].push(val); |
|||
return acc; |
|||
}, []); |
|||
|
|||
const ShoppingComplexAttachmentStatus = (props: AllWidgetProps<any>) => { |
|||
const zonesList = useMemo(() => { |
|||
const zones = ["All"]; |
|||
for (let i = 1; i <= 15; i++) zones.push(String(i).padStart(2, "0")); |
|||
return zones; |
|||
}, []); |
|||
|
|||
const [zones] = useState<string[]>(zonesList); |
|||
const [wards, setWards] = useState<string[]>([]); |
|||
const [selectedZone, setSelectedZone] = useState<string>("All"); |
|||
const [selectedWard, setSelectedWard] = useState<string>("All"); |
|||
|
|||
const [summary, setSummary] = useState<SummaryInfo | null>(null); |
|||
const [features, setFeatures] = useState<FeatureInfo[]>([]); |
|||
const [loading, setLoading] = useState(false); |
|||
|
|||
const shoppingLayer = useMemo( |
|||
() => |
|||
new FeatureLayer({ |
|||
url: SHOPPING_COMPLEX_URL |
|||
}), |
|||
[] |
|||
); |
|||
|
|||
useEffect(() => { |
|||
const loadWards = async () => { |
|||
try { |
|||
let where = "ward_no IS NOT NULL"; |
|||
if (selectedZone && selectedZone !== "All") { |
|||
where += ` AND zone_no='${selectedZone}'`; |
|||
} |
|||
|
|||
const q = shoppingLayer.createQuery(); |
|||
q.outFields = ["ward_no"]; |
|||
q.where = where; |
|||
q.returnGeometry = false; |
|||
|
|||
const result = await shoppingLayer.queryFeatures(q); |
|||
|
|||
const distinctWards = [ |
|||
...new Set( |
|||
result.features |
|||
.map(f => String(f.attributes.ward_no ?? "").trim()) |
|||
.filter(Boolean) |
|||
) |
|||
]; |
|||
|
|||
const sorted = distinctWards.slice().sort((a, b) => { |
|||
const na = Number(a); |
|||
const nb = Number(b); |
|||
if (!isNaN(na) && !isNaN(nb)) return na - nb; |
|||
return a.localeCompare(b); |
|||
}); |
|||
|
|||
setWards(sorted); |
|||
} catch (err) { |
|||
console.error("loadWards error:", err); |
|||
setWards([]); |
|||
} |
|||
}; |
|||
|
|||
loadWards(); |
|||
}, [selectedZone, shoppingLayer]); |
|||
|
|||
const checkFeatureDetails = async () => { |
|||
setLoading(true); |
|||
try { |
|||
let where = "1=1"; |
|||
if (selectedZone && selectedZone !== "All") { |
|||
where += ` AND zone_no='${selectedZone}'`; |
|||
} |
|||
if (selectedWard && selectedWard !== "All") { |
|||
where += ` AND ward_no='${selectedWard}'`; |
|||
} |
|||
|
|||
const qObj = shoppingLayer.createQuery(); |
|||
qObj.where = where; |
|||
qObj.returnGeometry = false; |
|||
|
|||
const allObjectIds = await shoppingLayer.queryObjectIds(qObj); |
|||
|
|||
if (!allObjectIds || allObjectIds.length === 0) { |
|||
setSummary({ |
|||
zone: selectedZone || "All Zones", |
|||
ward: selectedWard || "All Wards", |
|||
totalCount: 0, |
|||
attachedCount: 0, |
|||
withoutAttachmentCount: 0 |
|||
}); |
|||
setFeatures([]); |
|||
return; |
|||
} |
|||
|
|||
const batchSize = 1000; |
|||
const idChunks = chunkArray(allObjectIds, batchSize); |
|||
|
|||
const fetchedFeatures: __esri.Graphic[] = []; |
|||
|
|||
for (const chunk of idChunks) { |
|||
const q = shoppingLayer.createQuery(); |
|||
q.objectIds = chunk; |
|||
q.outFields = [ |
|||
"objectid", |
|||
"zone_no", |
|||
"ward_no", |
|||
"shopping_complex_name", |
|||
"shopping_complex_no", |
|||
"road_name", |
|||
"address", |
|||
"category" |
|||
]; |
|||
q.returnGeometry = false; |
|||
|
|||
const res = await shoppingLayer.queryFeatures(q); |
|||
fetchedFeatures.push(...res.features); |
|||
} |
|||
|
|||
const featureRows: FeatureInfo[] = fetchedFeatures.map(f => { |
|||
const objectid = |
|||
Number(f.attributes.objectid ?? f.attributes.OBJECTID ?? f.attributes.ObjectId) || 0; |
|||
|
|||
return { |
|||
objectid, |
|||
zone_no: String(f.attributes.zone_no ?? ""), |
|||
ward_no: String(f.attributes.ward_no ?? ""), |
|||
shopping_complex_name: f.attributes.shopping_complex_name, |
|||
shopping_complex_no: f.attributes.shopping_complex_no, |
|||
road_name: f.attributes.road_name, |
|||
address: f.attributes.address, |
|||
category: f.attributes.category, |
|||
attachmentCount: 0, |
|||
attachmentStatus: "No Attachment" |
|||
}; |
|||
}); |
|||
|
|||
const attachMap: Record<string, any[]> = {}; |
|||
const attachChunks = chunkArray( |
|||
featureRows.map(f => f.objectid), |
|||
500 |
|||
); |
|||
|
|||
for (const chunk of attachChunks) { |
|||
const res = await shoppingLayer.queryAttachments({ objectIds: chunk }); |
|||
Object.assign(attachMap, res); |
|||
} |
|||
|
|||
let attachedCount = 0; |
|||
let withoutAttachmentCount = 0; |
|||
|
|||
const mergedRows = featureRows.map(f => { |
|||
const attachments = attachMap[String(f.objectid)] ?? attachMap[f.objectid] ?? []; |
|||
const attachmentCount = Array.isArray(attachments) ? attachments.length : 0; |
|||
const attachmentStatus = attachmentCount > 0 ? "Attached" : "No Attachment"; |
|||
|
|||
if (attachmentStatus === "Attached") attachedCount++; |
|||
else withoutAttachmentCount++; |
|||
|
|||
return { |
|||
...f, |
|||
attachmentCount, |
|||
attachmentStatus |
|||
}; |
|||
}); |
|||
|
|||
setSummary({ |
|||
zone: selectedZone || "All Zones", |
|||
ward: selectedWard || "All Wards", |
|||
totalCount: allObjectIds.length, |
|||
attachedCount, |
|||
withoutAttachmentCount |
|||
}); |
|||
|
|||
setFeatures(mergedRows); |
|||
} catch (err) { |
|||
console.error("Error in checkFeatureDetails:", err); |
|||
} finally { |
|||
setLoading(false); |
|||
} |
|||
}; |
|||
|
|||
const exportToExcel = () => { |
|||
try { |
|||
const summaryRows = [ |
|||
["Zone", summary ? summary.zone : selectedZone || "All Zones"], |
|||
["Ward", summary ? summary.ward : selectedWard || "All Wards"], |
|||
["Total Shops", summary ? summary.totalCount : 0], |
|||
["With Attachment", summary ? summary.attachedCount : 0], |
|||
["Without Attachment", summary ? summary.withoutAttachmentCount : 0] |
|||
]; |
|||
|
|||
const detailHeaders = [ |
|||
"Sl.no", |
|||
"ObjectId", |
|||
"Zone", |
|||
"Ward", |
|||
"Complex Name", |
|||
"Complex No", |
|||
"Road Name", |
|||
"Address", |
|||
"Category", |
|||
"Attachment Count", |
|||
"Attachment Status" |
|||
]; |
|||
|
|||
const detailRows = features.map((f, idx) => [ |
|||
idx + 1, |
|||
f.objectid, |
|||
f.zone_no, |
|||
f.ward_no, |
|||
f.shopping_complex_name || "", |
|||
f.shopping_complex_no || "", |
|||
f.road_name || "", |
|||
f.address || "", |
|||
f.category || "", |
|||
f.attachmentCount, |
|||
f.attachmentStatus |
|||
]); |
|||
|
|||
const wb = XLSX.utils.book_new(); |
|||
|
|||
const wsSummary = XLSX.utils.aoa_to_sheet([ |
|||
["Shopping Complex Attachment Status"], |
|||
[], |
|||
...summaryRows |
|||
]); |
|||
XLSX.utils.book_append_sheet(wb, wsSummary, "Summary"); |
|||
|
|||
const wsDetails = XLSX.utils.aoa_to_sheet([detailHeaders, ...detailRows]); |
|||
XLSX.utils.book_append_sheet(wb, wsDetails, "Details"); |
|||
|
|||
const wbout = XLSX.write(wb, { bookType: "xlsx", type: "array" }); |
|||
saveAs( |
|||
new Blob([wbout], { type: "application/octet-stream" }), |
|||
`Shopping_Complex_Attachment_Status_${selectedZone || "All"}.xlsx` |
|||
); |
|||
} catch (err) { |
|||
console.error("exportToExcel error:", err); |
|||
} |
|||
}; |
|||
|
|||
const exportToPDF = () => { |
|||
try { |
|||
const doc = new jsPDF("l", "pt", "a4"); |
|||
|
|||
doc.setFontSize(14); |
|||
doc.text("Shopping Complex Attachment Status", 40, 40); |
|||
|
|||
const summaryLines = [ |
|||
`Zone: ${summary ? summary.zone : selectedZone || "All Zones"}`, |
|||
`Ward: ${summary ? summary.ward : selectedWard || "All Wards"}`, |
|||
`Total Shops: ${summary ? summary.totalCount : 0}`, |
|||
`With Attachment: ${summary ? summary.attachedCount : 0}`, |
|||
`Without Attachment: ${summary ? summary.withoutAttachmentCount : 0}` |
|||
]; |
|||
|
|||
doc.setFontSize(10); |
|||
let y = 60; |
|||
summaryLines.forEach(line => { |
|||
doc.text(line, 40, y); |
|||
y += 14; |
|||
}); |
|||
|
|||
const headers = [ |
|||
"Sl.no", |
|||
"ObjectId", |
|||
"Zone", |
|||
"Ward", |
|||
"Complex Name", |
|||
"Complex No", |
|||
"Road Name", |
|||
"Address", |
|||
"Category", |
|||
"Attachment Count", |
|||
"Attachment Status" |
|||
]; |
|||
|
|||
const body = features.map((f, idx) => [ |
|||
idx + 1, |
|||
f.objectid, |
|||
f.zone_no, |
|||
f.ward_no, |
|||
f.shopping_complex_name || "", |
|||
f.shopping_complex_no || "", |
|||
f.road_name || "", |
|||
f.address || "", |
|||
f.category || "", |
|||
f.attachmentCount, |
|||
f.attachmentStatus |
|||
]); |
|||
|
|||
if (body.length === 0) { |
|||
doc.text("No records found.", 40, y + 10); |
|||
} else { |
|||
autoTable(doc, { |
|||
startY: y + 6, |
|||
head: [headers], |
|||
body, |
|||
styles: { fontSize: 8 }, |
|||
headStyles: { fillColor: [200, 200, 200] } |
|||
}); |
|||
} |
|||
|
|||
doc.save(`Shopping_Complex_Attachment_Status_${selectedZone || "All"}.pdf`); |
|||
} catch (err) { |
|||
console.error("exportToPDF error:", err); |
|||
} |
|||
}; |
|||
|
|||
return ( |
|||
<div className="property-widget-parent"> |
|||
<div className="property-widget" style={{ padding: 8 }}> |
|||
<JimuMapViewComponent useMapWidgetId={props.useMapWidgetIds?.[0]} /> |
|||
|
|||
<div style={{ marginTop: 8 }}> |
|||
<label>Zone: </label> |
|||
<select |
|||
value={selectedZone} |
|||
onChange={e => { |
|||
setSelectedZone(e.target.value); |
|||
setSelectedWard("All"); |
|||
setSummary(null); |
|||
setFeatures([]); |
|||
}} |
|||
> |
|||
{zones.map(z => ( |
|||
<option key={z} value={z}> |
|||
{z === "All" ? "All Zones" : z} |
|||
</option> |
|||
))} |
|||
</select> |
|||
</div> |
|||
|
|||
<div style={{ marginTop: 8 }}> |
|||
<label>Ward: </label> |
|||
<select |
|||
value={selectedWard} |
|||
onChange={e => { |
|||
setSelectedWard(e.target.value); |
|||
setSummary(null); |
|||
setFeatures([]); |
|||
}} |
|||
> |
|||
<option value="All">All Wards</option> |
|||
{wards.map(w => ( |
|||
<option key={w} value={w}> |
|||
{w} |
|||
</option> |
|||
))} |
|||
</select> |
|||
</div> |
|||
|
|||
<div style={{ marginTop: 10 }}> |
|||
<button onClick={checkFeatureDetails} disabled={loading}> |
|||
{loading ? "Loading..." : "Show Summary"} |
|||
</button> |
|||
|
|||
<button |
|||
onClick={exportToExcel} |
|||
disabled={loading || features.length === 0} |
|||
style={{ marginLeft: 8 }} |
|||
title="Export summary & details to Excel" |
|||
> |
|||
Export Excel |
|||
</button> |
|||
|
|||
<button |
|||
onClick={exportToPDF} |
|||
disabled={loading || features.length === 0} |
|||
style={{ marginLeft: 8 }} |
|||
title="Export summary & details to PDF" |
|||
> |
|||
Export PDF |
|||
</button> |
|||
</div> |
|||
|
|||
{summary && ( |
|||
<div style={{ marginTop: "15px", border: "1px solid #ccc", padding: "10px" }}> |
|||
<p> |
|||
<strong>Zone:</strong> {summary.zone} |
|||
</p> |
|||
<p> |
|||
<strong>Ward:</strong> {summary.ward} |
|||
</p> |
|||
<p> |
|||
<strong>Total Shops:</strong> {summary.totalCount} |
|||
</p> |
|||
<p> |
|||
<strong>With Attachment:</strong> ✅ {summary.attachedCount} |
|||
</p> |
|||
<p> |
|||
<strong>Without Attachment:</strong> ❌ {summary.withoutAttachmentCount} |
|||
</p> |
|||
</div> |
|||
)} |
|||
|
|||
{features.length > 0 && ( |
|||
<div style={{ marginTop: "15px" }}> |
|||
<h4>Shopping Complex Attachment Status:</h4> |
|||
<div style={{ maxHeight: "400px", overflow: "auto" }}> |
|||
<table style={{ borderCollapse: "collapse", width: "100%" }} border={1}> |
|||
<thead> |
|||
<tr> |
|||
<th>Sl.no</th> |
|||
<th>Zone</th> |
|||
<th>Ward</th> |
|||
<th>Complex Name</th> |
|||
<th>Complex No</th> |
|||
<th>Road Name</th> |
|||
<th>Address</th> |
|||
<th>Category</th> |
|||
<th>Attachment Count</th> |
|||
<th>Attachment Status</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
{features.map((f, index) => ( |
|||
<tr key={f.objectid}> |
|||
<td>{index + 1}</td> |
|||
<td>{f.zone_no}</td> |
|||
<td>{f.ward_no}</td> |
|||
<td>{f.shopping_complex_name}</td> |
|||
<td>{f.shopping_complex_no}</td> |
|||
<td>{f.road_name}</td> |
|||
<td>{f.address}</td> |
|||
<td>{f.category}</td> |
|||
<td>{f.attachmentCount}</td> |
|||
<td>{f.attachmentStatus}</td> |
|||
</tr> |
|||
))} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
)} |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default ShoppingComplexAttachmentStatus; |
|||
@ -0,0 +1,8 @@ |
|||
import { React, AllWidgetProps } from 'jimu-core'; |
|||
import ZoneAbstractWidget from '../index'; |
|||
|
|||
const Widget = (props: AllWidgetProps<any>) => { |
|||
return <ZoneAbstractWidget {...props} />; |
|||
}; |
|||
|
|||
export default Widget; |
|||
@ -0,0 +1,24 @@ |
|||
.widget-container { |
|||
padding: 15px; |
|||
background: white; |
|||
border-radius: 8px; |
|||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); |
|||
} |
|||
|
|||
h3 { |
|||
font-size: 18px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.export-button { |
|||
margin-top: 10px; |
|||
background-color: #0078d4; |
|||
color: white; |
|||
} |
|||
|
|||
.table-container { |
|||
max-height: 400px; |
|||
overflow-y: auto; |
|||
border: 1px solid #ddd; |
|||
} |
|||
|
|||
@ -0,0 +1,14 @@ |
|||
import { React } from 'jimu-core' |
|||
import _Widget from '../src/runtime/widget' |
|||
import { widgetRender, wrapWidget } from 'jimu-for-test' |
|||
|
|||
const render = widgetRender() |
|||
describe('test simple widget', () => { |
|||
it('simple test', () => { |
|||
const Widget = wrapWidget(_Widget, { |
|||
config: { exampleConfigProperty: 'a' } |
|||
}) |
|||
const { queryByText } = render(<Widget widgetId="Widget_1" />) |
|||
expect(queryByText('exampleConfigProperty: a').tagName).toBe('P') |
|||
}) |
|||
}) |
|||
@ -0,0 +1,5 @@ |
|||
{ |
|||
"exampleConfigProperty": "test", |
|||
"useMapWidget": true |
|||
|
|||
} |
|||
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue