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