Files
sgeUpdated/sge-frontend/src/views/DataCenterManagement.js

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='&copy; <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);