From c7e60c25eb6de93394e9240120644100d400b00d Mon Sep 17 00:00:00 2001 From: Khaled Elagamy Date: Mon, 18 Aug 2025 06:12:19 +0300 Subject: [PATCH] Add datacenter with multiple emission source and inputs --- .../src/redux/actions/dataCenter/index.js | 8 +- .../src/views/DataCenterManagement.js | 583 +++++++++++++----- 2 files changed, 430 insertions(+), 161 deletions(-) diff --git a/sge-frontend/src/redux/actions/dataCenter/index.js b/sge-frontend/src/redux/actions/dataCenter/index.js index 249ea18..a8df393 100644 --- a/sge-frontend/src/redux/actions/dataCenter/index.js +++ b/sge-frontend/src/redux/actions/dataCenter/index.js @@ -251,8 +251,8 @@ export const createDataCenter = (dataCenterData) => { emissionScopeId: dataCenterData.emissionScopeId || null, sectorId: dataCenterData.sectorId || null, subSectorId: dataCenterData.subSectorId || null, - emissionSourceId: dataCenterData.emissionSourceId || null, - consuptionUnitId: dataCenterData.consuptionUnitId || null, + dataCenterEmissionSources: + dataCenterData.dataCenterEmissionSources || [], activitySubUnitId: dataCenterData.activitySubUnitId || null, }, }, @@ -382,8 +382,8 @@ export const updateDataCenter = (id, dataCenterData) => { emissionScopeId: dataCenterData.emissionScopeId || null, sectorId: dataCenterData.sectorId || null, subSectorId: dataCenterData.subSectorId || null, - emissionSourceId: dataCenterData.emissionSourceId || null, - consuptionUnitId: dataCenterData.consuptionUnitId || null, + dataCenterEmissionSources: + dataCenterData.dataCenterEmissionSources || [], activitySubUnitId: dataCenterData.activitySubUnitId || null, }, }, diff --git a/sge-frontend/src/views/DataCenterManagement.js b/sge-frontend/src/views/DataCenterManagement.js index 620e25d..0f14767 100644 --- a/sge-frontend/src/views/DataCenterManagement.js +++ b/sge-frontend/src/views/DataCenterManagement.js @@ -28,26 +28,37 @@ 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 { + 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 { + 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'; +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 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 - } + "User-Agent": "SGE-DataCenter-Management", // Required by Nominatim's usage policy + }, }); const Swal = withReactContent(SweetAlert); @@ -55,9 +66,9 @@ 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') + 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 @@ -66,30 +77,33 @@ const MapMarker = ({ position, setPosition, setSelectedDataCenter }) => { 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 => { + nominatimAxios + .get(`/reverse?format=json&lat=${e.latlng.lat}&lon=${e.latlng.lng}`) + .then((response) => { const address = response.data.display_name; - setSelectedDataCenter(prev => ({ + setSelectedDataCenter((prev) => ({ ...prev, address, latitude: e.latlng.lat, - longitude: e.latlng.lng + longitude: e.latlng.lng, })); }) - .catch(error => { - console.error('Error getting address:', error); + .catch((error) => { + console.error("Error getting address:", error); // Still update coordinates even if address lookup fails - setSelectedDataCenter(prev => ({ + setSelectedDataCenter((prev) => ({ ...prev, latitude: e.latlng.lat, - longitude: e.latlng.lng + longitude: e.latlng.lng, })); }); - } + }, }); // Only render marker if position exists and has valid coordinates - return position && position[0] && position[1] ? : null; + return position && position[0] && position[1] ? ( + + ) : null; }; const DataCenterManagement = () => { @@ -115,15 +129,14 @@ const DataCenterManagement = () => { emissionScopeId: null, sectorId: null, subSectorId: null, - emissionSourceId: null, - consuptionUnitId: null, - activitySubUnitId: 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); + console.log("DataCenter Store:", state.dataCenter); return state.dataCenter; }); const emissionScopeStore = useSelector((state) => state.emissionScope); @@ -138,7 +151,7 @@ const DataCenterManagement = () => { 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); @@ -170,7 +183,7 @@ const DataCenterManagement = () => { @@ -200,7 +213,9 @@ const DataCenterManagement = () => { onClick={() => handleEditDataCenter(row)} > - {t("Cruds.edit")} + + {t("Cruds.edit")} + )} {permissionCheck("data_center_delete") && ( @@ -210,7 +225,9 @@ const DataCenterManagement = () => { onClick={() => handleDeleteDataCenter(row)} > - {t("Cruds.delete")} + + {t("Cruds.delete")} + )} @@ -269,16 +286,41 @@ const DataCenterManagement = () => { ); }, [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 (selectedDataCenter?.emissionSourceId) { - dispatch( - getConsuptionUnits({ - id: selectedDataCenter?.emissionSourceId, - sector: selectedDataCenter?.sectorId, - }) - ); + if ( + showAddModal && + !editingDataCenter && + selectedDataCenter.dataCenterEmissionSources.length === 0 + ) { + setSelectedDataCenter((prev) => ({ + ...prev, + dataCenterEmissionSources: [ + { + emissionSourceId: null, + consuptionUnitId: null, + isDefault: true, + }, + ], + })); } - }, [selectedDataCenter?.emissionSourceId]); + }, [ + showAddModal, + editingDataCenter, + selectedDataCenter.dataCenterEmissionSources.length, + ]); useEffect(() => { if (selectedSubSector != null) { @@ -352,8 +394,18 @@ const DataCenterManagement = () => { }, [selectedDataCenter.areaId, areasStore?.areas]); const handleEditDataCenter = (row) => { - console.log('Editing data center:', 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(), @@ -368,17 +420,20 @@ const DataCenterManagement = () => { emissionScopeId: row.emissionScope?.id || null, sectorId: row.sector?.id || null, subSectorId: row.subSector?.id || null, - emissionSourceId: row.emissionSource?.id || null, - consuptionUnitId: row.consuptionUnit?.id || null, - activitySubUnitId: row.activitySubUnit?.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); - + // Only set map position if we have both address and valid coordinates - setMapPosition(row.address && row.latitude && row.longitude ? [row.latitude, row.longitude] : null); + setMapPosition( + row.address && row.latitude && row.longitude + ? [row.latitude, row.longitude] + : null + ); setShowAddModal(true); }; @@ -403,17 +458,16 @@ const DataCenterManagement = () => { enqueueSnackbar(t("DataCenter.deleteSuccess"), { variant: "success" }); } catch (error) { console.error("Delete error:", error); - enqueueSnackbar( - error?.message || t("DataCenter.deleteError"), - { variant: "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")); @@ -448,7 +502,7 @@ const DataCenterManagement = () => { try { const lat = parseFloat(selectedDataCenter.latitude); const lng = parseFloat(selectedDataCenter.longitude); - + if (isNaN(lat) || lat < -90 || lat > 90) { errors.push(t("DataCenter.invalidLatitude")); } @@ -473,23 +527,60 @@ const DataCenterManagement = () => { if (selectedDataCenter.subSectorId && !selectedDataCenter.sectorId) { errors.push(t("DataCenter.sectorRequired")); } - if (selectedDataCenter.emissionSourceId && !selectedDataCenter.subSectorId) { - errors.push(t("DataCenter.subSectorRequired")); - } - if (selectedDataCenter.consuptionUnitId && !selectedDataCenter.emissionSourceId) { - errors.push(t("DataCenter.emissionSourceRequired")); - } - if (selectedDataCenter.activitySubUnitId && !selectedDataCenter.subSectorId) { + 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 => { + validationErrors.forEach((error) => { enqueueSnackbar(error, { variant: "error" }); }); return; @@ -498,20 +589,26 @@ const DataCenterManagement = () => { try { // Format data according to GraphQL input type const dataToSubmit = { - dataCenter: selectedDataCenter.name, + 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, + 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, - emissionSourceId: selectedDataCenter.emissionSourceId, - consuptionUnitId: selectedDataCenter.consuptionUnitId, - activitySubUnitId: selectedDataCenter.activitySubUnitId + dataCenterEmissionSources: + selectedDataCenter.dataCenterEmissionSources.filter( + (source) => source.emissionSourceId && source.consuptionUnitId + ), + activitySubUnitId: selectedDataCenter.activitySubUnitId, }; if (editingDataCenter) { @@ -523,26 +620,27 @@ const DataCenterManagement = () => { 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" }); + 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" } - ); + enqueueSnackbar(error?.message || t("DataCenter.submitError"), { + variant: "error", + }); } } }; @@ -554,13 +652,28 @@ const DataCenterManagement = () => { externalId: "", number: "", address: "", + areaId: null, + cityId: null, latitude: null, longitude: null, ayposURL: "", - city: "" + 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) => { @@ -594,45 +707,47 @@ const DataCenterManagement = () => { const geocodeAddress = useCallback(async (address) => { if (!address || !address.trim()) { setMapPosition(null); - setSelectedDataCenter(prev => ({ + setSelectedDataCenter((prev) => ({ ...prev, latitude: null, - longitude: null + longitude: null, })); return false; } try { - const response = await nominatimAxios.get(`/search?format=json&q=${encodeURIComponent(address)}`); + 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 => ({ + setSelectedDataCenter((prev) => ({ ...prev, latitude: parseFloat(lat), - longitude: parseFloat(lon) + longitude: parseFloat(lon), })); return true; } // If no results found, clear the coordinates setMapPosition(null); - setSelectedDataCenter(prev => ({ + setSelectedDataCenter((prev) => ({ ...prev, latitude: null, - longitude: null + longitude: null, })); return false; } catch (error) { - console.error('Error geocoding address:', error); + console.error("Error geocoding address:", error); // On error, clear the coordinates setMapPosition(null); - setSelectedDataCenter(prev => ({ + setSelectedDataCenter((prev) => ({ ...prev, latitude: null, - longitude: null + longitude: null, })); return false; } @@ -641,18 +756,18 @@ const DataCenterManagement = () => { // Update the address input handler const handleAddressChange = (e) => { const newAddress = e.target.value; - setSelectedDataCenter(prev => ({ + setSelectedDataCenter((prev) => ({ ...prev, - address: newAddress + address: newAddress, })); // If address is empty, clear the coordinates if (!newAddress.trim()) { setMapPosition(null); - setSelectedDataCenter(prev => ({ + setSelectedDataCenter((prev) => ({ ...prev, latitude: null, - longitude: null + longitude: null, })); } else { // If address is not empty, try to geocode it after a delay @@ -675,9 +790,7 @@ const DataCenterManagement = () => { size="lg" > - {editingDataCenter - ? t("DataCenter.edit") - : t("DataCenter.add")} + {editingDataCenter ? t("DataCenter.edit") : t("DataCenter.add")}
@@ -685,7 +798,8 @@ const DataCenterManagement = () => { { { - + {/* Emission Scope Section */} -
Emission Scope Configuration
+
+ Emission Scope Configuration +
@@ -828,7 +945,8 @@ const DataCenterManagement = () => { placeholder="Select emission scope" options={emissionScopesOptions} value={emissionScopesOptions?.find( - (option) => option.value === selectedDataCenter.emissionScopeId + (option) => + option.value === selectedDataCenter.emissionScopeId )} onChange={(option) => setSelectedDataCenter({ @@ -843,7 +961,9 @@ const DataCenterManagement = () => { - + option.value === selectedDataCenter.emissionSourceId + +
+ + Configure emission sources for this data center. At least + one emission source with consumption unit is required. + + {selectedDataCenter.dataCenterEmissionSources.map( + (source, index) => ( + + + + + 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", + }), + }} + /> + + + +
+ + +
+ +
+ ) )} - onChange={(option) => { - setSelectedDataCenter({ - ...selectedDataCenter, - emissionSourceId: option?.value, - consuptionUnitId: null, - }); - }} - isClearable - filterOption={customFilterForSelect} - isDisabled={!selectedDataCenter.subSectorId} - /> - - - - - -