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: null, latitude: row.latitude, longitude: row.longitude, ayposURL: row.ayposURL || "", city: row.city || "", 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, 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")}
setSelectedDataCenter({ ...selectedDataCenter, name: e.target.value, }) } /> setSelectedDataCenter({ ...selectedDataCenter, externalId: e.target.value, }) } /> setSelectedDataCenter({ ...selectedDataCenter, ayposURL: e.target.value, }) } /> option.value === selectedDataCenter.cityId )} onChange={(option) => { setSelectedDataCenter({ ...selectedDataCenter, cityId: option?.value, city: option?.label || "", }); }} isClearable filterOption={customFilterForSelect} isDisabled={!selectedDataCenter.areaId} />
{/* Emission Scope Section */}
Emission Scope Configuration
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} /> 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" /> option.value === selectedDataCenter.activitySubUnitId )} onChange={(option) => { setSelectedDataCenter({ ...selectedDataCenter, activitySubUnitId: option?.value, }); }} isClearable filterOption={customFilterForSelect} isDisabled={!selectedDataCenter.subSectorId} menuPlacement="top" />
{ setMapPosition(pos); setSelectedDataCenter({ ...selectedDataCenter, latitude: pos[0], longitude: pos[1], }); }} setSelectedDataCenter={setSelectedDataCenter} />
); }; 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);