officially bring in new configurtaion with dynamic pricing for sodistore home and pro
This commit is contained in:
parent
b0bb332482
commit
795e77d304
|
|
@ -24,12 +24,7 @@ import { TimeSpan, UnixTime } from '../../../dataCache/time';
|
||||||
import { fetchDataJson } from '../Installations/fetchData';
|
import { fetchDataJson } from '../Installations/fetchData';
|
||||||
import { FetchResult } from '../../../dataCache/dataCache';
|
import { FetchResult } from '../../../dataCache/dataCache';
|
||||||
import BatteryViewSodioHome from '../BatteryView/BatteryViewSodioHome';
|
import BatteryViewSodioHome from '../BatteryView/BatteryViewSodioHome';
|
||||||
import SodistoreHomeConfiguration from './SodistoreHomeConfiguration';
|
|
||||||
import SodistoreHomeConfigurationV2 from './SodistoreHomeConfigurationV2';
|
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<number>([790, 839]);
|
|
||||||
import TopologySodistoreHome from '../Topology/TopologySodistoreHome';
|
import TopologySodistoreHome from '../Topology/TopologySodistoreHome';
|
||||||
import Overview from '../Overview/overview';
|
import Overview from '../Overview/overview';
|
||||||
import WeeklyReport from './WeeklyReport';
|
import WeeklyReport from './WeeklyReport';
|
||||||
|
|
@ -604,19 +599,11 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
<Route
|
<Route
|
||||||
path={routes.configuration}
|
path={routes.configuration}
|
||||||
element={
|
element={
|
||||||
CONFIG_V2_INSTALLATION_IDS.has(props.current_installation.id) ? (
|
|
||||||
<SodistoreHomeConfigurationV2
|
<SodistoreHomeConfigurationV2
|
||||||
values={values}
|
values={values}
|
||||||
id={props.current_installation.id}
|
id={props.current_installation.id}
|
||||||
installation={props.current_installation}
|
installation={props.current_installation}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<SodistoreHomeConfiguration
|
|
||||||
values={values}
|
|
||||||
id={props.current_installation.id}
|
|
||||||
installation={props.current_installation}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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<string, string> = {
|
|
||||||
'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<ConfigurationValues> => ({
|
|
||||||
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<ConfigurationValues> = {
|
|
||||||
...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<Partial<ConfigurationValues>>(() => {
|
|
||||||
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<ConfigurationValues> = {
|
|
||||||
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 (
|
|
||||||
<Container maxWidth="xl">
|
|
||||||
<Grid
|
|
||||||
container
|
|
||||||
direction="row"
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="stretch"
|
|
||||||
spacing={3}
|
|
||||||
>
|
|
||||||
{isErrorDateModalOpen && (
|
|
||||||
<Modal open={isErrorDateModalOpen} onClose={() => {}}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '50%',
|
|
||||||
left: '50%',
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
width: 450,
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
borderRadius: 4,
|
|
||||||
boxShadow: 24,
|
|
||||||
p: 4,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
gutterBottom
|
|
||||||
sx={{ fontWeight: 'bold' }}
|
|
||||||
>
|
|
||||||
{dateSelectionError}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
sx={{
|
|
||||||
marginTop: 2,
|
|
||||||
textTransform: 'none',
|
|
||||||
bgcolor: '#ffc04d',
|
|
||||||
color: '#111111',
|
|
||||||
'&:hover': { bgcolor: '#f7b34d' }
|
|
||||||
}}
|
|
||||||
onClick={handleOkOnErrorDateModal}
|
|
||||||
>
|
|
||||||
Ok
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Grid item xs={12} md={12}>
|
|
||||||
<CardContent>
|
|
||||||
<Box
|
|
||||||
component="form"
|
|
||||||
sx={{
|
|
||||||
'& .MuiTextField-root': { m: 1, width: 390 }
|
|
||||||
}}
|
|
||||||
noValidate
|
|
||||||
autoComplete="off"
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
<div style={{ marginBottom: '5px' }}>
|
|
||||||
<FormControlLabel
|
|
||||||
labelPlacement="start"
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
name="controlPermission"
|
|
||||||
checked={Boolean(formValues.controlPermission)}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormValues((prev) => ({
|
|
||||||
...prev,
|
|
||||||
controlPermission: e.target.checked,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
sx={{ transform: "scale(1.4)", marginLeft: "15px" }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id="controlPermission"
|
|
||||||
defaultMessage="Control Permission"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '5px' }}>
|
|
||||||
<TextField
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id="batteriesCount "
|
|
||||||
defaultMessage="Batteries Count"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
name="batteriesCount"
|
|
||||||
value={formValues.batteriesCount}
|
|
||||||
onChange={handleChange}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{device === 4 && (
|
|
||||||
<>
|
|
||||||
<div style={{ marginBottom: '5px' }}>
|
|
||||||
<TextField
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id="clusterNumber"
|
|
||||||
defaultMessage="Cluster Number"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
name="clusterNumber"
|
|
||||||
value={formValues.clusterNumber}
|
|
||||||
onChange={handleChange}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '5px' }}>
|
|
||||||
<TextField
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id="PvNumber"
|
|
||||||
defaultMessage="PV Number"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
name="PvNumber"
|
|
||||||
value={formValues.PvNumber}
|
|
||||||
onChange={handleChange}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '5px' }}>
|
|
||||||
{/*<TextField*/}
|
|
||||||
{/* label={*/}
|
|
||||||
{/* <FormattedMessage*/}
|
|
||||||
{/* id="minimum_soc "*/}
|
|
||||||
{/* defaultMessage="Minimum SoC (%)"*/}
|
|
||||||
{/* />*/}
|
|
||||||
{/* }*/}
|
|
||||||
{/* name="minimumSoC"*/}
|
|
||||||
{/* value={formValues.minimumSoC}*/}
|
|
||||||
{/* onChange={handleChange}*/}
|
|
||||||
{/* helperText={*/}
|
|
||||||
{/* errors.minimumSoC ? (*/}
|
|
||||||
{/* <span style={{ color: 'red' }}>*/}
|
|
||||||
{/* Value should be between {device === 4 ? '5–100' : '10–30'}%*/}
|
|
||||||
{/* </span>*/}
|
|
||||||
{/* ) : (*/}
|
|
||||||
{/* ''*/}
|
|
||||||
{/* )*/}
|
|
||||||
{/* }*/}
|
|
||||||
{/* fullWidth*/}
|
|
||||||
{/*/>*/}
|
|
||||||
<TextField
|
|
||||||
label={intl.formatMessage({ id: 'minimumSocPercent' })}
|
|
||||||
name="minimumSoC"
|
|
||||||
value={formValues.minimumSoC}
|
|
||||||
onChange={handleChange}
|
|
||||||
error={Boolean(errors.minimumSoC)}
|
|
||||||
helperText={errors.minimumSoC}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '5px' }}>
|
|
||||||
<TextField
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id="maximumChargingCurrent "
|
|
||||||
defaultMessage="Maximum Charging Current"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
name="maximumChargingCurrent"
|
|
||||||
value={formValues.maximumChargingCurrent}
|
|
||||||
onChange={handleChange}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '5px' }}>
|
|
||||||
<TextField
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id="maximumDischargingCurrent "
|
|
||||||
defaultMessage="Maximum Discharging Current"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
name="maximumDischargingCurrent"
|
|
||||||
value={formValues.maximumDischargingCurrent}
|
|
||||||
onChange={handleChange}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '5px', marginTop: '10px' }}>
|
|
||||||
<FormControl fullWidth sx={{ marginLeft: 1, width: 390 }}>
|
|
||||||
<InputLabel
|
|
||||||
sx={{
|
|
||||||
fontSize: 14,
|
|
||||||
backgroundColor: 'white'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id="operating_priority"
|
|
||||||
defaultMessage="Operating Priority"
|
|
||||||
/>
|
|
||||||
</InputLabel>
|
|
||||||
<Select
|
|
||||||
value={
|
|
||||||
OperatingPriorityOptions[formValues.operatingPriority]
|
|
||||||
}
|
|
||||||
onChange={handleOperatingPriorityChange}
|
|
||||||
>
|
|
||||||
{OperatingPriorityOptions.map((option) => (
|
|
||||||
<MenuItem key={option} value={option}>
|
|
||||||
{option}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
|
|
||||||
{/* --- Sinexcel + BatteryPriority (maps to TimeChargeDischarge on device) --- */}
|
|
||||||
{device === 4 &&
|
|
||||||
OperatingPriorityOptions[formValues.operatingPriority] ===
|
|
||||||
'BatteryPriority' && (
|
|
||||||
<>
|
|
||||||
{/* Power input*/}
|
|
||||||
<div style={{ marginBottom: '5px' }}>
|
|
||||||
<TextField
|
|
||||||
label={intl.formatMessage({ id: 'powerW' })}
|
|
||||||
name="timeChargeandDischargePower"
|
|
||||||
value={formValues.timeChargeandDischargePower}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleTimeChargeDischargeChange(e.target.name, e.target.value)
|
|
||||||
}
|
|
||||||
helperText={intl.formatMessage({ id: 'enterPowerValue' })}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Start DateTime */}
|
|
||||||
<div style={{ marginBottom: '5px' }}>
|
|
||||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
|
||||||
<DateTimePicker
|
|
||||||
ampm={false}
|
|
||||||
label={intl.formatMessage({ id: 'startDateTime' })}
|
|
||||||
value={
|
|
||||||
formValues.startTimeChargeandDischargeDayandTime
|
|
||||||
? dayjs(formValues.startTimeChargeandDischargeDayandTime)
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
onChange={(newValue) =>
|
|
||||||
setFormValues((prev) => ({
|
|
||||||
...prev,
|
|
||||||
startTimeChargeandDischargeDayandTime: newValue
|
|
||||||
? newValue.toDate()
|
|
||||||
: null,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
sx={{
|
|
||||||
marginTop: 2,
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</LocalizationProvider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stop DateTime */}
|
|
||||||
<div style={{ marginBottom: '5px' }}>
|
|
||||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
|
||||||
<DateTimePicker
|
|
||||||
ampm={false}
|
|
||||||
label={intl.formatMessage({ id: 'stopDateTime' })}
|
|
||||||
value={
|
|
||||||
formValues.stopTimeChargeandDischargeDayandTime
|
|
||||||
? dayjs(formValues.stopTimeChargeandDischargeDayandTime)
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
onChange={(newValue) =>
|
|
||||||
setFormValues((prev) => ({
|
|
||||||
...prev,
|
|
||||||
stopTimeChargeandDischargeDayandTime: newValue
|
|
||||||
? newValue.toDate()
|
|
||||||
: null,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
sx={{
|
|
||||||
marginTop: 2,
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</LocalizationProvider>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginTop: 10
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
sx={{
|
|
||||||
marginLeft: '10px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id="applychanges"
|
|
||||||
defaultMessage="Apply Changes"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
{loading && (
|
|
||||||
<CircularProgress
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
marginLeft: '20px'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{updated && (
|
|
||||||
<Alert
|
|
||||||
severity="success"
|
|
||||||
sx={{
|
|
||||||
ml: 1,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="successfullyAppliedConfig" defaultMessage="Successfully applied configuration file" />
|
|
||||||
<IconButton
|
|
||||||
color="inherit"
|
|
||||||
size="small"
|
|
||||||
onClick={() => setUpdated(false)}
|
|
||||||
sx={{ marginLeft: '4px' }}
|
|
||||||
>
|
|
||||||
<CloseIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
{error && (
|
|
||||||
<Alert
|
|
||||||
severity="error"
|
|
||||||
sx={{
|
|
||||||
ml: 1,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="configErrorOccurred" defaultMessage="An error has occurred" />
|
|
||||||
<IconButton
|
|
||||||
color="inherit"
|
|
||||||
size="small"
|
|
||||||
onClick={() => setError(false)}
|
|
||||||
sx={{ marginLeft: '4px' }}
|
|
||||||
>
|
|
||||||
<CloseIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
</CardContent>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SodistoreHomeConfiguration;
|
|
||||||
Loading…
Reference in New Issue