import React, { useState, useEffect, memo, useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import ReactPaginate from "react-paginate";
import { ChevronDown, MoreVertical, Plus, Trash } from "react-feather";
import DataTable from "react-data-table-component";
import {
Card,
CardHeader,
CardTitle,
Input,
Label,
Row,
Col,
Button,
Modal,
ModalHeader,
ModalBody,
ModalFooter,
UncontrolledDropdown,
DropdownToggle,
DropdownMenu,
DropdownItem,
FormGroup,
Form,
} from "reactstrap";
import Select from "react-select";
import { Edit } from "@mui/icons-material";
import { useSnackbar } from "notistack";
import { default as SweetAlert } from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import {
getDataCenters,
createDataCenter,
updateDataCenter,
deleteDataCenter,
getEmissionScopes,
} from "../redux/actions/dataCenter";
import { getAreas, getAreasWithCriteria } from "../redux/actions/areas";
import { useTranslation } from "react-i18next";
import {
getSectors,
getSectorById,
getSubSectorById,
getConsuptionUnits,
} from "../redux/actions/datas";
import { getAllEmissionSources } from "../redux/actions/emissionSources";
import { permissionCheck } from "../components/permission-check";
import { customFilterForSelect } from "../utility/Utils";
import { MapContainer, TileLayer, Marker, useMapEvents } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
import axios from "axios";
import { debounce } from "lodash";
// Add Nominatim service configuration
const NOMINATIM_BASE_URL = "https://nominatim.openstreetmap.org";
const nominatimAxios = axios.create({
baseURL: NOMINATIM_BASE_URL,
headers: {
"User-Agent": "SGE-DataCenter-Management", // Required by Nominatim's usage policy
},
});
const Swal = withReactContent(SweetAlert);
// Fix Leaflet marker icon issue
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: require("leaflet/dist/images/marker-icon-2x.png"),
iconUrl: require("leaflet/dist/images/marker-icon.png"),
shadowUrl: require("leaflet/dist/images/marker-shadow.png"),
});
// Map marker component that handles clicks
const MapMarker = ({ position, setPosition, setSelectedDataCenter }) => {
useMapEvents({
click(e) {
setPosition([e.latlng.lat, e.latlng.lng]);
// Use Nominatim reverse geocoding directly
nominatimAxios
.get(`/reverse?format=json&lat=${e.latlng.lat}&lon=${e.latlng.lng}`)
.then((response) => {
const address = response.data.display_name;
setSelectedDataCenter((prev) => ({
...prev,
address,
latitude: e.latlng.lat,
longitude: e.latlng.lng,
}));
})
.catch((error) => {
console.error("Error getting address:", error);
// Still update coordinates even if address lookup fails
setSelectedDataCenter((prev) => ({
...prev,
latitude: e.latlng.lat,
longitude: e.latlng.lng,
}));
});
},
});
// Only render marker if position exists and has valid coordinates
return position && position[0] && position[1] ? (
) : null;
};
const DataCenterManagement = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const { enqueueSnackbar } = useSnackbar();
const [currentPage, setCurrentPage] = useState(1);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [searchValue, setSearchValue] = useState("");
const [showAddModal, setShowAddModal] = useState(false);
const [selectedDataCenter, setSelectedDataCenter] = useState({
name: "",
externalId: "",
number: "",
address: "",
areaId: null,
cityId: null,
latitude: null,
longitude: null,
ayposURL: "",
city: "",
emissionScopeId: null,
sectorId: null,
subSectorId: null,
dataCenterEmissionSources: [], // Array of emission sources with consumption units
activitySubUnitId: null,
});
const [mapPosition, setMapPosition] = useState(null);
const dataCenterStore = useSelector((state) => {
console.log("DataCenter Store:", state.dataCenter);
return state.dataCenter;
});
const emissionScopeStore = useSelector((state) => state.emissionScope);
const datasStore = useSelector((state) => state.datas);
const emissionSourceStore = useSelector((state) => state.emissionSources);
const areasStore = useSelector((state) => state.areas);
const [sectorsOptions, setSectorsOptions] = useState([]);
const [subSectorsOptions, setSubSectorsOptions] = useState([]);
const [emissionSourcesOptions, setEmissionSourcesOptions] = useState([]);
const [consuptionUnitsOptions, setConsuptionUnitsOptions] = useState([]);
const [activitySubUnitsOptions, setActivitySubUnitsOptions] = useState([]);
const [emissionScopesOptions, setEmissionScopesOptions] = useState([]);
const [areasOptions, setAreasOptions] = useState([]);
const [citiesOptions, setCitiesOptions] = useState([]);
// Add state for selected sector and sub sector like in data input
const [selectedSector, setSelectedSector] = useState(null);
const [selectedSubSector, setSelectedSubSector] = useState(null);
const [editingDataCenter, setEditingDataCenter] = useState(null);
const initialColumns = [
{
name: t("DataCenter.id"),
selector: (row) => row.externalId,
sortable: true,
minWidth: "100px",
cell: (row) => {row.externalId},
},
{
name: t("DataCenter.name"),
selector: (row) => row.dataCenter,
sortable: true,
minWidth: "200px",
},
{
name: "Dashboard",
selector: (row) => row.ayposURL,
sortable: false,
minWidth: "150px",
cell: (row) => (
{row.ayposURL ? (
) : (
-
)}
),
},
{
name: t("Actions"),
allowOverflow: true,
width: "150px",
center: true,
cell: (row) => {
return (
{permissionCheck("data_center_update") && (
handleEditDataCenter(row)}
>
{t("Cruds.edit")}
)}
{permissionCheck("data_center_delete") && (
handleDeleteDataCenter(row)}
>
{t("Cruds.delete")}
)}
);
},
},
];
const [serverSideColumns, setServerSideColumns] = useState(initialColumns);
useEffect(() => {
dispatch(getDataCenters());
dispatch(getSectors());
dispatch(getAreas());
dispatch(getEmissionScopes());
}, [dispatch]);
useEffect(() => {
setSectorsOptions(
datasStore?.sectors?.map((sector) => ({
value: sector?.id,
label: sector?.tag,
}))
);
}, [datasStore?.sectors]);
useEffect(() => {
setSubSectorsOptions([]);
setSubSectorsOptions(
datasStore?.sector?.subSectors?.map((subSector) => ({
value: subSector?.id,
label: subSector?.tag,
}))
);
}, [datasStore?.sector]);
useEffect(() => {
setActivitySubUnitsOptions(
datasStore?.subSector?.activitySubUnits?.map((activitySubUnit) => ({
value: activitySubUnit?.id,
label: activitySubUnit?.tag,
}))
);
}, [datasStore?.subSector]);
useEffect(() => {
setEmissionSourcesOptions(
emissionSourceStore?.emissionSources
?.filter((source) => source.convertUnitCheck != false)
?.map((source) => ({
value: source?.id,
label: source?.tag,
}))
);
}, [emissionSourceStore?.emissionSources]);
// Remove the old emission source effect since we now handle it in the emission sources component
// useEffect(() => {
// if (selectedDataCenter?.emissionSourceId) {
// dispatch(
// getConsuptionUnits({
// id: selectedDataCenter?.emissionSourceId,
// sector: selectedDataCenter?.sectorId,
// })
// );
// }
// }, [selectedDataCenter?.emissionSourceId]);
// Ensure there's always at least one emission source entry for new data centers
useEffect(() => {
if (
showAddModal &&
!editingDataCenter &&
selectedDataCenter.dataCenterEmissionSources.length === 0
) {
setSelectedDataCenter((prev) => ({
...prev,
dataCenterEmissionSources: [
{
emissionSourceId: null,
consuptionUnitId: null,
isDefault: true,
},
],
}));
}
}, [
showAddModal,
editingDataCenter,
selectedDataCenter.dataCenterEmissionSources.length,
]);
useEffect(() => {
if (selectedSubSector != null) {
dispatch(getAllEmissionSources(selectedSubSector));
}
}, [selectedSubSector]);
useEffect(() => {
if (selectedSector != null) {
dispatch(getSectorById(selectedSector));
}
}, [selectedSector]);
useEffect(() => {
if (selectedSubSector != null) {
dispatch(getSubSectorById(selectedSubSector));
}
}, [selectedSubSector]);
useEffect(() => {
setConsuptionUnitsOptions(
datasStore?.consuptionUnits?.map((consuptionUnit) => ({
value: consuptionUnit?.unit?.id,
label: consuptionUnit?.unit?.description,
}))
);
}, [datasStore?.consuptionUnits]);
useEffect(() => {
if (emissionScopeStore?.emissionScopes) {
setEmissionScopesOptions(
emissionScopeStore.emissionScopes.map((scope) => ({
value: scope.id,
label: scope.tag,
}))
);
}
}, [emissionScopeStore?.emissionScopes]);
// Set areas options when areas data is loaded
useEffect(() => {
if (areasStore?.areas) {
setAreasOptions(
areasStore.areas.map((area) => ({
value: area.id,
label: area.tag,
}))
);
}
}, [areasStore?.areas]);
// Set cities options when selected area changes
useEffect(() => {
if (selectedDataCenter.areaId && areasStore?.areas) {
const selectedArea = areasStore.areas.find(
(area) => area.id === selectedDataCenter.areaId
);
if (selectedArea?.cities) {
setCitiesOptions(
selectedArea.cities.map((city) => ({
value: city.id,
label: city.name,
}))
);
} else {
setCitiesOptions([]);
}
} else {
setCitiesOptions([]);
}
}, [selectedDataCenter.areaId, areasStore?.areas]);
const handleEditDataCenter = (row) => {
console.log("Editing data center:", row);
setEditingDataCenter(row);
// Convert dataCenterEmissionSources to the format expected by the form
const emissionSources = row.dataCenterEmissionSources
? row.dataCenterEmissionSources.map((dces) => ({
emissionSourceId: dces.emissionSource?.id,
consuptionUnitId: dces.consuptionUnit?.id,
isDefault: dces.isDefault || false,
}))
: [];
setSelectedDataCenter({
name: row.dataCenter,
externalId: row.externalId?.toString(),
number: row.number?.toString(),
address: row.address || "",
areaId: row.area?.id || null,
cityId: row.city?.id || null,
latitude: row.latitude,
longitude: row.longitude,
ayposURL: row.ayposURL || "",
city: row.city?.name || "",
emissionScopeId: row.emissionScope?.id || null,
sectorId: row.sector?.id || null,
subSectorId: row.subSector?.id || null,
dataCenterEmissionSources: emissionSources,
activitySubUnitId: row.activitySubUnit?.id || null,
});
// Set the selected sector and sub sector for cascading dropdowns
setSelectedSector(row.sector?.id);
setSelectedSubSector(row.subSector?.id);
// If there are existing emission sources, fetch consumption units for each
if (
row.dataCenterEmissionSources &&
row.dataCenterEmissionSources.length > 0
) {
row.dataCenterEmissionSources.forEach((dces) => {
if (dces.emissionSource && dces.emissionSource.id) {
dispatch(
getConsuptionUnits({
id: dces.emissionSource.id,
sector: row.sector?.id,
})
);
}
});
}
// Only set map position if we have both address and valid coordinates
setMapPosition(
row.address && row.latitude && row.longitude
? [row.latitude, row.longitude]
: null
);
setShowAddModal(true);
};
const handleDeleteDataCenter = async (row) => {
const result = await Swal.fire({
title: t("Common.areYouSure"),
text: t("Common.cantRevert"),
icon: "warning",
showCancelButton: true,
confirmButtonText: t("Common.yes"),
cancelButtonText: t("Common.no"),
customClass: {
confirmButton: "btn btn-primary",
cancelButton: "btn btn-outline-danger ms-1",
},
buttonsStyling: false,
});
if (result.value) {
try {
await dispatch(deleteDataCenter(row.id));
enqueueSnackbar(t("DataCenter.deleteSuccess"), { variant: "success" });
} catch (error) {
console.error("Delete error:", error);
enqueueSnackbar(error?.message || t("DataCenter.deleteError"), {
variant: "error",
});
}
}
};
const validateForm = () => {
const errors = [];
// Required fields validation
if (!selectedDataCenter.name?.trim()) {
errors.push(t("DataCenter.nameRequired"));
}
if (!selectedDataCenter.externalId?.trim()) {
errors.push(t("DataCenter.externalIdRequired"));
}
if (!selectedDataCenter.sectorId) {
errors.push(t("DataCenter.sectorRequired"));
}
// Number validations
try {
if (selectedDataCenter.externalId) {
const externalId = parseInt(selectedDataCenter.externalId);
if (isNaN(externalId) || externalId < 0) {
errors.push(t("DataCenter.externalIdMustBePositiveNumber"));
}
}
if (selectedDataCenter.number) {
const number = parseInt(selectedDataCenter.number);
if (isNaN(number) || number < 1) {
errors.push(t("DataCenter.numberMustBePositive"));
}
}
} catch (e) {
errors.push(t("DataCenter.invalidNumberFormat"));
}
// Coordinate validations
if (selectedDataCenter.latitude || selectedDataCenter.longitude) {
try {
const lat = parseFloat(selectedDataCenter.latitude);
const lng = parseFloat(selectedDataCenter.longitude);
if (isNaN(lat) || lat < -90 || lat > 90) {
errors.push(t("DataCenter.invalidLatitude"));
}
if (isNaN(lng) || lng < -180 || lng > 180) {
errors.push(t("DataCenter.invalidLongitude"));
}
} catch (e) {
errors.push(t("DataCenter.invalidCoordinates"));
}
}
// URL validation
if (selectedDataCenter.ayposURL?.trim()) {
try {
new URL(selectedDataCenter.ayposURL);
} catch (e) {
errors.push(t("DataCenter.invalidURL"));
}
}
// Relationship validations
if (selectedDataCenter.subSectorId && !selectedDataCenter.sectorId) {
errors.push(t("DataCenter.sectorRequired"));
}
if (
selectedDataCenter.activitySubUnitId &&
!selectedDataCenter.subSectorId
) {
errors.push(t("DataCenter.subSectorRequiredForActivity"));
}
// Emission sources validations
if (selectedDataCenter.dataCenterEmissionSources.length > 0) {
const validSources = selectedDataCenter.dataCenterEmissionSources.filter(
(source) => source.emissionSourceId && source.consuptionUnitId
);
if (validSources.length === 0) {
errors.push(t("DataCenter.atLeastOneEmissionSource"));
}
// Check for incomplete emission sources
selectedDataCenter.dataCenterEmissionSources.forEach((source, index) => {
if (
(source.emissionSourceId && !source.consuptionUnitId) ||
(!source.emissionSourceId && source.consuptionUnitId)
) {
errors.push(
t("DataCenter.incompleteEmissionSource", { index: index + 1 })
);
}
});
// Check for duplicate emission sources
const sourceIds = validSources.map((s) => s.emissionSourceId);
const duplicates = sourceIds.filter(
(id, index) => sourceIds.indexOf(id) !== index
);
if (duplicates.length > 0) {
errors.push(t("DataCenter.duplicateEmissionSources"));
}
// Ensure exactly one default emission source
const defaultSources = validSources.filter((s) => s.isDefault);
if (defaultSources.length === 0) {
errors.push(t("DataCenter.oneDefaultEmissionSourceRequired"));
} else if (defaultSources.length > 1) {
errors.push(t("DataCenter.onlyOneDefaultEmissionSourceAllowed"));
}
}
return errors;
};
const handleSubmit = async () => {
const validationErrors = validateForm();
if (validationErrors.length > 0) {
validationErrors.forEach((error) => {
enqueueSnackbar(error, { variant: "error" });
});
return;
}
try {
// Format data according to GraphQL input type
const dataToSubmit = {
name: selectedDataCenter.name,
externalId: parseInt(selectedDataCenter.externalId),
number: parseInt(selectedDataCenter.number || "1"),
address: selectedDataCenter.address,
areaId: selectedDataCenter.areaId,
cityId: selectedDataCenter.cityId,
latitude: selectedDataCenter.latitude
? parseFloat(selectedDataCenter.latitude)
: null,
longitude: selectedDataCenter.longitude
? parseFloat(selectedDataCenter.longitude)
: null,
ayposURL: selectedDataCenter.ayposURL,
emissionScopeId: selectedDataCenter.emissionScopeId,
sectorId: selectedDataCenter.sectorId,
subSectorId: selectedDataCenter.subSectorId,
dataCenterEmissionSources:
selectedDataCenter.dataCenterEmissionSources.filter(
(source) => source.emissionSourceId && source.consuptionUnitId
),
activitySubUnitId: selectedDataCenter.activitySubUnitId,
};
if (editingDataCenter) {
// Update existing data center
await dispatch(updateDataCenter(editingDataCenter.id, dataToSubmit));
enqueueSnackbar(t("DataCenter.updateSuccess"), { variant: "success" });
} else {
// Create new data center
await dispatch(createDataCenter(dataToSubmit));
enqueueSnackbar(t("DataCenter.createSuccess"), { variant: "success" });
}
// Refresh the data centers list
await dispatch(getDataCenters());
handleCloseModal();
} catch (error) {
console.error("Submit error:", error);
// Handle specific error cases
if (error.message?.includes("duplicate")) {
enqueueSnackbar(t("DataCenter.duplicateExternalId"), {
variant: "error",
});
} else if (error.message?.includes("permission")) {
enqueueSnackbar(t("Common.noPermission"), { variant: "error" });
} else if (error.message?.includes("not found")) {
enqueueSnackbar(t("DataCenter.resourceNotFound"), { variant: "error" });
} else {
enqueueSnackbar(error?.message || t("DataCenter.submitError"), {
variant: "error",
});
}
}
};
const handleCloseModal = () => {
setShowAddModal(false);
setSelectedDataCenter({
name: "",
externalId: "",
number: "",
address: "",
areaId: null,
cityId: null,
latitude: null,
longitude: null,
ayposURL: "",
city: "",
emissionScopeId: null,
sectorId: null,
subSectorId: null,
dataCenterEmissionSources: [
{
emissionSourceId: null,
consuptionUnitId: null,
isDefault: true,
},
],
activitySubUnitId: null,
});
setMapPosition(null);
setEditingDataCenter(null);
setSelectedSector(null);
setSelectedSubSector(null);
};
const handleFilter = (e) => {
setSearchValue(e.target.value);
};
const CustomPagination = () => (
setCurrentPage(page.selected + 1)}
pageCount={Math.ceil(dataCenterStore.total / rowsPerPage)}
breakLabel="..."
pageRangeDisplayed={2}
marginPagesDisplayed={2}
activeClassName="active"
pageClassName="page-item"
breakClassName="page-item"
nextLinkClassName="page-link"
pageLinkClassName="page-link"
breakLinkClassName="page-link"
previousLinkClassName="page-link"
nextClassName="page-item next-item"
previousClassName="page-item prev-item"
containerClassName="pagination react-paginate separated-pagination pagination-sm justify-content-end pe-1 mt-1"
/>
);
// Add geocoding function
const geocodeAddress = useCallback(async (address) => {
if (!address || !address.trim()) {
setMapPosition(null);
setSelectedDataCenter((prev) => ({
...prev,
latitude: null,
longitude: null,
}));
return false;
}
try {
const response = await nominatimAxios.get(
`/search?format=json&q=${encodeURIComponent(address)}`
);
if (response.data && response.data[0]) {
const { lat, lon } = response.data[0];
const newPosition = [parseFloat(lat), parseFloat(lon)];
setMapPosition(newPosition);
setSelectedDataCenter((prev) => ({
...prev,
latitude: parseFloat(lat),
longitude: parseFloat(lon),
}));
return true;
}
// If no results found, clear the coordinates
setMapPosition(null);
setSelectedDataCenter((prev) => ({
...prev,
latitude: null,
longitude: null,
}));
return false;
} catch (error) {
console.error("Error geocoding address:", error);
// On error, clear the coordinates
setMapPosition(null);
setSelectedDataCenter((prev) => ({
...prev,
latitude: null,
longitude: null,
}));
return false;
}
}, []);
// Update the address input handler
const handleAddressChange = (e) => {
const newAddress = e.target.value;
setSelectedDataCenter((prev) => ({
...prev,
address: newAddress,
}));
// If address is empty, clear the coordinates
if (!newAddress.trim()) {
setMapPosition(null);
setSelectedDataCenter((prev) => ({
...prev,
latitude: null,
longitude: null,
}));
} else {
// If address is not empty, try to geocode it after a delay
debouncedGeocode(newAddress);
}
};
// Debounced version of geocoding
const debouncedGeocode = useCallback(
debounce((address) => geocodeAddress(address), 1000),
[geocodeAddress]
);
const renderModal = () => {
return (
{editingDataCenter ? t("DataCenter.edit") : t("DataCenter.add")}
);
};
return (
{t("DataCenter.title")}
{permissionCheck("data_center_create") && (
)}
setRowsPerPage(Number(e.target.value))}
>
}
paginationDefaultPage={currentPage}
paginationComponent={CustomPagination}
data={dataCenterStore?.dataCenters || []}
progressPending={dataCenterStore?.loading}
progressComponent={
Loading...
}
noDataComponent={
{t("Common.noDataAvailable")}
}
/>
{renderModal()}
);
};
export default memo(DataCenterManagement);