|
|
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;
|