diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx index 3fedde12d..819d7e51e 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx @@ -24,12 +24,7 @@ import { TimeSpan, UnixTime } from '../../../dataCache/time'; import { fetchDataJson } from '../Installations/fetchData'; import { FetchResult } from '../../../dataCache/dataCache'; import BatteryViewSodioHome from '../BatteryView/BatteryViewSodioHome'; -import SodistoreHomeConfiguration from './SodistoreHomeConfiguration'; import SodistoreHomeConfigurationV2 from './SodistoreHomeConfigurationV2'; - -// Pilot installations using the new per-cluster Configuration page (V2). -// All other installations keep using the original SodistoreHomeConfiguration (V1). -const CONFIG_V2_INSTALLATION_IDS = new Set([790, 839]); import TopologySodistoreHome from '../Topology/TopologySodistoreHome'; import Overview from '../Overview/overview'; import WeeklyReport from './WeeklyReport'; @@ -604,19 +599,11 @@ function SodioHomeInstallation(props: singleInstallationProps) { - ) : ( - - ) + } /> )} diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistoreHomeConfiguration.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistoreHomeConfiguration.tsx deleted file mode 100644 index aafbbd349..000000000 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistoreHomeConfiguration.tsx +++ /dev/null @@ -1,715 +0,0 @@ -import { ConfigurationValues, JSONRecordData } from '../Log/graph.util'; -import { - Alert, - Box, - CardContent, - CircularProgress, - Container, - FormControl, - Grid, - IconButton, - InputLabel, - Modal, - Select, - TextField, - Typography, - useTheme -} from '@mui/material'; - -import React, { useContext, useState, useEffect } from 'react'; -import { FormattedMessage, useIntl } from 'react-intl'; -import Button from '@mui/material/Button'; -import { Close as CloseIcon } from '@mui/icons-material'; -import MenuItem from '@mui/material/MenuItem'; -import axiosConfig from '../../../Resources/axiosConfig'; -import { UserContext } from '../../../contexts/userContext'; -import { ProductIdContext } from '../../../contexts/ProductIdContextProvider'; -import { I_Installation } from 'src/interfaces/InstallationTypes'; -import { LocalizationProvider } from '@mui/x-date-pickers'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import {DateTimePicker } from '@mui/x-date-pickers'; -import dayjs from 'dayjs'; -import Switch from '@mui/material/Switch'; -import FormControlLabel from '@mui/material/FormControlLabel'; - -interface SodistoreHomeConfigurationProps { - values: JSONRecordData; - id: number; - installation: I_Installation; -} - -function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) { - const intl = useIntl(); - if (props.values === null) { - return null; - } - - const device = props.installation.device; - - const OperatingPriorityOptions = - device === 3 || device === 4 - ? ['LoadPriority', 'BatteryPriority', 'GridPriority'] - : []; - - // Sinexcel S3 stores WorkingMode enum names — map them to Growatt-style display names - const sinexcelS3ToDisplayName: Record = { - 'SpontaneousSelfUse': 'LoadPriority', - 'TimeChargeDischarge': 'BatteryPriority', - 'PvPriorityCharging': 'GridPriority', - }; - - const [errors, setErrors] = useState({ - minimumSoC: false, - gridSetPoint: false - }); - - const SetErrorForField = (field_name, state) => { - setErrors((prevErrors) => ({ - ...prevErrors, - [field_name]: state - })); - }; - const theme = useTheme(); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(false); - const [updated, setUpdated] = useState(false); - const [dateSelectionError, setDateSelectionError] = useState(''); - const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false); - const context = useContext(UserContext); - const { currentUser, setUser } = context; - const { product, setProduct } = useContext(ProductIdContext); - - // Resolve S3 OperatingPriority to display index (Sinexcel uses different enum names) - const resolveOperatingPriorityIndex = (s3Value: string) => { - const displayName = device === 4 ? (sinexcelS3ToDisplayName[s3Value] ?? s3Value) : s3Value; - return OperatingPriorityOptions.indexOf(displayName); - }; - - // Storage key for pending config (optimistic update) - const pendingConfigKey = `pendingConfig_${props.id}`; - - // Helper to build form values from S3 data - const getS3Values = (): Partial => ({ - minimumSoC: props.values.Config.MinSoc, - maximumDischargingCurrent: props.values.Config.MaximumDischargingCurrent, - maximumChargingCurrent: props.values.Config.MaximumChargingCurrent, - operatingPriority: resolveOperatingPriorityIndex( - props.values.Config.OperatingPriority - ), - batteriesCount: props.values.Config.BatteriesCount, - clusterNumber: props.values.Config.ClusterNumber ?? 1, - PvNumber: props.values.Config.PvNumber ?? 0, - timeChargeandDischargePower: props.values.Config?.TimeChargeandDischargePower ?? 0, - startTimeChargeandDischargeDayandTime: (() => { - const raw = props.values.Config?.StartTimeChargeandDischargeDayandTime; - const parsed = raw ? dayjs(raw) : null; - return parsed && parsed.year() >= 2020 ? parsed.toDate() : new Date(); - })(), - stopTimeChargeandDischargeDayandTime: (() => { - const raw = props.values.Config?.StopTimeChargeandDischargeDayandTime; - const parsed = raw ? dayjs(raw) : null; - return parsed && parsed.year() >= 2020 ? parsed.toDate() : new Date(); - })(), - controlPermission: String(props.values.Config.ControlPermission).toLowerCase() === "true", - }); - - // Restore pending config from localStorage, converting date strings back to Date objects. - // Returns { values, s3ConfigSnapshot } or null if no pending config. - const restorePendingConfig = () => { - try { - const pendingStr = localStorage.getItem(pendingConfigKey); - if (!pendingStr) return null; - - const pending = JSON.parse(pendingStr); - const v = pending.values; - const values: Partial = { - ...v, - // JSON.stringify converts Date→string; restore them back to Date objects - startTimeChargeandDischargeDayandTime: - v.startTimeChargeandDischargeDayandTime - ? dayjs(v.startTimeChargeandDischargeDayandTime).toDate() - : null, - stopTimeChargeandDischargeDayandTime: - v.stopTimeChargeandDischargeDayandTime - ? dayjs(v.stopTimeChargeandDischargeDayandTime).toDate() - : null, - }; - - return { values, s3ConfigSnapshot: pending.s3ConfigSnapshot || null }; - } catch (e) { - console.error('[Config:restore] Failed to parse localStorage', e); - localStorage.removeItem(pendingConfigKey); - return null; - } - }; - - // Fingerprint S3 Config for change detection (not value comparison) - const getS3ConfigFingerprint = () => JSON.stringify(props.values.Config); - - // Initialize form from localStorage (if pending submit exists) or from S3 - // This runs in the useState initializer so the component never renders stale values - const [formValues, setFormValues] = useState>(() => { - const pending = restorePendingConfig(); - const s3 = getS3Values(); - if (pending) { - // Check if S3 has new data since submit (fingerprint changed from snapshot) - const currentFingerprint = getS3ConfigFingerprint(); - const s3Changed = pending.s3ConfigSnapshot && currentFingerprint !== pending.s3ConfigSnapshot; - - if (s3Changed) { - // Device uploaded new data since our submit — trust S3 (device is authority) - localStorage.removeItem(pendingConfigKey); - return s3; - } - - // S3 still has same data as when we submitted — show pending values - return pending.values; - } - return s3; - }); - - // When S3 data updates (polled every 60s), reconcile with any pending localStorage. - // Strategy: device is the authority. Once S3 Config changes from the snapshot taken at - // submit time, the device has uploaded new data — trust S3 regardless of values. - useEffect(() => { - const s3Values = getS3Values(); - const pending = restorePendingConfig(); - - if (pending) { - const currentFingerprint = getS3ConfigFingerprint(); - const s3Changed = pending.s3ConfigSnapshot && currentFingerprint !== pending.s3ConfigSnapshot; - if (s3Changed) { - // S3 Config changed from snapshot → device uploaded new data → trust S3 - localStorage.removeItem(pendingConfigKey); - setFormValues(s3Values); - } else { - // S3 still has same data as at submit time — keep showing pending values - setFormValues(pending.values); - } - return; - } - - // No pending config — trust S3 (source of truth) - setFormValues(s3Values); - }, [props.values]); - - const handleOperatingPriorityChange = (event) => { - setFormValues({ - ...formValues, - ['operatingPriority']: OperatingPriorityOptions.indexOf( - event.target.value - ) - }); - }; - -// Add time validation function — only relevant for Sinexcel BatteryPriority - const validateTimeOnly = () => { - if (device === 4 && - OperatingPriorityOptions[formValues.operatingPriority] === 'BatteryPriority' && - formValues.startTimeChargeandDischargeDayandTime && - formValues.stopTimeChargeandDischargeDayandTime) { - const startHours = formValues.startTimeChargeandDischargeDayandTime.getHours(); - const startMinutes = formValues.startTimeChargeandDischargeDayandTime.getMinutes(); - const stopHours = formValues.stopTimeChargeandDischargeDayandTime.getHours(); - const stopMinutes = formValues.stopTimeChargeandDischargeDayandTime.getMinutes(); - - const startTimeInMinutes = startHours * 60 + startMinutes; - const stopTimeInMinutes = stopHours * 60 + stopMinutes; - - if (startTimeInMinutes >= stopTimeInMinutes) { - setDateSelectionError(intl.formatMessage({ id: 'stopTimeMustBeLater' })); - setErrorDateModalOpen(true); - return false; - } - } - return true; - }; - - const handleSubmit = async (e) => { - if (!validateTimeOnly()) { - return; - } - const configurationToSend: Partial = { - minimumSoC: formValues.minimumSoC, - maximumDischargingCurrent: formValues.maximumDischargingCurrent, - maximumChargingCurrent: formValues.maximumChargingCurrent, - operatingPriority: formValues.operatingPriority, - batteriesCount:formValues.batteriesCount, - clusterNumber:formValues.clusterNumber, - PvNumber:formValues.PvNumber, - timeChargeandDischargePower: formValues.timeChargeandDischargePower, - startTimeChargeandDischargeDayandTime: formValues.startTimeChargeandDischargeDayandTime - ? new Date(formValues.startTimeChargeandDischargeDayandTime.getTime() - formValues.startTimeChargeandDischargeDayandTime.getTimezoneOffset() * 60000) - : null, - stopTimeChargeandDischargeDayandTime: formValues.stopTimeChargeandDischargeDayandTime - ? new Date(formValues.stopTimeChargeandDischargeDayandTime.getTime() - formValues.stopTimeChargeandDischargeDayandTime.getTimezoneOffset() * 60000) - : null, - controlPermission:formValues.controlPermission - }; - - setLoading(true); - const res = await axiosConfig - .post( - `/EditInstallationConfig?installationId=${props.id}&product=${product}`, - configurationToSend - ) - .catch((err) => { - if (err.response) { - setError(true); - setLoading(false); - } - }); - - if (res) { - setUpdated(true); - setLoading(false); - - // Save submitted values + S3 snapshot to localStorage for optimistic UI update. - // s3ConfigSnapshot = fingerprint of S3 Config at submit time. - // When S3 Config changes from this snapshot, the device has uploaded new data. - const cachePayload = { - values: formValues, - submittedAt: Date.now(), - s3ConfigSnapshot: getS3ConfigFingerprint(), - }; - localStorage.setItem(pendingConfigKey, JSON.stringify(cachePayload)); - } - }; - - const handleChange = (e) => { - const { name, value } = e.target; - - if (name === 'minimumSoC') { - const numValue = parseFloat(value); - - // invalid characters or not a number - if (/[^0-9.]/.test(value) || isNaN(numValue)) { - SetErrorForField(name, 'Invalid number format'); - } else { - const minsocRanges = { - 3: { min: 10, max: 30 }, - 4: { min: 5, max: 100 }, - }; - - const { min, max } = minsocRanges[device] || { min: 10, max: 30 }; - - if (numValue < min || numValue > max) { - SetErrorForField(name, `Value should be between ${min}-${max}%`); - } else { - // ✅ valid → clear error - SetErrorForField(name, ''); - } - } - } - - setFormValues(prev => ({ - ...prev, - [name]: value, - })); - }; - - const handleTimeChargeDischargeChange = (name: string, value: any) => { - setFormValues((prev) => ({ - ...prev, - [name]: value - })); - }; - - const handleOkOnErrorDateModal = () => { - setErrorDateModalOpen(false); - }; - - return ( - - - {isErrorDateModalOpen && ( - {}}> - - - {dateSelectionError} - - - - - - )} - - - - - <> -
- - setFormValues((prev) => ({ - ...prev, - controlPermission: e.target.checked, - })) - } - sx={{ transform: "scale(1.4)", marginLeft: "15px" }} - /> - } - - label={ - - } - /> -
- -
- - } - name="batteriesCount" - value={formValues.batteriesCount} - onChange={handleChange} - fullWidth - /> -
- - {device === 4 && ( - <> -
- - } - name="clusterNumber" - value={formValues.clusterNumber} - onChange={handleChange} - fullWidth - /> -
- -
- - } - name="PvNumber" - value={formValues.PvNumber} - onChange={handleChange} - fullWidth - /> -
- - )} - - -
- {/**/} - {/* }*/} - {/* name="minimumSoC"*/} - {/* value={formValues.minimumSoC}*/} - {/* onChange={handleChange}*/} - {/* helperText={*/} - {/* errors.minimumSoC ? (*/} - {/* */} - {/* Value should be between {device === 4 ? '5–100' : '10–30'}%*/} - {/* */} - {/* ) : (*/} - {/* ''*/} - {/* )*/} - {/* }*/} - {/* fullWidth*/} - {/*/>*/} - - -
- -
- - } - name="maximumChargingCurrent" - value={formValues.maximumChargingCurrent} - onChange={handleChange} - fullWidth - /> -
- -
- - } - name="maximumDischargingCurrent" - value={formValues.maximumDischargingCurrent} - onChange={handleChange} - fullWidth - /> -
- -
- - - - - - -
- - - {/* --- Sinexcel + BatteryPriority (maps to TimeChargeDischarge on device) --- */} - {device === 4 && - OperatingPriorityOptions[formValues.operatingPriority] === - 'BatteryPriority' && ( - <> - {/* Power input*/} -
- - handleTimeChargeDischargeChange(e.target.name, e.target.value) - } - helperText={intl.formatMessage({ id: 'enterPowerValue' })} - fullWidth - /> -
- - {/* Start DateTime */} -
- - - setFormValues((prev) => ({ - ...prev, - startTimeChargeandDischargeDayandTime: newValue - ? newValue.toDate() - : null, - })) - } - renderInput={(params) => ( - - )} - /> - -
- - {/* Stop DateTime */} -
- - - setFormValues((prev) => ({ - ...prev, - stopTimeChargeandDischargeDayandTime: newValue - ? newValue.toDate() - : null, - })) - } - renderInput={(params) => ( - - )} - /> - -
- - )} - -
- - {loading && ( - - )} - - {updated && ( - - - setUpdated(false)} - sx={{ marginLeft: '4px' }} - > - - - - )} - {error && ( - - - setError(false)} - sx={{ marginLeft: '4px' }} - > - - - - )} -
-
-
-
-
-
- ); -} - -export default SodistoreHomeConfiguration;