Web Developer Work - I
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1718 lines
31 KiB

import React, { useEffect, useState } from "react";
import FeatureLayer from "@arcgis/core/layers/FeatureLayer";
import {
Box,
Button,
MenuItem,
Paper,
Select,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography
} from "@mui/material";
import {
Download,
FileText,
RefreshCcw,
BarChart3
} from "lucide-react";
import * as XLSX from "xlsx";
import jsPDF from "jspdf";
import "jspdf-autotable";
import "./styles.css";
/* =========================================================
STATUS CONFIGURATION
========================================================= */
const STATUS_MAP: Record<string, string> = {
"01": "Active",
"02": "Inactive"
};
/* =========================================================
LAYER CONFIGURATION
========================================================= */
const LAYER_CONFIG: Record<string, any> = {
"Disposal Location": {
url:
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/WaterInundationLayers/FeatureServer/3",
zoneField: "zone",
statusField: "status"
},
"NDRF": {
url:
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/WaterInundationLayers/FeatureServer/7",
zoneField: "zone_no"
},
"Police Station": {
url:
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/WaterInundationLayers/FeatureServer/8",
zoneField: "zone_no"
},
"Fire Station": {
url:
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/WaterInundationLayers/FeatureServer/4",
zoneField: "zone_no"
},
"CMWSSB": {
url:
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/WaterInundationLayers/FeatureServer/2",
zoneField: "zone_no"
},
"Amma Unavagam": {
url:
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/WaterInundationLayers/FeatureServer/0",
zoneField: "zone_no"
},
"SWD": {
url:
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/SWD_OLD/FeatureServer/0",
zoneField: "zone",
statusField: "operational_status"
},
"Motor Location": {
url:
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/WaterInundationLayers/FeatureServer/6",
zoneField: "zone",
statusField: "operational_status"
},
"Boat Location": {
url:
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/WaterInundationLayers/FeatureServer/1",
zoneField: "zone",
statusField: "operational_status"
},
"Cooking Center": {
url:
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/WaterInundationLayers/FeatureServer/5",
zoneField: "zone_no",
statusField: "status"
},
"Relief Center": {
url:
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/WaterInundationLayers/FeatureServer/9",
zoneField: "zone",
statusField: "status"
},
"Water Inundation Point": {
url:
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/VULNERABLE_INUNDATION/FeatureServer/1",
zoneField: "zone",
statusField: "operational_status"
},
"Water Inundation Line": {
url:
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/WaterInundationLayers/FeatureServer/12",
zoneField: "zone",
statusField: "operational_status"
},
"Vulnerable Point": {
url:
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/VULNERABLE_INUNDATION/FeatureServer/0",
zoneField: "new_zone",
statusField: "operational_status"
},
"Vulnerable Line": {
url:
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/WaterInundationLayers/FeatureServer/11",
zoneField: "new_zone",
statusField: "operational_status"
},
"Sump": {
url:
"https://gisgcc.chennaicorporation.gov.in/server/rest/services/GCCDepts/Sump_and_Pump_Location/FeatureServer/0",
zoneField: "zone",
statusField: "operational_status"
}
};
const LAYERS = Object.keys(LAYER_CONFIG);
/* =========================================================
TYPES
========================================================= */
interface LayerStatus {
Active: number;
Inactive: number;
Blank: number;
Total: number;
}
interface ZoneSummary {
zone: string;
layers: Record<string, LayerStatus>;
active: number;
inactive: number;
blank: number;
total: number;
}
/* =========================================================
EMPTY LAYER
========================================================= */
const emptyLayerStatus = (): LayerStatus => ({
Active: 0,
Inactive: 0,
Blank: 0,
Total: 0
});
/* =========================================================
EMPTY ZONE
========================================================= */
const createZone = (zone: string): ZoneSummary => {
const layers: Record<string, LayerStatus> = {};
LAYERS.forEach(layer => {
layers[layer] = emptyLayerStatus();
});
return {
zone,
layers,
active: 0,
inactive: 0,
blank: 0,
total: 0
};
};
/* =========================================================
COMPONENT
========================================================= */
const ZoneSummaryWidget = () => {
const [zoneOptions, setZoneOptions] =
useState<string[]>([]);
const [zoneFilter, setZoneFilter] =
useState<string>("All");
const [filteredData, setFilteredData] =
useState<ZoneSummary[]>([]);
const [loading, setLoading] =
useState(false);
const [overall, setOverall] = useState({
totalZones: 0,
totalAssets: 0,
active: 0,
inactive: 0,
blank: 0,
layerWise: {} as Record<
string,
LayerStatus
>
});
/* =========================================================
LOAD ZONES
========================================================= */
useEffect(() => {
loadZones();
}, []);
const loadZones = async () => {
const zoneSet = new Set<string>();
for (const layerName of LAYERS) {
try {
const config =
LAYER_CONFIG[layerName];
const layer =
new FeatureLayer({
url: config.url
});
const query =
layer.createQuery();
query.where = "1=1";
query.outFields = [
config.zoneField
];
query.returnGeometry = false;
query.returnDistinctValues = true;
const result =
await layer.queryFeatures(query);
result.features.forEach(
feature => {
const value =
feature.attributes[
config.zoneField
];
if (
value !== null &&
value !== undefined &&
`${value}`.trim() !== ""
) {
zoneSet.add(
`${value}`
);
}
}
);
} catch (error) {
console.error(
`Zone loading error - ${layerName}`,
error
);
}
}
const zones =
Array.from(zoneSet).sort(
(a, b) =>
Number(a) - Number(b)
);
setZoneOptions(zones);
};
/* =========================================================
BUILD WHERE
========================================================= */
const getWhere = (
layerName: string
) => {
const config =
LAYER_CONFIG[layerName];
if (
zoneFilter === "All" ||
!zoneFilter
) {
return "1=1";
}
return `${config.zoneField} = '${zoneFilter.replace(
/'/g,
"''"
)}'`;
};
/* =========================================================
FETCH SUMMARY
========================================================= */
const fetchSummary = async () => {
setLoading(true);
try {
const zoneData:
Record<string, ZoneSummary> = {};
const createIfMissing = (
zone: string
) => {
if (!zoneData[zone]) {
zoneData[zone] =
createZone(zone);
}
};
/* -----------------------------------------------------
PROCESS EACH LAYER
----------------------------------------------------- */
for (
const layerName of LAYERS
) {
const config =
LAYER_CONFIG[layerName];
try {
const layer =
new FeatureLayer({
url: config.url
});
const query =
layer.createQuery();
query.where =
getWhere(layerName);
query.returnGeometry =
false;
/* =================================================
STATUS LAYER
================================================= */
if (config.statusField) {
query.outFields = [
config.zoneField,
config.statusField
];
query.outStatistics = [
{
statisticType: "count",
onStatisticField: "OBJECTID",
outStatisticFieldName: "feature_count"
}
];
query.groupByFieldsForStatistics = [
config.zoneField,
config.statusField
];
const result =
await layer.queryFeatures(query);
result.features.forEach(
feature => {
const attributes =
feature.attributes;
const zoneValue =
attributes[
config.zoneField
];
if (
zoneValue === null ||
zoneValue === undefined
) {
return;
}
const zone =
`${zoneValue}`;
createIfMissing(zone);
const count =
Number(
attributes.feature_count
) || 0;
const rawStatus =
attributes[
config.statusField
];
const status =
`${rawStatus ?? ""}`.trim();
let statusName:
keyof LayerStatus =
"Blank";
/* Standard operational status */
if (
STATUS_MAP[status]
) {
statusName =
STATUS_MAP[
status
] as keyof LayerStatus;
}
/* Yes / Newly Added */
else if (
status.toLowerCase() ===
"yes" ||
status.toLowerCase() ===
"newly added"
) {
statusName =
"Active";
}
zoneData[zone]
.layers[layerName]
[statusName] += count;
zoneData[zone]
.layers[layerName]
.Total += count;
if (
statusName === "Active"
) {
zoneData[zone]
.active += count;
}
else if (
statusName === "Inactive"
) {
zoneData[zone]
.inactive += count;
}
else {
zoneData[zone]
.blank += count;
}
zoneData[zone]
.total += count;
}
);
}
/* =================================================
NORMAL TOTAL-ONLY LAYER
================================================= */
else {
query.outStatistics = [
{
statisticType: "count",
onStatisticField: "OBJECTID",
outStatisticFieldName: "feature_count"
}
];
query.groupByFieldsForStatistics = [
config.zoneField
];
const result =
await layer.queryFeatures(query);
result.features.forEach(
feature => {
const attributes =
feature.attributes;
const zoneValue =
attributes[
config.zoneField
];
if (
zoneValue === null ||
zoneValue === undefined
) {
return;
}
const zone =
`${zoneValue}`;
createIfMissing(zone);
const count =
Number(
attributes.feature_count
) || 0;
zoneData[zone]
.layers[layerName]
.Total += count;
zoneData[zone]
.total += count;
}
);
}
} catch (error) {
console.error(
`Error querying ${layerName}`,
error
);
}
}
/* =====================================================
SORT
===================================================== */
const data =
Object.values(zoneData).sort(
(a, b) =>
Number(a.zone) -
Number(b.zone)
);
/* =====================================================
OVERALL
===================================================== */
const layerWise:
Record<string, LayerStatus> = {};
LAYERS.forEach(layer => {
layerWise[layer] =
emptyLayerStatus();
});
let totalAssets = 0;
let active = 0;
let inactive = 0;
let blank = 0;
data.forEach(zone => {
totalAssets += zone.total;
active += zone.active;
inactive += zone.inactive;
blank += zone.blank;
LAYERS.forEach(layer => {
layerWise[layer].Active +=
zone.layers[layer].Active;
layerWise[layer].Inactive +=
zone.layers[layer].Inactive;
layerWise[layer].Blank +=
zone.layers[layer].Blank;
layerWise[layer].Total +=
zone.layers[layer].Total;
});
});
setFilteredData(data);
setOverall({
totalZones:
data.length,
totalAssets,
active,
inactive,
blank,
layerWise
});
} catch (error) {
console.error(
"Summary error:",
error
);
} finally {
setLoading(false);
}
};
/* =========================================================
RESET
========================================================= */
const resetDashboard = () => {
setZoneFilter("All");
setFilteredData([]);
setOverall({
totalZones: 0,
totalAssets: 0,
active: 0,
inactive: 0,
blank: 0,
layerWise: {}
});
};
/* =========================================================
EXCEL
========================================================= */
const exportExcel = () => {
if (!filteredData.length) {
return;
}
const rows: any[] = [];
const header = [
"Zone"
];
LAYERS.forEach(layer => {
header.push(
`${layer} Active`
);
header.push(
`${layer} Inactive`
);
header.push(
`${layer} Blank`
);
header.push(
`${layer} Total`
);
});
header.push(
"Active Total"
);
header.push(
"Inactive Total"
);
header.push(
"Blank Total"
);
header.push(
"Zone Total"
);
rows.push(header);
filteredData.forEach(zone => {
const row: any[] = [
zone.zone
];
LAYERS.forEach(layer => {
const data =
zone.layers[layer];
row.push(
data.Active
);
row.push(
data.Inactive
);
row.push(
data.Blank
);
row.push(
data.Total
);
});
row.push(zone.active);
row.push(zone.inactive);
row.push(zone.blank);
row.push(zone.total);
rows.push(row);
});
const worksheet =
XLSX.utils.aoa_to_sheet(rows);
worksheet["!cols"] =
header.map(() => ({
wch: 16
}));
const workbook =
XLSX.utils.book_new();
XLSX.utils.book_append_sheet(
workbook,
worksheet,
"Zone Summary"
);
XLSX.writeFile(
workbook,
`Zone_Wise_Summary_${zoneFilter}.xlsx`
);
};
/* =========================================================
PDF
========================================================= */
const exportPDF = () => {
if (!filteredData.length) {
return;
}
const doc =
new jsPDF(
"landscape",
"mm",
"a3"
);
doc.setFontSize(18);
doc.text(
"Zone-wise Operational Summary",
15,
15
);
doc.setFontSize(9);
doc.text(
`Zone: ${zoneFilter}`,
15,
22
);
const head = [
[
"Zone",
...LAYERS.flatMap(
layer => [
`${layer} A`,
`${layer} I`,
`${layer} B`,
`${layer} T`
]
),
"Active",
"Inactive",
"Blank",
"Total"
]
];
const body =
filteredData.map(
zone => [
zone.zone,
...LAYERS.flatMap(
layer => {
const data =
zone.layers[layer];
return [
data.Active,
data.Inactive,
data.Blank,
data.Total
];
}
),
zone.active,
zone.inactive,
zone.blank,
zone.total
]
);
(doc as any).autoTable({
head,
body,
startY: 28,
theme: "grid",
styles: {
fontSize: 4,
cellPadding: 1
},
headStyles: {
fontSize: 4,
fontStyle: "bold"
}
});
doc.save(
`Zone_Wise_Summary_${zoneFilter}.pdf`
);
};
/* =========================================================
RENDER
========================================================= */
return (
<div className="zone-dashboard">
{/* =====================================================
TOP HEADER
===================================================== */}
<div className="dashboard-header">
<div className="header-left">
<div className="header-icon">
<BarChart3 size={24} />
</div>
<div>
<div className="dashboard-title">
Zone-wise Summary
</div>
<div className="dashboard-subtitle">
Operational Asset Monitoring
</div>
</div>
</div>
<div className="header-layer-count">
{LAYERS.length} Layers
</div>
</div>
{/* =====================================================
FILTER BAR
===================================================== */}
<div className="filter-card">
<div className="filter-title">
Zone Selection
</div>
<Select
value={zoneFilter}
onChange={(e) =>
setZoneFilter(
e.target.value
)
}
className="modern-zone-select"
size="small"
>
<MenuItem value="All">
All Zones
</MenuItem>
{zoneOptions.map(
zone => (
<MenuItem
key={zone}
value={zone}
>
Zone {zone}
</MenuItem>
)
)}
</Select>
<Button
variant="contained"
className="modern-fetch-button"
onClick={fetchSummary}
disabled={loading}
startIcon={
<BarChart3 size={17} />
}
>
{loading
? "Loading..."
: "View Summary"}
</Button>
<Button
variant="outlined"
className="modern-reset-button"
onClick={resetDashboard}
startIcon={
<RefreshCcw size={16} />
}
>
Reset
</Button>
<div className="export-area">
<Button
variant="outlined"
className="excel-button"
onClick={exportExcel}
disabled={
filteredData.length === 0
}
startIcon={
<Download size={16} />
}
>
Excel
</Button>
<Button
variant="outlined"
className="pdf-button"
onClick={exportPDF}
disabled={
filteredData.length === 0
}
startIcon={
<FileText size={16} />
}
>
PDF
</Button>
</div>
</div>
{/* =====================================================
KPI CARDS
===================================================== */}
{filteredData.length > 0 && (
<div className="kpi-grid">
<div className="kpi-card kpi-zone">
<div className="kpi-label">
Zones
</div>
<div className="kpi-value">
{overall.totalZones}
</div>
</div>
<div className="kpi-card kpi-total">
<div className="kpi-label">
Total Assets
</div>
<div className="kpi-value">
{overall.totalAssets.toLocaleString()}
</div>
</div>
<div className="kpi-card kpi-active">
<div className="kpi-label">
Active
</div>
<div className="kpi-value">
{overall.active.toLocaleString()}
</div>
</div>
<div className="kpi-card kpi-inactive">
<div className="kpi-label">
Inactive
</div>
<div className="kpi-value">
{overall.inactive.toLocaleString()}
</div>
</div>
<div className="kpi-card kpi-blank">
<div className="kpi-label">
Blank
</div>
<div className="kpi-value">
{overall.blank.toLocaleString()}
</div>
</div>
</div>
)}
{/* =====================================================
TABLE
===================================================== */}
<div className="table-card">
<div className="table-title-row">
<div>
<div className="table-title">
Zone-wise Layer Count
</div>
<div className="table-subtitle">
Active, inactive, blank and total
feature counts
</div>
</div>
<div className="record-count">
{filteredData.length} Zones
</div>
</div>
<TableContainer
component={Paper}
className="modern-table-container"
>
<Table
stickyHeader
className="modern-table"
>
<TableHead>
<TableRow>
<TableCell className="zone-header">
Zone
</TableCell>
{LAYERS.map(
layer => (
<React.Fragment
key={layer}
>
<TableCell className="active-header">
{layer}
<span>
Active
</span>
</TableCell>
<TableCell className="inactive-header">
{layer}
<span>
Inactive
</span>
</TableCell>
<TableCell className="blank-header">
{layer}
<span>
Blank
</span>
</TableCell>
<TableCell className="total-header">
{layer}
<span>
Total
</span>
</TableCell>
</React.Fragment>
)
)}
<TableCell className="active-header">
Active
</TableCell>
<TableCell className="inactive-header">
Inactive
</TableCell>
<TableCell className="blank-header">
Blank
</TableCell>
<TableCell className="total-header">
Total
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredData.map(
zone => (
<TableRow
key={zone.zone}
className="data-row"
>
<TableCell className="zone-value">
<span className="zone-pill">
{zone.zone}
</span>
</TableCell>
{LAYERS.map(
layer => {
const data =
zone.layers[layer];
return (
<React.Fragment
key={layer}
>
<TableCell className="active-cell">
{data.Active}
</TableCell>
<TableCell className="inactive-cell">
{data.Inactive}
</TableCell>
<TableCell className="blank-cell">
{data.Blank}
</TableCell>
<TableCell className="total-cell">
{data.Total}
</TableCell>
</React.Fragment>
);
}
)}
<TableCell className="active-cell main-total">
{zone.active}
</TableCell>
<TableCell className="inactive-cell main-total">
{zone.inactive}
</TableCell>
<TableCell className="blank-cell main-total">
{zone.blank}
</TableCell>
<TableCell className="total-cell main-total">
{zone.total}
</TableCell>
</TableRow>
)
)}
{filteredData.length === 0 && (
<TableRow>
<TableCell
colSpan={
1 +
LAYERS.length * 4 +
4
}
className="empty-table"
>
<div className="empty-icon">
<BarChart3 size={32} />
</div>
<div>
Select a zone and click
<strong>
{" "}View Summary
</strong>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</div>
{/* =====================================================
OVERALL SUMMARY
===================================================== */}
{filteredData.length > 0 && (
<div className="overall-card">
<div className="overall-header">
<div>
<div className="overall-title">
Overall Summary
</div>
<div className="overall-subtitle">
Consolidated layer-wise operational status
</div>
</div>
</div>
<div className="layer-summary-grid">
{LAYERS.map(
layer => {
const data =
overall.layerWise[
layer
] || emptyLayerStatus();
return (
<div
className="layer-summary-card"
key={layer}
>
<div className="layer-card-title">
{layer}
</div>
<div className="layer-total">
{data.Total.toLocaleString()}
</div>
{LAYER_CONFIG[
layer
].statusField && (
<div className="status-mini-row">
<span className="mini-active">
Active
<b>
{data.Active}
</b>
</span>
<span className="mini-inactive">
Inactive
<b>
{data.Inactive}
</b>
</span>
<span className="mini-blank">
Blank
<b>
{data.Blank}
</b>
</span>
</div>
)}
{!LAYER_CONFIG[
layer
].statusField && (
<div className="total-only-label">
Total Features
</div>
)}
</div>
);
}
)}
</div>
</div>
)}
</div>
);
};
export default ZoneSummaryWidget;