forked from BLC/sgeUpdated
1431 lines
50 KiB
JavaScript
1431 lines
50 KiB
JavaScript
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] ? (
|
|
<Marker position={position} />
|
|
) : 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) => <span>{row.externalId}</span>,
|
|
},
|
|
{
|
|
name: t("DataCenter.name"),
|
|
selector: (row) => row.dataCenter,
|
|
sortable: true,
|
|
minWidth: "200px",
|
|
},
|
|
{
|
|
name: "Dashboard",
|
|
selector: (row) => row.ayposURL,
|
|
sortable: false,
|
|
minWidth: "150px",
|
|
cell: (row) => (
|
|
<div className="d-flex justify-content-center w-100">
|
|
{row.ayposURL ? (
|
|
<Button
|
|
color="primary"
|
|
size="sm"
|
|
onClick={() => window.open(row.ayposURL, "_blank")}
|
|
>
|
|
Dashboard
|
|
</Button>
|
|
) : (
|
|
<span>-</span>
|
|
)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
name: t("Actions"),
|
|
allowOverflow: true,
|
|
width: "150px",
|
|
center: true,
|
|
cell: (row) => {
|
|
return (
|
|
<div className="d-flex">
|
|
<UncontrolledDropdown>
|
|
<DropdownToggle className="pl-1" tag="span">
|
|
<MoreVertical size={15} />
|
|
</DropdownToggle>
|
|
<DropdownMenu container={"body"} end>
|
|
{permissionCheck("data_center_update") && (
|
|
<DropdownItem
|
|
tag="a"
|
|
className="w-100"
|
|
onClick={() => handleEditDataCenter(row)}
|
|
>
|
|
<Edit size={15} />
|
|
<span className="align-middle ml-50">
|
|
{t("Cruds.edit")}
|
|
</span>
|
|
</DropdownItem>
|
|
)}
|
|
{permissionCheck("data_center_delete") && (
|
|
<DropdownItem
|
|
tag="a"
|
|
className="w-100"
|
|
onClick={() => handleDeleteDataCenter(row)}
|
|
>
|
|
<Trash size={15} />
|
|
<span className="align-middle ml-50">
|
|
{t("Cruds.delete")}
|
|
</span>
|
|
</DropdownItem>
|
|
)}
|
|
</DropdownMenu>
|
|
</UncontrolledDropdown>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
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 = () => (
|
|
<ReactPaginate
|
|
previousLabel=""
|
|
nextLabel=""
|
|
forcePage={currentPage - 1}
|
|
onPageChange={(page) => 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 (
|
|
<Modal
|
|
isOpen={showAddModal}
|
|
toggle={handleCloseModal}
|
|
className="modal-dialog-centered"
|
|
size="lg"
|
|
>
|
|
<ModalHeader toggle={handleCloseModal}>
|
|
{editingDataCenter ? t("DataCenter.edit") : t("DataCenter.add")}
|
|
</ModalHeader>
|
|
<ModalBody>
|
|
<Form>
|
|
<Row>
|
|
<Col sm="12">
|
|
<FormGroup>
|
|
<Label for="name">
|
|
{t("DataCenter.name")}{" "}
|
|
<span className="text-danger">*</span>
|
|
</Label>
|
|
<Input
|
|
type="text"
|
|
name="name"
|
|
id="name"
|
|
placeholder={t("DataCenter.name")}
|
|
value={selectedDataCenter.name}
|
|
onChange={(e) =>
|
|
setSelectedDataCenter({
|
|
...selectedDataCenter,
|
|
name: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</FormGroup>
|
|
</Col>
|
|
<Col sm="6">
|
|
<FormGroup>
|
|
<Label for="externalId">
|
|
{t("DataCenter.externalId")}{" "}
|
|
<span className="text-danger">*</span>
|
|
</Label>
|
|
<Input
|
|
type="text"
|
|
name="externalId"
|
|
id="externalId"
|
|
placeholder={t("DataCenter.externalId")}
|
|
value={selectedDataCenter.externalId}
|
|
onChange={(e) =>
|
|
setSelectedDataCenter({
|
|
...selectedDataCenter,
|
|
externalId: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</FormGroup>
|
|
</Col>
|
|
<Col sm="6">
|
|
<FormGroup>
|
|
<Label for="ayposURL">{t("DataCenter.ayposURL")}</Label>
|
|
<Input
|
|
type="text"
|
|
name="ayposURL"
|
|
id="ayposURL"
|
|
placeholder={t("DataCenter.ayposURL")}
|
|
value={selectedDataCenter.ayposURL}
|
|
onChange={(e) =>
|
|
setSelectedDataCenter({
|
|
...selectedDataCenter,
|
|
ayposURL: e.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</FormGroup>
|
|
</Col>
|
|
<Col sm="6">
|
|
<FormGroup>
|
|
<Label for="area">Area</Label>
|
|
<Select
|
|
id="area"
|
|
name="area"
|
|
placeholder="Select area"
|
|
options={areasOptions}
|
|
value={areasOptions?.find(
|
|
(option) => option.value === selectedDataCenter.areaId
|
|
)}
|
|
onChange={(option) => {
|
|
setSelectedDataCenter({
|
|
...selectedDataCenter,
|
|
areaId: option?.value,
|
|
cityId: null,
|
|
city: "",
|
|
});
|
|
}}
|
|
isClearable
|
|
filterOption={customFilterForSelect}
|
|
/>
|
|
</FormGroup>
|
|
</Col>
|
|
<Col sm="6">
|
|
<FormGroup>
|
|
<Label for="city">City</Label>
|
|
<Select
|
|
id="city"
|
|
name="city"
|
|
placeholder="Select city"
|
|
options={citiesOptions}
|
|
value={citiesOptions?.find(
|
|
(option) => option.value === selectedDataCenter.cityId
|
|
)}
|
|
onChange={(option) => {
|
|
setSelectedDataCenter({
|
|
...selectedDataCenter,
|
|
cityId: option?.value,
|
|
city: option?.label || "",
|
|
});
|
|
}}
|
|
isClearable
|
|
filterOption={customFilterForSelect}
|
|
isDisabled={!selectedDataCenter.areaId}
|
|
/>
|
|
</FormGroup>
|
|
</Col>
|
|
<Col sm="12">
|
|
<FormGroup>
|
|
<Label for="address">{t("DataCenter.address")}</Label>
|
|
<div className="d-flex">
|
|
<Input
|
|
type="text"
|
|
name="address"
|
|
id="address"
|
|
placeholder={t("DataCenter.address")}
|
|
value={selectedDataCenter.address}
|
|
onChange={handleAddressChange}
|
|
/>
|
|
<Button
|
|
color="primary"
|
|
className="ml-1"
|
|
onClick={() => {
|
|
if (selectedDataCenter.address) {
|
|
geocodeAddress(selectedDataCenter.address);
|
|
}
|
|
}}
|
|
>
|
|
{t("DataCenter.findOnMap")}
|
|
</Button>
|
|
</div>
|
|
</FormGroup>
|
|
</Col>
|
|
|
|
{/* Emission Scope Section */}
|
|
<Col sm="12">
|
|
<h5 className="mt-3 mb-2 text-primary">
|
|
Emission Scope Configuration
|
|
</h5>
|
|
</Col>
|
|
<Col sm="6">
|
|
<FormGroup>
|
|
<Label for="emissionScope">Emission Scope</Label>
|
|
<Select
|
|
id="emissionScope"
|
|
name="emissionScope"
|
|
placeholder="Select emission scope"
|
|
options={emissionScopesOptions}
|
|
value={emissionScopesOptions?.find(
|
|
(option) =>
|
|
option.value === selectedDataCenter.emissionScopeId
|
|
)}
|
|
onChange={(option) =>
|
|
setSelectedDataCenter({
|
|
...selectedDataCenter,
|
|
emissionScopeId: option?.value,
|
|
})
|
|
}
|
|
isClearable
|
|
filterOption={customFilterForSelect}
|
|
/>
|
|
</FormGroup>
|
|
</Col>
|
|
<Col sm="6">
|
|
<FormGroup>
|
|
<Label for="sector">
|
|
Sector <span className="text-danger">*</span>
|
|
</Label>
|
|
<Select
|
|
id="sector"
|
|
name="sector"
|
|
placeholder="Select sector"
|
|
options={sectorsOptions}
|
|
value={sectorsOptions?.find(
|
|
(option) => option.value === selectedDataCenter.sectorId
|
|
)}
|
|
onChange={(option) => {
|
|
setSelectedSector(option?.value);
|
|
setSelectedDataCenter({
|
|
...selectedDataCenter,
|
|
sectorId: option?.value,
|
|
subSectorId: null,
|
|
dataCenterEmissionSources: [
|
|
{
|
|
emissionSourceId: null,
|
|
consuptionUnitId: null,
|
|
isDefault: true,
|
|
},
|
|
],
|
|
activitySubUnitId: null,
|
|
});
|
|
}}
|
|
isClearable
|
|
filterOption={customFilterForSelect}
|
|
/>
|
|
</FormGroup>
|
|
</Col>
|
|
<Col sm="6">
|
|
<FormGroup>
|
|
<Label for="subSector">Sub Sector</Label>
|
|
<Select
|
|
id="subSector"
|
|
name="subSector"
|
|
placeholder="Select sub sector"
|
|
options={subSectorsOptions}
|
|
value={subSectorsOptions?.find(
|
|
(option) =>
|
|
option.value === selectedDataCenter.subSectorId
|
|
)}
|
|
onChange={(option) => {
|
|
setSelectedSubSector(option?.value);
|
|
setSelectedDataCenter({
|
|
...selectedDataCenter,
|
|
subSectorId: option?.value,
|
|
dataCenterEmissionSources: [
|
|
{
|
|
emissionSourceId: null,
|
|
consuptionUnitId: null,
|
|
isDefault: true,
|
|
},
|
|
],
|
|
activitySubUnitId: null,
|
|
});
|
|
}}
|
|
isClearable
|
|
filterOption={customFilterForSelect}
|
|
isDisabled={!selectedDataCenter.sectorId}
|
|
/>
|
|
</FormGroup>
|
|
</Col>
|
|
<Col sm="12">
|
|
<FormGroup>
|
|
<Label>Emission Sources & Consumption Units</Label>
|
|
<div className="border rounded p-3 bg-light">
|
|
<div className="d-flex justify-content-between align-items-center mb-3">
|
|
<small className="text-muted">
|
|
Configure emission sources for this data center. At
|
|
least one emission source with consumption unit is
|
|
required.
|
|
</small>
|
|
<Button
|
|
color="primary"
|
|
size="sm"
|
|
onClick={() => {
|
|
setSelectedDataCenter({
|
|
...selectedDataCenter,
|
|
dataCenterEmissionSources: [
|
|
...selectedDataCenter.dataCenterEmissionSources,
|
|
{
|
|
emissionSourceId: null,
|
|
consuptionUnitId: null,
|
|
isDefault: false,
|
|
},
|
|
],
|
|
});
|
|
}}
|
|
disabled={!selectedDataCenter.subSectorId}
|
|
className="d-none d-sm-flex"
|
|
>
|
|
<Plus size={14} className="me-1" />
|
|
Add Source
|
|
</Button>
|
|
</div>
|
|
{selectedDataCenter.dataCenterEmissionSources.map(
|
|
(source, index) => (
|
|
<div
|
|
key={index}
|
|
className="border rounded p-3 mb-3 bg-white"
|
|
>
|
|
<Row className="g-3">
|
|
<Col xs="12" md="6">
|
|
<Label
|
|
for={`emissionSource-${index}`}
|
|
className="form-label fw-bold"
|
|
>
|
|
Emission Source *
|
|
</Label>
|
|
<Select
|
|
id={`emissionSource-${index}`}
|
|
placeholder="Select emission source..."
|
|
options={emissionSourcesOptions}
|
|
value={emissionSourcesOptions?.find(
|
|
(option) =>
|
|
option.value === source.emissionSourceId
|
|
)}
|
|
onChange={(option) => {
|
|
const updatedSources = [
|
|
...selectedDataCenter.dataCenterEmissionSources,
|
|
];
|
|
updatedSources[index] = {
|
|
...updatedSources[index],
|
|
emissionSourceId: option?.value,
|
|
consuptionUnitId: null, // Reset consumption unit when emission source changes
|
|
};
|
|
setSelectedDataCenter({
|
|
...selectedDataCenter,
|
|
dataCenterEmissionSources: updatedSources,
|
|
});
|
|
// Fetch consumption units for the selected emission source
|
|
if (option?.value) {
|
|
dispatch(
|
|
getConsuptionUnits({
|
|
id: option.value,
|
|
sector: selectedDataCenter?.sectorId,
|
|
})
|
|
);
|
|
}
|
|
}}
|
|
isClearable
|
|
filterOption={customFilterForSelect}
|
|
isDisabled={!selectedDataCenter.subSectorId}
|
|
styles={{
|
|
placeholder: (provided) => ({
|
|
...provided,
|
|
color: "#6e6b7b",
|
|
}),
|
|
}}
|
|
menuPlacement="auto"
|
|
/>
|
|
</Col>
|
|
<Col xs="12" md="6">
|
|
<Label
|
|
for={`consuptionUnit-${index}`}
|
|
className="form-label fw-bold"
|
|
>
|
|
Consumption Unit *
|
|
</Label>
|
|
<Select
|
|
id={`consuptionUnit-${index}`}
|
|
placeholder="Select consumption unit..."
|
|
options={consuptionUnitsOptions}
|
|
value={consuptionUnitsOptions?.find(
|
|
(option) =>
|
|
option.value === source.consuptionUnitId
|
|
)}
|
|
onChange={(option) => {
|
|
const updatedSources = [
|
|
...selectedDataCenter.dataCenterEmissionSources,
|
|
];
|
|
updatedSources[index] = {
|
|
...updatedSources[index],
|
|
consuptionUnitId: option?.value,
|
|
};
|
|
setSelectedDataCenter({
|
|
...selectedDataCenter,
|
|
dataCenterEmissionSources: updatedSources,
|
|
});
|
|
}}
|
|
isClearable
|
|
filterOption={customFilterForSelect}
|
|
isDisabled={!source.emissionSourceId}
|
|
styles={{
|
|
placeholder: (provided) => ({
|
|
...provided,
|
|
color: "#6e6b7b",
|
|
}),
|
|
}}
|
|
menuPlacement="auto"
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
<Row className="mt-3">
|
|
<Col xs="12">
|
|
<div className="d-flex flex-column flex-sm-row gap-2 justify-content-between align-items-start align-items-sm-center">
|
|
<div className="d-flex gap-2 flex-wrap">
|
|
<Button
|
|
color={
|
|
source.isDefault
|
|
? "primary"
|
|
: "outline-secondary"
|
|
}
|
|
size="sm"
|
|
onClick={() => {
|
|
const updatedSources = [
|
|
...selectedDataCenter.dataCenterEmissionSources,
|
|
];
|
|
// First, set all sources to not default
|
|
updatedSources.forEach(
|
|
(s) => (s.isDefault = false)
|
|
);
|
|
// Then set the selected one as default
|
|
updatedSources[index].isDefault = true;
|
|
setSelectedDataCenter({
|
|
...selectedDataCenter,
|
|
dataCenterEmissionSources:
|
|
updatedSources,
|
|
});
|
|
}}
|
|
title="Set as default emission source"
|
|
>
|
|
{source.isDefault
|
|
? "★ Default"
|
|
: "☆ Set Default"}
|
|
</Button>
|
|
{source.isDefault && (
|
|
<span className="badge bg-success align-self-center">
|
|
Default Source
|
|
</span>
|
|
)}
|
|
</div>
|
|
<Button
|
|
color="outline-danger"
|
|
size="sm"
|
|
onClick={() => {
|
|
const updatedSources =
|
|
selectedDataCenter.dataCenterEmissionSources.filter(
|
|
(_, i) => i !== index
|
|
);
|
|
// If we're removing the default source and there are other sources, make the first one default
|
|
if (
|
|
source.isDefault &&
|
|
updatedSources.length > 0
|
|
) {
|
|
updatedSources[0].isDefault = true;
|
|
}
|
|
setSelectedDataCenter({
|
|
...selectedDataCenter,
|
|
dataCenterEmissionSources: updatedSources,
|
|
});
|
|
}}
|
|
disabled={
|
|
selectedDataCenter.dataCenterEmissionSources
|
|
.length === 1
|
|
}
|
|
title="Remove emission source"
|
|
>
|
|
<Trash size={14} className="me-1" />
|
|
Remove
|
|
</Button>
|
|
</div>
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
)
|
|
)}
|
|
<div className="text-center mt-3">
|
|
<Button
|
|
color="primary"
|
|
size="sm"
|
|
onClick={() => {
|
|
setSelectedDataCenter({
|
|
...selectedDataCenter,
|
|
dataCenterEmissionSources: [
|
|
...selectedDataCenter.dataCenterEmissionSources,
|
|
{
|
|
emissionSourceId: null,
|
|
consuptionUnitId: null,
|
|
isDefault: false,
|
|
},
|
|
],
|
|
});
|
|
}}
|
|
disabled={!selectedDataCenter.subSectorId}
|
|
className="w-100 d-sm-none"
|
|
>
|
|
<Plus size={14} className="me-1" />
|
|
Add Another Emission Source
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</FormGroup>
|
|
</Col>
|
|
<Col sm="12">
|
|
<FormGroup>
|
|
<Label for="activitySubUnit">Activity Sub Unit</Label>
|
|
<Select
|
|
id="activitySubUnit"
|
|
name="activitySubUnit"
|
|
placeholder="Select activity sub unit..."
|
|
options={activitySubUnitsOptions}
|
|
value={activitySubUnitsOptions?.find(
|
|
(option) =>
|
|
option.value === selectedDataCenter.activitySubUnitId
|
|
)}
|
|
onChange={(option) => {
|
|
setSelectedDataCenter({
|
|
...selectedDataCenter,
|
|
activitySubUnitId: option?.value,
|
|
});
|
|
}}
|
|
isClearable
|
|
filterOption={customFilterForSelect}
|
|
isDisabled={!selectedDataCenter.subSectorId}
|
|
menuPlacement="top"
|
|
/>
|
|
</FormGroup>
|
|
</Col>
|
|
|
|
<Col sm="12">
|
|
<FormGroup>
|
|
<Label>{t("DataCenter.location")}</Label>
|
|
<div style={{ height: "400px", width: "100%" }}>
|
|
<MapContainer
|
|
center={mapPosition || [39.9334, 32.8597]} // Default to Ankara
|
|
zoom={13}
|
|
style={{ height: "100%", width: "100%" }}
|
|
>
|
|
<TileLayer
|
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
/>
|
|
<MapMarker
|
|
position={mapPosition}
|
|
setPosition={(pos) => {
|
|
setMapPosition(pos);
|
|
setSelectedDataCenter({
|
|
...selectedDataCenter,
|
|
latitude: pos[0],
|
|
longitude: pos[1],
|
|
});
|
|
}}
|
|
setSelectedDataCenter={setSelectedDataCenter}
|
|
/>
|
|
</MapContainer>
|
|
</div>
|
|
</FormGroup>
|
|
</Col>
|
|
</Row>
|
|
</Form>
|
|
</ModalBody>
|
|
<ModalFooter>
|
|
<Button
|
|
color="primary"
|
|
onClick={handleSubmit}
|
|
disabled={
|
|
!selectedDataCenter.name || !selectedDataCenter.externalId
|
|
}
|
|
>
|
|
{t("Common.save")}
|
|
</Button>
|
|
<Button color="secondary" onClick={handleCloseModal}>
|
|
{t("Common.cancel")}
|
|
</Button>
|
|
</ModalFooter>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader className="border-bottom">
|
|
<CardTitle tag="h4">{t("DataCenter.title")}</CardTitle>
|
|
{permissionCheck("data_center_create") && (
|
|
<Button
|
|
className="ml-2"
|
|
color="primary"
|
|
onClick={() => setShowAddModal(true)}
|
|
>
|
|
<Plus size={15} />
|
|
<span className="align-middle ml-50">{t("DataCenter.create")}</span>
|
|
</Button>
|
|
)}
|
|
</CardHeader>
|
|
<Row className="mx-0 mt-1 mb-50">
|
|
<Col sm="6">
|
|
<div className="d-flex align-items-center">
|
|
<Label for="sort-select">{t("Show")}</Label>
|
|
<Input
|
|
className="dataTable-select"
|
|
type="select"
|
|
id="sort-select"
|
|
value={rowsPerPage}
|
|
onChange={(e) => setRowsPerPage(Number(e.target.value))}
|
|
>
|
|
<option value={10}>10</option>
|
|
<option value={25}>25</option>
|
|
<option value={50}>50</option>
|
|
<option value={75}>75</option>
|
|
<option value={100}>100</option>
|
|
</Input>
|
|
</div>
|
|
</Col>
|
|
<Col
|
|
className="d-flex align-items-center justify-content-sm-end mt-sm-0 mt-1"
|
|
sm="6"
|
|
>
|
|
<Label className="me-1" for="search-input">
|
|
{t("Filter")}
|
|
</Label>
|
|
<Input
|
|
className="dataTable-filter"
|
|
type="text"
|
|
bsSize="sm"
|
|
id="search-input"
|
|
value={searchValue}
|
|
onChange={handleFilter}
|
|
placeholder={t("DataCenter.searchPlaceholder")}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
<div className="react-dataTable">
|
|
<DataTable
|
|
noHeader
|
|
pagination
|
|
columns={serverSideColumns}
|
|
paginationPerPage={rowsPerPage}
|
|
className="react-dataTable"
|
|
sortIcon={<ChevronDown size={10} />}
|
|
paginationDefaultPage={currentPage}
|
|
paginationComponent={CustomPagination}
|
|
data={dataCenterStore?.dataCenters || []}
|
|
progressPending={dataCenterStore?.loading}
|
|
progressComponent={<div className="text-center p-3">Loading...</div>}
|
|
noDataComponent={
|
|
<div className="p-2 text-center">{t("Common.noDataAvailable")}</div>
|
|
}
|
|
/>
|
|
</div>
|
|
{renderModal()}
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default memo(DataCenterManagement);
|