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 { 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<number>([790, 839]);
|
||||
import TopologySodistoreHome from '../Topology/TopologySodistoreHome';
|
||||
import Overview from '../Overview/overview';
|
||||
import WeeklyReport from './WeeklyReport';
|
||||
|
|
@ -604,19 +599,11 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
<Route
|
||||
path={routes.configuration}
|
||||
element={
|
||||
CONFIG_V2_INSTALLATION_IDS.has(props.current_installation.id) ? (
|
||||
<SodistoreHomeConfigurationV2
|
||||
values={values}
|
||||
id={props.current_installation.id}
|
||||
installation={props.current_installation}
|
||||
/>
|
||||
) : (
|
||||
<SodistoreHomeConfiguration
|
||||
values={values}
|
||||
id={props.current_installation.id}
|
||||
installation={props.current_installation}
|
||||
/>
|
||||
)
|
||||
<SodistoreHomeConfigurationV2
|
||||
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