Update frontend. Added Features: SodioHome installations, User Access

This commit is contained in:
Noe 2025-09-01 12:59:01 +02:00
parent b424643213
commit 0d8978c6b0
39 changed files with 1906 additions and 1219 deletions

View File

@ -1,4 +1,4 @@
npm run build && rsync -rv .* ubuntu@194.182.190.208:~/frontend/ && ssh ubuntu@194.182.190.208 'sudo cp -rf ~/frontend/build/* /var/www/html/monitor.innov.energy/html/' && ssh ubuntu@194.182.190.208 'sudo npm install -g serve'
npm run build && rsync -rv .* ubuntu@194.182.190.208:~/frontend/ && ssh ubuntu@194.182.190.208 'sudo cp -rf ~/frontend/build/* /var/www/html/monitor.inesco.energy/html/' && ssh ubuntu@194.182.190.208 'sudo npm install -g serve'
#npm run build && rsync -rv .* ubuntu@91.92.154.141:~/frontend/ && ssh ubuntu@91.92.154.141 'sudo cp -rf ~/frontend/build/* /var/www/html/monitor.innov.energy/html/' && ssh ubuntu@91.92.154.141 'sudo npm install -g serve'
#npm run build && rsync -rv .* ubuntu@91.92.154.141:~/frontend/ && ssh ubuntu@91.92.154.141 'sudo cp -rf ~/frontend/build/* /var/www/html/monitor.inesco.energy/html/' && ssh ubuntu@91.92.154.141 'sudo npm install -g serve'

View File

@ -1 +1 @@
npm run build && rsync -rv .* ubuntu@91.92.154.141:~/frontend/ && ssh ubuntu@91.92.154.141 'sudo cp -rf ~/frontend/build/* /var/www/html/stage.innov.energy/html/' && ssh ubuntu@91.92.154.141 'sudo npm install -g serve'
npm run build && rsync -rv .* ubuntu@91.92.154.141:~/frontend/ && ssh ubuntu@91.92.154.141 'sudo cp -rf ~/frontend/build/* /var/www/html/stage.inesco.energy/html/' && ssh ubuntu@91.92.154.141 'sudo npm install -g serve'

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8"/>
<link href="%PUBLIC_URL%/favicon.png" rel="shortcut icon"/>
<link href="%PUBLIC_URL%/Logo.svg" rel="shortcut icon"/>
<meta
content="width=device-width, initial-scale=1, shrink-to-fit=no"
name="viewport"
@ -13,7 +13,7 @@
href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400&display=swap"
rel="stylesheet"
/>
<title>InnovEnergy</title>
<title>Inesco Energy</title>
</head>
<body>

View File

@ -186,7 +186,7 @@ function App() {
path={routes.sodiohome_installations + '*'}
element={
<AccessContextProvider>
<SodioHomeInstallationTabs />
<SodioHomeInstallationTabs product={2} />
</AccessContextProvider>
}
/>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -1,12 +1,12 @@
import axios from 'axios';
export const axiosConfigWithoutToken = axios.create({
baseURL: 'https://monitor.innov.energy/api'
baseURL: 'https://monitor.inesco.energy/api'
//baseURL: 'http://127.0.0.1:7087/api'
});
const axiosConfig = axios.create({
baseURL: 'https://monitor.innov.energy/api'
baseURL: 'https://monitor.inesco.energy/api'
//baseURL: 'http://127.0.0.1:7087/api'
});

View File

@ -18,7 +18,7 @@ function Footer() {
>
<Box>
<Typography variant="subtitle1">
&copy; 2024 - InnovEnergy AG
&copy; 2025 - Inesco Energy AG
</Typography>
</Box>
<Typography
@ -29,11 +29,11 @@ function Footer() {
>
Crafted by{' '}
<Link
href="https://www.innov.energy/"
href="https://www.inesco.energy/"
target="_blank"
rel="noopener noreferrer"
>
InnovEnergy AG
Inesco Energy AG
</Link>
</Typography>
</Box>

View File

@ -10,7 +10,8 @@ import {
Typography,
useTheme
} from '@mui/material';
import innovenergyLogo from 'src/Resources/innoveng_logo_on_orange.png';
import inescologo from 'src/Resources/Logo.svg';
import { UserContext } from 'src/contexts/userContext';
import { TokenContext } from 'src/contexts/tokenContext';
import Avatar from '@mui/material/Avatar';
@ -76,8 +77,8 @@ function ForgotPassword() {
<Container maxWidth="xl" sx={{ pt: 2 }}>
<Grid container>
<Grid item xs={3} container justifyContent="flex-start" mb={2}>
<a href="https://monitor.innov.energy/">
<img src={innovenergyLogo} alt="innovenergy logo" height="100" />
<a href="https://monitor.inesco.energy/">
<img src={inescologo} alt="inesco logo" height="100" />
</a>
</Grid>
</Grid>
@ -100,7 +101,7 @@ function ForgotPassword() {
transform: 'translate(-50%, -50%)'
}}
>
<Avatar sx={{ m: 1, bgcolor: '#ffc04d' }}>
<Avatar sx={{ m: 1, bgcolor: '#00b33c' }}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
@ -134,15 +135,15 @@ function ForgotPassword() {
}}
/>
{loading && <CircularProgress sx={{ color: '#ffc04d' }} />}
{loading && <CircularProgress sx={{ color: '#00b33c' }} />}
<Button
sx={{
mt: 3,
mb: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
'&:hover': { bgcolor: '#f7b34d' }
bgcolor: '#00b33c',
'&:hover': { bgcolor: '#009933' }
}}
variant="contained"
fullWidth={true}
@ -181,9 +182,9 @@ function ForgotPassword() {
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
bgcolor: '#00b33c',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
'&:hover': { bgcolor: '#009933' }
}}
onClick={() => setErrorModalOpen(false)}
>
@ -221,9 +222,9 @@ function ForgotPassword() {
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
bgcolor: '#00b33c',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
'&:hover': { bgcolor: '#009933' }
}}
onClick={handleReturn}
>

View File

@ -10,7 +10,8 @@ import {
Typography,
useTheme
} from '@mui/material';
import innovenergyLogo from 'src/Resources/innoveng_logo_on_orange.png';
import inescologo from 'src/Resources/Logo.svg';
import axiosConfig from 'src/Resources/axiosConfig';
import { UserContext } from 'src/contexts/userContext';
import { TokenContext } from 'src/contexts/tokenContext';
@ -73,8 +74,8 @@ function ResetPassword() {
<Container maxWidth="xl" sx={{ pt: 2 }}>
<Grid container>
<Grid item xs={3} container justifyContent="flex-start" mb={2}>
<a href="https://monitor.innov.energy/">
<img src={innovenergyLogo} alt="innovenergy logo" height="100" />
<a href="https://monitor.inesco.energy/">
<img src={inescologo} alt="inesco logo" height="100" />
</a>
</Grid>
</Grid>
@ -97,7 +98,7 @@ function ResetPassword() {
transform: 'translate(-50%, -50%)'
}}
>
<Avatar sx={{ m: 1, bgcolor: '#ffc04d' }}>
<Avatar sx={{ m: 1, bgcolor: '#00b33c' }}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
@ -137,7 +138,7 @@ function ResetPassword() {
/>
{loading && (
<CircularProgress sx={{ color: '#ffc04d', marginLeft: '170px' }} />
<CircularProgress sx={{ color: '#00b33c', marginLeft: '170px' }} />
)}
{password != verifypassword && (
@ -155,8 +156,8 @@ function ResetPassword() {
mt: 3,
mb: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
'&:hover': { bgcolor: '#f7b34d' }
bgcolor: '#00b33c',
'&:hover': { bgcolor: '#009933' }
}}
variant="contained"
fullWidth={true}
@ -195,9 +196,9 @@ function ResetPassword() {
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
bgcolor: '#00b33c',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
'&:hover': { bgcolor: '#009933' }
}}
onClick={() => setOpen(false)}
>

View File

@ -10,7 +10,8 @@ import {
Typography,
useTheme
} from '@mui/material';
import innovenergyLogo from 'src/Resources/innoveng_logo_on_orange.png';
import inescologo from 'src/Resources/Logo.svg';
import axiosConfig from 'src/Resources/axiosConfig';
import { UserContext } from 'src/contexts/userContext';
import { TokenContext } from 'src/contexts/tokenContext';
@ -74,8 +75,8 @@ function SetNewPassword() {
<Container maxWidth="xl" sx={{ pt: 2 }}>
<Grid container>
<Grid item xs={3} container justifyContent="flex-start" mb={2}>
<a href="https://monitor.innov.energy/">
<img src={innovenergyLogo} alt="innovenergy logo" height="100" />
<a href="https://monitor.inesco.energy/">
<img src={inescologo} alt="inesco logo" height="100" />
</a>
</Grid>
</Grid>
@ -98,7 +99,7 @@ function SetNewPassword() {
transform: 'translate(-50%, -50%)'
}}
>
<Avatar sx={{ m: 1, bgcolor: '#ffc04d' }}>
<Avatar sx={{ m: 1, bgcolor: '#00b33c' }}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
@ -138,7 +139,7 @@ function SetNewPassword() {
/>
{loading && (
<CircularProgress sx={{ color: '#ffc04d', marginLeft: '0px' }} />
<CircularProgress sx={{ color: '#00b33c', marginLeft: '0px' }} />
)}
{password != verifypassword && (
@ -156,8 +157,8 @@ function SetNewPassword() {
mt: 3,
mb: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
'&:hover': { bgcolor: '#f7b34d' }
bgcolor: '#00b33c',
'&:hover': { bgcolor: '#009933' }
}}
variant="contained"
fullWidth={true}
@ -196,9 +197,9 @@ function SetNewPassword() {
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
bgcolor: '#00b33c',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
'&:hover': { bgcolor: '#009933' }
}}
onClick={() => setOpen(false)}
>

View File

@ -15,7 +15,7 @@ import {
import Avatar from '@mui/material/Avatar';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Link from '@mui/material/Link';
import innovenergyLogo from 'src/Resources/innoveng_logo_on_orange.png';
import inescologo from 'src/Resources/Logo.svg';
import { axiosConfigWithoutToken } from 'src/Resources/axiosConfig';
import Cookies from 'universal-cookie';
import { UserContext } from 'src/contexts/userContext';
@ -93,6 +93,8 @@ function Login() {
navigate(routes.installations);
} else if (response.data.accessToSalidomo) {
navigate(routes.salidomo_installations);
} else if (response.data.accessToSodistoreMax) {
navigate(routes.sodistore_installations);
} else {
navigate(routes.sodiohome_installations);
}
@ -113,8 +115,8 @@ function Login() {
<Container maxWidth="xl" sx={{ pt: 2 }} className="login">
<Grid container>
<Grid item xs={3} container justifyContent="flex-start" mb={2}>
<a href="https://monitor.innov.energy/">
<img src={innovenergyLogo} alt="innovenergy logo" height="100" />
<a href="https://monitor.inesco.energy/">
<img src={inescologo} alt="inescologo" height="100" />
</a>
</Grid>
</Grid>
@ -137,7 +139,7 @@ function Login() {
transform: 'translate(-50%, -50%)'
}}
>
<Avatar sx={{ m: 1, bgcolor: '#ffc04d' }}>
<Avatar sx={{ m: 1, bgcolor: '#00b33c' }}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
@ -193,7 +195,7 @@ function Login() {
checked={rememberMe}
onChange={handleRememberMeChange}
icon={<CheckBoxOutlineBlankIcon style={{ color: 'grey' }} />}
checkedIcon={<CheckBoxIcon style={{ color: '#ffc04d' }} />}
checkedIcon={<CheckBoxIcon style={{ color: '#00b33c' }} />}
style={{ marginLeft: -175 }}
/>
}
@ -204,8 +206,8 @@ function Login() {
sx={{
mb: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
'&:hover': { bgcolor: '#f7b34d' }
bgcolor: '#00b33c',
'&:hover': { bgcolor: '#009933' }
}}
variant="contained"
fullWidth={true}
@ -218,7 +220,7 @@ function Login() {
{loading && (
<CircularProgress
sx={{
color: '#ffc04d',
color: '#009933',
marginLeft: '20px'
}}
/>
@ -253,9 +255,9 @@ function Login() {
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
bgcolor: '#00b33c',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
'&:hover': { bgcolor: '#009933' }
}}
onClick={() => setOpen(false)}
>

View File

@ -43,6 +43,8 @@ function BatteryView(props: BatteryViewProps) {
const currentLocation = useLocation();
const navigate = useNavigate();
const { product, setProduct } = useContext(ProductIdContext);
const sortedBatteryView =
props.values != null && props.values?.Battery?.Devices
? Object.entries(props.values.Battery.Devices)
@ -58,8 +60,6 @@ function BatteryView(props: BatteryViewProps) {
navigate(routes.mainstats);
};
const { product, setProduct } = useContext(ProductIdContext);
useEffect(() => {
if (sortedBatteryView.length == 0) {
setLoading(true);
@ -232,7 +232,7 @@ function BatteryView(props: BatteryViewProps) {
<TableCell align="center">Battery</TableCell>
<TableCell align="center">Firmware</TableCell>
<TableCell align="center">Power</TableCell>
<TableCell align="center">Voltage</TableCell>
<TableCell align="center">Battery Voltage</TableCell>
<TableCell align="center">SoC</TableCell>
<TableCell align="center">Temperature</TableCell>
{product === 0 ? (
@ -293,7 +293,7 @@ function BatteryView(props: BatteryViewProps) {
</TableCell>
<TableCell
sx={{
width: '10%',
width: '14%',
textAlign: 'center',
backgroundColor:

View File

@ -0,0 +1,333 @@
import React, { useEffect, useState } from 'react';
import {
Container,
Grid,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography
} from '@mui/material';
import { JSONRecordData } from '../Log/graph.util';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import Button from '@mui/material/Button';
import { FormattedMessage } from 'react-intl';
import { I_S3Credentials } from '../../../interfaces/S3Types';
import routes from '../../../Resources/routes.json';
import CircularProgress from '@mui/material/CircularProgress';
interface BatteryViewSodioHomeProps {
values: JSONRecordData;
s3Credentials: I_S3Credentials;
installationId: number;
connected: boolean;
}
function BatteryViewSodioHome(props: BatteryViewSodioHomeProps) {
if (props.values === null && props.connected == true) {
return null;
}
const currentLocation = useLocation();
const navigate = useNavigate();
const sortedBatteryView =
props.values != null &&
props.values?.AcDcGrowatt?.BatteriesRecords?.Batteries
? Object.entries(props.values.AcDcGrowatt.BatteriesRecords.Batteries)
.map(([BatteryId, battery]) => {
return { BatteryId, battery }; // Here we return an object with the id and device
})
.sort((a, b) => parseInt(b.BatteryId) - parseInt(a.BatteryId))
: [];
console.log('battery view', sortedBatteryView);
const [loading, setLoading] = useState(sortedBatteryView.length == 0);
const handleMainStatsButton = () => {
navigate(routes.mainstats);
};
useEffect(() => {
if (sortedBatteryView.length == 0) {
setLoading(true);
} else {
setLoading(false);
}
}, [sortedBatteryView]);
return (
<>
{!props.connected && (
<Container
maxWidth="xl"
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '70vh'
}}
>
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
<Typography
variant="body2"
style={{ color: 'black', fontWeight: 'bold' }}
mt={2}
>
Unable to communicate with the installation
</Typography>
<Typography variant="body2" style={{ color: 'black' }}>
Please wait or refresh the page
</Typography>
</Container>
)}
{loading && props.connected && (
<Container
maxWidth="xl"
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '70vh'
}}
>
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
<Typography
variant="body2"
style={{ color: 'black', fontWeight: 'bold' }}
mt={2}
>
Battery service is not available at the moment
</Typography>
<Typography variant="body2" style={{ color: 'black' }}>
Please wait or refresh the page
</Typography>
</Container>
)}
{!loading && props.connected && (
<Container maxWidth="xl">
<Grid container>
<Grid
item
xs={6}
md={6}
sx={{
display:
!currentLocation.pathname.includes('detailed_view') &&
!currentLocation.pathname.includes('mainstats')
? 'block'
: 'none'
}}
>
<Button
variant="contained"
sx={{
marginTop: '20px',
backgroundColor: '#808080',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage
id="battery_view"
defaultMessage="Battery View"
/>
</Button>
<Button
variant="contained"
onClick={handleMainStatsButton}
sx={{
marginTop: '20px',
marginLeft: '20px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="main_stats" defaultMessage="Main Stats" />
</Button>
</Grid>
</Grid>
{/*<Grid container>*/}
{/* <Routes>*/}
{/* <Route*/}
{/* path={routes.mainstats + '*'}*/}
{/* element={*/}
{/* <MainStats*/}
{/* s3Credentials={props.s3Credentials}*/}
{/* id={props.installationId}*/}
{/* ></MainStats>*/}
{/* }*/}
{/* />*/}
{/* {product === 0*/}
{/* ? Object.entries(props.values.Battery.Devices).map(*/}
{/* ([BatteryId, battery]) => (*/}
{/* <Route*/}
{/* key={routes.detailed_view + BatteryId}*/}
{/* path={routes.detailed_view + BatteryId}*/}
{/* element={*/}
{/* <DetailedBatteryView*/}
{/* batteryId={Number(BatteryId)}*/}
{/* s3Credentials={props.s3Credentials}*/}
{/* batteryData={battery}*/}
{/* installationId={props.installationId}*/}
{/* productNum={product}*/}
{/* ></DetailedBatteryView>*/}
{/* }*/}
{/* />*/}
{/* )*/}
{/* )*/}
{/* : Object.entries(props.values.Battery.Devices).map(*/}
{/* ([BatteryId, battery]) => (*/}
{/* <Route*/}
{/* key={routes.detailed_view + BatteryId}*/}
{/* path={routes.detailed_view + BatteryId}*/}
{/* element={*/}
{/* <DetailedBatteryViewSodistore*/}
{/* batteryId={Number(BatteryId)}*/}
{/* s3Credentials={props.s3Credentials}*/}
{/* batteryData={battery}*/}
{/* installationId={props.installationId}*/}
{/* productNum={product}*/}
{/* ></DetailedBatteryViewSodistore>*/}
{/* }*/}
{/* />*/}
{/* )*/}
{/* )}*/}
{/* </Routes>*/}
{/*</Grid>*/}
<TableContainer
component={Paper}
sx={{
marginTop: '20px',
marginBottom: '20px',
display:
!currentLocation.pathname.includes('detailed_view') &&
!currentLocation.pathname.includes('mainstats')
? 'block'
: 'none'
}}
>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell align="center">Battery</TableCell>
<TableCell align="center">Power</TableCell>
<TableCell align="center">Battery Voltage</TableCell>
<TableCell align="center">Current</TableCell>
<TableCell align="center">SoC</TableCell>
<TableCell align="center">SoH</TableCell>
<TableCell align="center">Daily Charge Energy</TableCell>
<TableCell align="center">Daily Discharge Energy</TableCell>
</TableRow>
</TableHead>
<TableBody>
{sortedBatteryView.map(({ BatteryId, battery }) => (
<TableRow
key={BatteryId}
style={{
height: '10px'
}}
>
<TableCell
component="th"
scope="row"
align="center"
sx={{ fontWeight: 'bold' }}
>
<Link
style={{ color: 'black' }}
to={routes.detailed_view + BatteryId}
>
{'Battery Cluster ' + BatteryId}
</Link>
</TableCell>
<TableCell
sx={{
width: '13%',
textAlign: 'center'
}}
>
{battery.Power + ' ' + 'W'}
</TableCell>
<TableCell
sx={{
width: '13%',
textAlign: 'center',
backgroundColor: '#32CD32',
color: 'black'
}}
>
{battery.Voltage + ' ' + 'V'}
</TableCell>
<TableCell
sx={{
width: '13%',
textAlign: 'center',
backgroundColor: '#32CD32',
color: 'inherit'
}}
>
{battery.Current + ' A'}
</TableCell>
<TableCell
sx={{
width: '8%',
textAlign: 'center',
backgroundColor: '#32CD32',
color: 'inherit'
}}
>
{battery.Soc + ' %'}
</TableCell>
<TableCell
sx={{
width: '8%',
textAlign: 'center',
backgroundColor: '#32CD32',
color: 'inherit'
}}
>
{battery.Soh + ' %'}
</TableCell>
<TableCell
sx={{
width: '15%',
textAlign: 'center',
backgroundColor: '#32CD32',
color: 'inherit'
}}
>
{battery.DailyChargeEnergy + ' Wh'}
</TableCell>
<TableCell
sx={{
width: '15%',
textAlign: 'center',
backgroundColor: '#32CD32',
color: 'inherit'
}}
>
{battery.DailyDischargeEnergy + ' Wh'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Container>
)}
</>
);
}
export default BatteryViewSodioHome;

View File

@ -169,7 +169,7 @@ function DetailedBatteryViewSodistore(
align="left"
sx={{ fontWeight: 'bold' }}
>
Total Battery Voltage
Bus Voltage
</TableCell>
<TableCell
align="right"
@ -183,6 +183,29 @@ function DetailedBatteryViewSodistore(
' V'}
</TableCell>
</TableRow>
<TableRow>
<TableCell
component="th"
scope="row"
align="left"
sx={{ fontWeight: 'bold' }}
>
Battery Voltage
</TableCell>
<TableCell
align="right"
sx={{
width: '6ch',
whiteSpace: 'nowrap',
paddingRight: '12px'
}}
>
{props.batteryData.BatteryDeligreenDataRecord
.TotalBatteryVoltage + ' V'}
</TableCell>
</TableRow>
<TableRow>
<TableCell
component="th"
@ -349,25 +372,6 @@ function DetailedBatteryViewSodistore(
{props.batteryData.BatteryDeligreenDataRecord.Soh + ' %'}
</TableCell>
</TableRow>
<TableRow>
<TableCell
component="th"
scope="row"
align="left"
sx={{ fontWeight: 'bold' }}
>
Port Voltage
</TableCell>
<TableCell
align="right"
sx={{
width: '6ch',
whiteSpace: 'nowrap',
paddingRight: '12px'
}}
></TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>

View File

@ -11,10 +11,13 @@ import {
InputLabel,
Modal,
Select,
Tab,
Tabs,
TextField,
Typography,
useTheme
} from '@mui/material';
import React, { useContext, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import Button from '@mui/material/Button';
@ -29,6 +32,7 @@ import { UserContext } from '../../../contexts/userContext';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
import { TimePicker } from '@mui/lab';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
interface ConfigurationProps {
values: JSONRecordData;
@ -40,6 +44,9 @@ function Configuration(props: ConfigurationProps) {
return null;
}
//console.log(props.values.Config);
const [activeTab, setActiveTab] = useState<'charge' | 'discharge'>('charge');
const CalibrationChargeOptions = [
'Repetitive Calibration',
'Additional Calibration',
@ -84,11 +91,12 @@ function Configuration(props: ConfigurationProps) {
const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
const context = useContext(UserContext);
const { currentUser, setUser } = context;
const { product, setProduct } = useContext(ProductIdContext);
const [formValues, setFormValues] = useState<ConfigurationValues>({
minimumSoC: props.values.Config.MinSoc,
gridSetPoint: (props.values.Config.GridSetPoint as number) / 1000,
CalibrationChargeState: CalibrationChargeOptionsController.indexOf(
calibrationChargeState: CalibrationChargeOptionsController.indexOf(
props.values.Config.ForceCalibrationChargeState.toString()
),
calibrationChargeDate:
@ -100,9 +108,27 @@ function Configuration(props: ConfigurationProps) {
.toDate()
: dayjs(props.values.Config.DayAndTimeForAdditionalCalibration)
// .add(localOffset, 'minute')
.toDate()
.toDate(),
...(product === 3 && {
calibrationDischargeState: CalibrationChargeOptionsController.indexOf(
props.values.Config.ForceCalibrationDischargeState.toString()
),
calibrationDischargeDate:
CalibrationChargeOptionsController.indexOf(
props.values.Config.ForceCalibrationDischargeState.toString()
) == 0
? dayjs(
props.values.Config.DownDayAndTimeForRepetitiveCalibration
).toDate()
: dayjs(
props.values.Config.DownDayAndTimeForAdditionalCalibration
).toDate()
})
});
// console.log(formValues);
const handleSubmit = async (e) => {
if (
CalibrationChargeOptionsController.indexOf(
@ -116,7 +142,7 @@ function Configuration(props: ConfigurationProps) {
setErrorDateModalOpen(true);
return;
} else if (
formValues.CalibrationChargeState === 1 &&
formValues.calibrationChargeState === 1 &&
dayjs(formValues.calibrationChargeDate).isBefore(dayjs())
) {
//console.log('asked for', dayjs(formValues.calibrationChargeDate));
@ -128,13 +154,17 @@ function Configuration(props: ConfigurationProps) {
const configurationToSend: ConfigurationValues = {
minimumSoC: formValues.minimumSoC,
gridSetPoint: formValues.gridSetPoint,
CalibrationChargeState: formValues.CalibrationChargeState,
calibrationChargeState: formValues.calibrationChargeState,
calibrationChargeDate: dayjs
.utc(formValues.calibrationChargeDate)
.add(localOffset, 'minute')
.toDate(),
calibrationDischargeState: formValues.calibrationDischargeState,
calibrationDischargeDate: dayjs
.utc(formValues.calibrationDischargeDate)
.add(localOffset, 'minute')
.toDate()
};
// console.log('will send ', dayjs(formValues.calibrationChargeDate));
setLoading(true);
const res = await axiosConfig
@ -169,6 +199,15 @@ function Configuration(props: ConfigurationProps) {
});
};
const handleConfirmDischarge = (newDate) => {
//console.log('non adapted day is ', newDate);
//console.log('adapted day is ', dayjs.utc(newDate).toDate());
setFormValues({
...formValues,
['calibrationDischargeDate']: dayjs(newDate).toDate()
});
};
const handleSelectedCalibrationChargeDay = (event) => {
const selectedDay = daysInWeek.indexOf(event.target.value);
const currentDate = dayjs();
@ -184,10 +223,25 @@ function Configuration(props: ConfigurationProps) {
});
};
const handleSelectedCalibrationDisChargeDay = (event) => {
const selectedDay = daysInWeek.indexOf(event.target.value);
const currentDate = dayjs();
let difference = selectedDay - currentDate.day();
if (difference < 0) {
difference += 7;
}
const adjustedDate = currentDate.add(difference, 'day');
setFormValues({
...formValues,
['calibrationDischargeDate']: adjustedDate.toDate()
});
};
const handleSelectedCalibrationChargeChange = (event) => {
setFormValues({
...formValues,
['CalibrationChargeState']: CalibrationChargeOptions.indexOf(
['calibrationChargeState']: CalibrationChargeOptions.indexOf(
event.target.value
),
['calibrationChargeDate']:
@ -201,6 +255,23 @@ function Configuration(props: ConfigurationProps) {
});
};
const handleSelectedCalibrationDisChargeChange = (event) => {
setFormValues({
...formValues,
['calibrationDischargeState']: CalibrationChargeOptions.indexOf(
event.target.value
),
['calibrationDischargeDate']:
CalibrationChargeOptions.indexOf(event.target.value) == 0
? dayjs(props.values.Config.DownDayAndTimeForRepetitiveCalibration)
// .add(localOffset, 'minute')
.toDate()
: dayjs(props.values.Config.DownDayAndTimeForAdditionalCalibration)
// .add(localOffset, 'minute')
.toDate()
});
};
const handleChange = (e) => {
const { name, value } = e.target;
@ -284,7 +355,84 @@ function Configuration(props: ConfigurationProps) {
</Box>
</Modal>
)}
<Grid item xs={12} md={12}>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
mt: 2,
mb: 2
}}
>
<Tabs
value={activeTab}
onChange={(e, newValue) => setActiveTab(newValue)}
textColor="inherit"
TabIndicatorProps={{ style: { display: 'none' } }} // hide default underline
sx={{
bgcolor: '#f5f5f5',
borderRadius: 2,
p: 0.5,
boxShadow: 1,
width: 500,
height: 47
}}
>
<Tab
value="charge"
label="Calibration Charge"
sx={(theme) => ({
flex: 2,
fontWeight: 'bold',
borderRadius: 2,
textTransform: 'none',
color:
activeTab === 'charge'
? 'white'
: theme.palette.text.primary,
bgcolor:
activeTab === 'charge'
? theme.palette.primary.main
: 'transparent',
'&:hover': {
bgcolor:
activeTab === 'charge'
? theme.palette.primary.dark
: '#eee'
}
})}
/>
{product === 3 && (
<Tab
value="discharge"
label="Calibration Discharge"
sx={(theme) => ({
flex: 2,
fontWeight: 'bold',
borderRadius: 2,
textTransform: 'none',
color:
activeTab === 'discharge'
? 'white'
: theme.palette.text.primary,
bgcolor:
activeTab === 'discharge'
? theme.palette.primary.main
: 'transparent',
'&:hover': {
bgcolor:
activeTab === 'discharge'
? theme.palette.primary.dark
: '#eee'
}
})}
/>
)}
</Tabs>
</Box>
<CardContent>
<Box
component="form"
@ -294,98 +442,34 @@ function Configuration(props: ConfigurationProps) {
noValidate
autoComplete="off"
>
<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 0-100%
</span>
) : (
''
)
}
fullWidth
/>
</div>
<div style={{ marginBottom: '5px', marginTop: '10px' }}>
<FormControl fullWidth sx={{ marginLeft: 1, width: 390 }}>
<InputLabel
sx={{
fontSize: 14,
backgroundColor: 'white'
}}
>
<FormattedMessage
id="forced_calibration_charge"
defaultMessage="Calibration Charge State"
/>
</InputLabel>
<Select
value={
CalibrationChargeOptions[
formValues.CalibrationChargeState
]
}
onChange={handleSelectedCalibrationChargeChange}
>
{CalibrationChargeOptions.map((option) => (
<MenuItem key={option} value={option}>
{option}
</MenuItem>
))}
</Select>
</FormControl>
</div>
{formValues.CalibrationChargeState == 1 && (
<div>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DateTimePicker
label="Select Next Calibration Charge Date"
value={dayjs(formValues.calibrationChargeDate)}
onChange={handleConfirm}
renderInput={(params) => (
<TextField
{...params}
sx={{
marginTop: 2, // Apply styles here
width: '100%' // Optional: You can adjust the width or other styling here
}}
/>
)}
/>
{/*<DateTimePicker*/}
{/* format="DD/MM/YYYY HH:mm"*/}
{/* ampm={false}*/}
{/* label="Select Next Calibration Charge Date"*/}
{/* value={dayjs(formValues.calibrationChargeDate)}*/}
{/* onChange={handleConfirm}*/}
{/* sx={{*/}
{/* marginTop: 2*/}
{/* }} // This should work with the correct imports*/}
{/*/>*/}
</LocalizationProvider>
</div>
)}
{formValues.CalibrationChargeState == 0 && (
{activeTab === 'charge' && (
<>
<div style={{ marginBottom: '5px' }}>
<FormControl
<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 0-100%
</span>
) : (
''
)
}
fullWidth
sx={{ marginLeft: 1, width: 390, marginTop: 2 }}
>
/>
</div>
<div style={{ marginBottom: '5px', marginTop: '10px' }}>
<FormControl fullWidth sx={{ marginLeft: 1, width: 390 }}>
<InputLabel
sx={{
fontSize: 14,
@ -393,106 +477,283 @@ function Configuration(props: ConfigurationProps) {
}}
>
<FormattedMessage
id="calibration_charge_day"
defaultMessage="Calibration Charge Day"
id="forced_calibration_charge"
defaultMessage="Calibration Charge State"
/>
</InputLabel>
<Select
value={
daysInWeek[formValues.calibrationChargeDate.getDay()]
CalibrationChargeOptions[
formValues.calibrationChargeState
]
}
onChange={handleSelectedCalibrationChargeDay}
onChange={handleSelectedCalibrationChargeChange}
>
{daysInWeek.map((day) => (
<MenuItem key={day} value={day}>
{day}
{CalibrationChargeOptions.map((option) => (
<MenuItem key={option} value={option}>
{option}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div style={{ marginBottom: '5px', marginTop: '10px' }}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<TimePicker
ampm={false}
label="Calibration Charge Hour"
value={dayjs(formValues.calibrationChargeDate)}
onChange={(newTime) => handleConfirm(dayjs(newTime))}
renderInput={(params) => (
<TextField
{...params}
{formValues.calibrationChargeState == 1 && (
<div>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DateTimePicker
label="Select Next Calibration Charge Date"
value={dayjs(formValues.calibrationChargeDate)}
onChange={handleConfirm}
renderInput={(params) => (
<TextField
{...params}
sx={{
marginTop: 2, // Apply styles here
width: '100%' // Optional: You can adjust the width or other styling here
}}
/>
)}
/>
</LocalizationProvider>
</div>
)}
{formValues.calibrationChargeState == 0 && (
<>
<div style={{ marginBottom: '5px' }}>
<FormControl
fullWidth
sx={{ marginLeft: 1, width: 390, marginTop: 2 }}
>
<InputLabel
sx={{
marginTop: 2, // Apply styles here
width: '100%' // Optional: You can adjust the width or other styling here
fontSize: 14,
backgroundColor: 'white'
}}
>
<FormattedMessage
id="calibration_charge_day"
defaultMessage="Calibration Charge Day"
/>
</InputLabel>
<Select
value={
daysInWeek[
formValues.calibrationChargeDate.getDay()
]
}
onChange={handleSelectedCalibrationChargeDay}
>
{daysInWeek.map((day) => (
<MenuItem key={day} value={day}>
{day}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div style={{ marginBottom: '5px', marginTop: '10px' }}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<TimePicker
ampm={false}
label="Calibration Charge Hour"
value={dayjs(formValues.calibrationChargeDate)}
onChange={(newTime) =>
handleConfirm(dayjs(newTime))
}
renderInput={(params) => (
<TextField
{...params}
sx={{
marginTop: 2, // Apply styles here
width: '100%' // Optional: You can adjust the width or other styling here
}}
/>
)}
/>
)}
/>
</LocalizationProvider>
</LocalizationProvider>
</div>
</>
)}
<div style={{ marginBottom: '5px' }}>
<TextField
label={
<FormattedMessage
id="grid_set_point"
defaultMessage="Grid Set Point (kW)"
/>
}
name="gridSetPoint"
value={formValues.gridSetPoint}
onChange={handleChange}
helperText={
errors.gridSetPoint ? (
<span style={{ color: 'red' }}>
Please provide a valid number
</span>
) : (
''
)
}
fullWidth
/>
</div>
<div style={{ marginBottom: '5px' }}>
<TextField
label={
<FormattedMessage
id="Installed_Power_DC1010"
defaultMessage="Installed Power DC1010 (kW)"
/>
}
value={
(props.values.DcDc.SystemControl
.NumberOfConnectedSlaves as number) * 10
}
fullWidth
/>
</div>
{props.values.Config.MaxBatteryDischargingCurrent && (
<div style={{ marginBottom: '5px' }}>
<TextField
label={
<FormattedMessage
id="Maximum_Discharge_Power"
defaultMessage="Maximum Discharge Power (W)"
/>
}
value={
(props.values.Config
.MaxBatteryDischargingCurrent as number) *
48 *
(props.values.DcDc.SystemControl
.NumberOfConnectedSlaves as number)
}
fullWidth
/>
</div>
)}
</>
)}
<div style={{ marginBottom: '5px' }}>
<TextField
label={
<FormattedMessage
id="grid_set_point"
defaultMessage="Grid Set Point (kW)"
/>
}
name="gridSetPoint"
value={formValues.gridSetPoint}
onChange={handleChange}
helperText={
errors.gridSetPoint ? (
<span style={{ color: 'red' }}>
Please provide a valid number
</span>
) : (
''
)
}
fullWidth
/>
</div>
<div style={{ marginBottom: '5px' }}>
<TextField
label={
<FormattedMessage
id="Installed_Power_DC1010"
defaultMessage="Installed Power DC1010 (kW)"
/>
}
value={
(props.values.DcDc.SystemControl
.NumberOfConnectedSlaves as number) * 10
}
fullWidth
/>
</div>
{product == 3 && activeTab === 'discharge' && (
<>
<div style={{ marginBottom: '5px', marginTop: '10px' }}>
<FormControl fullWidth sx={{ marginLeft: 1, width: 390 }}>
<InputLabel
sx={{
fontSize: 14,
backgroundColor: 'white'
}}
>
<FormattedMessage
id="calibration_discharge_state"
defaultMessage="Calibration Discharge State"
/>
</InputLabel>
<Select
value={
CalibrationChargeOptions[
formValues.calibrationDischargeState
]
}
onChange={handleSelectedCalibrationDisChargeChange}
>
{CalibrationChargeOptions.map((option) => (
<MenuItem key={option} value={option}>
{option}
</MenuItem>
))}
</Select>
</FormControl>
</div>
{props.values.Config.MaxBatteryDischargingCurrent && (
<div style={{ marginBottom: '5px' }}>
<TextField
label={
<FormattedMessage
id="Maximum_Discharge_Power"
defaultMessage="Maximum Discharge Power (W)"
/>
}
value={
(props.values.Config
.MaxBatteryDischargingCurrent as number) *
48 *
(props.values.DcDc.SystemControl
.NumberOfConnectedSlaves as number)
}
fullWidth
/>
</div>
{formValues.calibrationDischargeState == 1 && (
<div>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DateTimePicker
label="Select Next Calibration Discharge Date"
value={dayjs(formValues.calibrationDischargeDate)}
onChange={handleConfirmDischarge}
renderInput={(params) => (
<TextField
{...params}
sx={{
marginTop: 2, // Apply styles here
width: '100%' // Optional: You can adjust the width or other styling here
}}
/>
)}
/>
</LocalizationProvider>
</div>
)}
{formValues.calibrationDischargeState == 0 && (
<>
<div style={{ marginBottom: '5px' }}>
<FormControl
fullWidth
sx={{ marginLeft: 1, width: 390, marginTop: 2 }}
>
<InputLabel
sx={{
fontSize: 14,
backgroundColor: 'white'
}}
>
<FormattedMessage
id="calibration_charge_day"
defaultMessage="Calibration Discharge Day"
/>
</InputLabel>
<Select
value={
daysInWeek[
formValues.calibrationDischargeDate.getDay()
]
}
onChange={handleSelectedCalibrationDisChargeDay}
>
{daysInWeek.map((day) => (
<MenuItem key={day} value={day}>
{day}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div style={{ marginBottom: '5px', marginTop: '10px' }}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<TimePicker
ampm={false}
label="Calibration Discharge Hour"
value={dayjs(formValues.calibrationDischargeDate)}
onChange={(newTime) =>
handleConfirmDischarge(dayjs(newTime))
}
renderInput={(params) => (
<TextField
{...params}
sx={{
marginTop: 2, // Apply styles here
width: '100%' // Optional: You can adjust the width or other styling here
}}
/>
)}
/>
</LocalizationProvider>
</div>
</>
)}
</>
)}
{/*<div>*/}
{/* <TextField*/}
{/* label={*/}

View File

@ -20,6 +20,7 @@ import { I_Installation } from '../../../interfaces/InstallationTypes';
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
import { UserContext } from '../../../contexts/userContext';
import { UserType } from '../../../interfaces/UserTypes';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
interface InformationProps {
values: I_Installation;
@ -38,6 +39,7 @@ function Information(props: InformationProps) {
const [formValues, setFormValues] = useState(props.values);
const requiredFields = ['name', 'region', 'location', 'country'];
const installationContext = useContext(InstallationsContext);
const { product, setProduct } = useContext(ProductIdContext);
const {
updateInstallation,
loading,
@ -265,6 +267,30 @@ function Information(props: InformationProps) {
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="information"
defaultMessage="Information"
/>
}
name="information"
value={formValues.information}
onChange={handleChange}
variant="outlined"
fullWidth
multiline
minRows={6} // 👈 Makes it visually bigger
maxRows={12} // 👈 Optional max height before scroll
inputProps={{
style: {
fontFamily: 'monospace' // optional: makes tabs/formatting more clear
}
}}
/>
</div>
{currentUser.userType == UserType.admin && (
<>
<div>
@ -282,8 +308,11 @@ function Information(props: InformationProps) {
label="S3 Bucket Name"
name="s3bucketname"
value={
formValues.s3BucketId +
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
product === 0 || product == 3
? formValues.s3BucketId +
'-3e5b3069-214a-43ee-8d85-57d72000c19d'
: formValues.s3BucketId +
'-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa'
}
variant="outlined"
fullWidth

View File

@ -1,393 +0,0 @@
import {
Alert,
Box,
CardContent,
CircularProgress,
Container,
Grid,
IconButton,
Modal,
TextField,
Typography,
useTheme
} from '@mui/material';
import { FormattedMessage } from 'react-intl';
import Button from '@mui/material/Button';
import { Close as CloseIcon } from '@mui/icons-material';
import React, { useContext, useState } from 'react';
import { I_Installation } from '../../../interfaces/InstallationTypes';
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
import { UserContext } from '../../../contexts/userContext';
import routes from '../../../Resources/routes.json';
import { useNavigate } from 'react-router-dom';
import { UserType } from '../../../interfaces/UserTypes';
interface InformationSodioHomeProps {
values: I_Installation;
type?: string;
}
function InformationSodioHome(props: InformationSodioHomeProps) {
if (props.values === null) {
return null;
}
const context = useContext(UserContext);
const { currentUser } = context;
const theme = useTheme();
const [formValues, setFormValues] = useState(props.values);
const requiredFields = ['name', 'region', 'location', 'country'];
const [openModalDeleteInstallation, setOpenModalDeleteInstallation] =
useState(false);
const navigate = useNavigate();
const DeviceTypes = ['Cerbo', 'Venus'];
const installationContext = useContext(InstallationsContext);
const {
updateInstallation,
deleteInstallation,
loading,
setLoading,
error,
setError,
updated,
setUpdated
} = installationContext;
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({
...formValues,
[name]: value
});
};
const handleSubmit = () => {
setLoading(true);
setError(false);
updateInstallation(formValues, props.type);
};
const handleDelete = () => {
setLoading(true);
setError(false);
setOpenModalDeleteInstallation(true);
};
const deleteInstallationModalHandle = () => {
setOpenModalDeleteInstallation(false);
deleteInstallation(formValues, props.type);
setLoading(false);
navigate(routes.salidomo_installations + routes.list, {
replace: true
});
};
const deleteInstallationModalHandleCancel = () => {
setOpenModalDeleteInstallation(false);
setLoading(false);
};
const areRequiredFieldsFilled = () => {
for (const field of requiredFields) {
if (!formValues[field]) {
return false;
}
}
return true;
};
return (
<>
{openModalDeleteInstallation && (
<Modal
open={openModalDeleteInstallation}
onClose={deleteInstallationModalHandleCancel}
aria-labelledby="error-modal"
aria-describedby="error-modal-description"
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 350,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<Typography
variant="body1"
gutterBottom
sx={{ fontWeight: 'bold' }}
>
Do you want to delete this installation?
</Typography>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
<Button
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={deleteInstallationModalHandle}
>
Delete
</Button>
<Button
sx={{
marginTop: 2,
marginLeft: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={deleteInstallationModalHandleCancel}
>
Cancel
</Button>
</div>
</Box>
</Modal>
)}
<Container maxWidth="xl">
<Grid
container
direction="row"
justifyContent="center"
alignItems="stretch"
spacing={3}
>
<Grid item xs={12} md={12}>
<CardContent>
<Box
component="form"
sx={{
'& .MuiTextField-root': { m: 1, width: '50ch' }
}}
noValidate
autoComplete="off"
>
<div>
<TextField
label={
<FormattedMessage
id="installation_name"
defaultMessage="Installation Name"
/>
}
name="name"
value={formValues.name}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div>
<TextField
label={
<FormattedMessage id="region" defaultMessage="Region" />
}
name="region"
value={formValues.region}
onChange={handleChange}
variant="outlined"
fullWidth
required
error={formValues.region === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="location"
defaultMessage="Location"
/>
}
name="location"
value={formValues.location}
onChange={handleChange}
variant="outlined"
fullWidth
required
error={formValues.location === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage id="country" defaultMessage="Country" />
}
name="country"
value={formValues.country}
onChange={handleChange}
variant="outlined"
fullWidth
required
error={formValues.country === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="information"
defaultMessage="Information"
/>
}
name="information"
value={formValues.information}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
{currentUser.userType == UserType.admin && (
<>
<div>
<TextField
label="BitWatt Cloud Access Key"
name="s3WriteKey"
value={formValues.s3WriteKey}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div>
<TextField
label="BitWatt Cloud Secret Key"
name="s3WriteSecret"
onChange={handleChange}
value={formValues.s3WriteSecret}
variant="outlined"
fullWidth
/>
</div>
</>
)}
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
<Button
variant="contained"
onClick={handleSubmit}
sx={{
marginLeft: '10px'
}}
disabled={!areRequiredFieldsFilled()}
>
<FormattedMessage
id="applyChanges"
defaultMessage="Apply Changes"
/>
</Button>
{currentUser.userType == UserType.admin && (
<Button
variant="contained"
onClick={handleDelete}
sx={{
marginLeft: '10px'
}}
>
<FormattedMessage
id="deleteInstallation"
defaultMessage="Delete Installation"
/>
</Button>
)}
{loading && (
<CircularProgress
sx={{
color: theme.palette.primary.main,
marginLeft: '20px'
}}
/>
)}
{error && (
<Alert
severity="error"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center'
}}
>
<FormattedMessage
id="errorOccured"
defaultMessage="An error has occurred"
/>
<IconButton
color="inherit"
size="small"
onClick={() => setError(false)}
sx={{ marginLeft: '4px' }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
{updated && (
<Alert
severity="success"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center'
}}
>
<FormattedMessage
id="successfullyUpdated"
defaultMessage="Successfully updated"
/>
<IconButton
color="inherit"
size="small"
onClick={() => setUpdated(false)} // Set error state to false on click
sx={{ marginLeft: '4px' }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
</div>
</Box>
</CardContent>
</Grid>
</Grid>
</Container>
</>
);
}
export default InformationSodioHome;

View File

@ -135,7 +135,6 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
}, []);
const handleSelectOneInstallation = (installationID: number): void => {
console.log('when selecting installation', product);
if (selectedInstallation != installationID) {
setSelectedInstallation(installationID);
setSelectedInstallation(-1);

View File

@ -19,11 +19,14 @@ interface installationFormProps {
cancel: () => void;
submit: () => void;
parentid: number;
productToInsert: number;
}
function installationForm(props: installationFormProps) {
const theme = useTheme();
const [open, setOpen] = useState(true);
console.log('productToInsert IS ', props.productToInsert);
const [formValues, setFormValues] = useState<Partial<I_Installation>>({
installationName: '',
name: '',
@ -59,7 +62,7 @@ function installationForm(props: installationFormProps) {
const handleSubmit = async (e) => {
setLoading(true);
formValues.parentId = props.parentid;
formValues.product = 0;
formValues.product = props.productToInsert;
const responseData = await createInstallation(formValues);
props.submit();
};

View File

@ -216,6 +216,20 @@ export interface Line {
Power: Power;
}
export interface SodioHomeBattery {
AccumulatedChargeEnergy: number;
AccumulatedDischargeEnergy: number;
Current: number;
DailyChargeEnergy: number;
DailyDischargeEnergy: number;
MaxAllowableChargePower: number;
MaxAllowableDischargePower: number;
Power: number;
Soc: number;
Soh: number;
Voltage: number;
}
// The interface for the Battery structure, with dynamic keys (Device IDs)
export interface JSONRecordData {
Battery: {
@ -282,9 +296,12 @@ export interface JSONRecordData {
CurtailP: number;
DayAndTimeForAdditionalCalibration: string;
DayAndTimeForRepetitiveCalibration: string;
DownDayAndTimeForAdditionalCalibration: string;
DownDayAndTimeForRepetitiveCalibration: string;
DisplayIndividualBatteries: string;
MaxBatteryDischargingCurrent: number;
ForceCalibrationChargeState: string;
ForceCalibrationDischargeState: string;
GridSetPoint: number;
HoldSocZone: number;
MinSoc: number;
@ -392,17 +409,101 @@ export interface JSONRecordData {
LoadOnDc: { Power: number };
PvOnDc: {
DcWh: number;
NbrOfStrings: number;
Dc: {
Voltage: number;
Current: number;
Power: number;
};
Strings: {
[PvId: string]: PvString;
[deviceId: string]: {
DcWh: number;
Dc: {
Voltage: number;
Current: number;
Power: number;
NbrOfStrings: number;
};
Strings: {
[PvId: string]: PvString;
};
};
};
AcDcGrowatt: {
AcChargeEnable: number;
ActivePowerPercentDerating: number;
ActualChargeDischargePowerControlValue: number;
AlarmMainCode: number;
AlarmSubCode: number;
BatteriesRecords: {
AverageSoc: number;
AverageSoh: number;
Batteries: { [deviceId: string]: SodioHomeBattery };
LowestSoc: number;
Power: number;
TotalChargeEnergy: number;
TotalDischargeEnergy: number;
TotalMaxCharge: number;
TotalMaxDischarge: number;
};
BatteryChargeCutoffVoltage: number;
BatteryClusterIndex: number;
BatteryDischargeCutoffVoltage: number;
BatteryMaxChargeCurrent: number;
BatteryMaxChargePower: number;
BatteryMaxDischargePower: number;
BatteryMaxdischargeCurrent: number;
BatteryOperatingMode: string;
BatteryType: number;
ChargeCutoffSoc: number;
ControlPermession: number;
DischargeCutoffSoc: number;
EmsCommunicationFailureTime: number;
EnableCommand: number;
EnableEmsCommunicationFailureTime: number;
EnableSyn: string;
EnergyToGrid: number;
EnergyToUser: number;
FaultMainCode: number;
FaultSubCode: number;
Frequency: number;
GridAbLineVoltage: number;
GridBcLineVoltage: number;
GridCaLineVoltage: number;
InverterActivePower: number;
InverterReactivePower: number;
InverterTemperature: number;
LoadPriorityDischargeCutoffSoc: number;
MaxActivePower: number;
MeterPower: number;
OffGridDischargeCutoffSoc: number;
OperatingPriority: string;
PhaseACurrent: number;
PhaseBCurrent: number;
PhaseCCurrent: number;
PowerFactor: number;
Pv1Current: number;
Pv1Voltage: number;
Pv2Current: number;
Pv2Voltage: number;
PvInputMaxPower: number;
RatedPower: number;
RemoteChargDischargePower: number;
RemotePowerControl: number;
SystemDateTime: string;
SystemOperatingMode: string;
TotalEnergyToGrid: number;
TotalEnergyToUser: number;
VppProtocolVerNumber: number;
};
// DcWh: number;
// // NbrOfStrings: number;
// Dc: { [deviceId: string]: Dc };
// // Dc: {
// // Voltage: number;
// // Current: number;
// // Power: number;
// // };
// Strings: {
// [PvId: string]: PvString;
// };
// };
}
export const parseChunkJson = (
@ -497,8 +598,10 @@ export interface I_BoxDataValue {
export type ConfigurationValues = {
minimumSoC: string | number;
gridSetPoint: number;
CalibrationChargeState: number;
calibrationChargeState: number;
calibrationChargeDate: Date | null;
calibrationDischargeState: number;
calibrationDischargeDate: Date | null;
};
//
// export interface Pv {

View File

@ -0,0 +1,499 @@
import React, {
Fragment,
useCallback,
useContext,
useEffect,
useState
} from 'react';
import {
Alert,
Box,
Container,
Divider,
FormControl,
Grid,
IconButton,
InputLabel,
ListItem,
MenuItem,
Modal,
Select,
useTheme
} from '@mui/material';
import { TokenContext } from 'src/contexts/tokenContext';
import { UserContext } from 'src/contexts/userContext';
import ListItemAvatar from '@mui/material/ListItemAvatar';
import Avatar from '@mui/material/Avatar';
import ListItemText from '@mui/material/ListItemText';
import PersonIcon from '@mui/icons-material/Person';
import Button from '@mui/material/Button';
import { Close as CloseIcon } from '@mui/icons-material';
import { AccessContext } from 'src/contexts/AccessContextProvider';
import { FormattedMessage } from 'react-intl';
import { InnovEnergyUser, UserType } from '../../../interfaces/UserTypes';
import PersonRemoveIcon from '@mui/icons-material/PersonRemove';
import {
I_Folder,
I_Installation
} from '../../../interfaces/InstallationTypes';
import axiosConfig from '../../../Resources/axiosConfig';
interface UserAccessProps {
current_user: InnovEnergyUser;
}
function UserAccess(props: UserAccessProps) {
if (props.current_user == undefined) {
return null;
}
const theme = useTheme();
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
const context = useContext(UserContext);
const { currentUser, setUser } = context;
const [openFolder, setOpenFolder] = useState(false);
const [openInstallation, setOpenInstallation] = useState(false);
const [openModal, setOpenModal] = useState(false);
const [selectedFolderNames, setSelectedFolderNames] = useState<string[]>([]);
const [selectedInstallationNames, setSelectedInstallationNames] = useState<
string[]
>([]);
const [folders, setFolders] = useState<I_Folder[]>([]);
const [installations, setInstallations] = useState<I_Installation[]>([]);
const accessContext = useContext(AccessContext);
const {
fetchInstallationsForUser,
accessibleInstallationsForUser,
error,
setError,
updated,
setUpdated,
updatedmessage,
errormessage,
setErrorMessage,
setUpdatedMessage,
RevokeAccessFromResource
} = accessContext;
const fetchFolders = useCallback(async () => {
return axiosConfig
.get('/GetAllFolders')
.then((res) => {
setFolders(res.data);
})
.catch((err) => {
if (err.response && err.response.status == 401) {
removeToken();
}
});
}, [setFolders]);
const fetchInstallations = useCallback(async () => {
try {
// fetch product 0
const res0 = await axiosConfig.get(
`/GetAllInstallationsFromProduct?product=0`
);
const installations0 = res0.data;
// fetch product 1
const res1 = await axiosConfig.get(
`/GetAllInstallationsFromProduct?product=3`
);
const installations1 = res1.data;
// aggregate
const combined = [...installations0, ...installations1];
// update
setInstallations(combined);
} catch (err) {
if (err.response && err.response.status === 401) {
removeToken();
}
} finally {
}
}, [setInstallations]);
useEffect(() => {
fetchInstallationsForUser(props.current_user.id);
}, [props.current_user]);
const handleGrantAccess = () => {
fetchFolders();
fetchInstallations();
setOpenModal(true);
setSelectedFolderNames([]);
setSelectedInstallationNames([]);
};
const handleFolderChange = (event) => {
setSelectedFolderNames(event.target.value);
};
const handleInstallationChange = (event) => {
setSelectedInstallationNames(event.target.value);
};
const handleOpenFolder = () => {
setOpenFolder(true);
};
const handleCloseFolder = () => {
setOpenFolder(false);
};
const handleCancel = () => {
setOpenModal(false);
};
const handleOpenInstallation = () => {
setOpenInstallation(true);
};
const handleCloseInstallation = () => {
setOpenInstallation(false);
};
const handleSubmit = async () => {
for (const folderName of selectedFolderNames) {
const folder = folders.find((folder) => folder.name === folderName);
await axiosConfig
.post(
`/GrantUserAccessToFolder?UserId=${props.current_user.id}&FolderId=${folder.id}`
)
.then((response) => {
if (response) {
setUpdatedMessage(
'Granted access to user ' + props.current_user.name
);
setUpdated(true);
}
})
.catch((err) => {
setErrorMessage('An error has occured');
setError(true);
});
}
for (const installationName of selectedInstallationNames) {
const installation = installations.find(
(installation) => installation.name === installationName
);
await axiosConfig
.post(
`/GrantUserAccessToInstallation?UserId=${props.current_user.id}&InstallationId=${installation.id}`
)
.then((response) => {
if (response) {
setUpdatedMessage(
'Granted access to user ' + props.current_user.name
);
setUpdated(true);
}
})
.catch((err) => {
setErrorMessage('An error has occured');
setError(true);
});
}
setOpenModal(false);
fetchInstallationsForUser(props.current_user.id);
};
return (
<Container maxWidth="xl">
<Grid container>
<Grid item xs={12} md={12}>
{updated && (
<Alert
severity="success"
sx={{
mt: 1
}}
>
{updatedmessage}
<IconButton
color="inherit"
size="small"
onClick={() => setUpdated(false)}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
{error && (
<Alert
severity="error"
sx={{
marginTop: '20px',
marginBottom: '20px'
}}
>
{errormessage}
<IconButton
color="inherit"
size="small"
onClick={() => setError(false)}
sx={{
marginLeft: '10px'
}}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
<Modal
open={openModal}
onClose={() => {}}
aria-labelledby="error-modal"
aria-describedby="error-modal-description"
>
<Box
sx={{
position: 'absolute',
top: '30%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 500,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4
}}
>
<Box
component="form"
sx={{
textAlign: 'center'
}}
noValidate
autoComplete="off"
>
<div>
<FormControl fullWidth sx={{ marginTop: 1, width: 390 }}>
<InputLabel
sx={{
fontSize: 14,
backgroundColor: 'white'
}}
>
<FormattedMessage
id="grantAccessToFolders"
defaultMessage="Grant access to folders"
/>
</InputLabel>
<Select
multiple
value={selectedFolderNames}
onChange={handleFolderChange}
open={openFolder}
onClose={handleCloseFolder}
onOpen={handleOpenFolder}
renderValue={(selected) => (
<div>
{selected.map((folder) => (
<span key={folder}>{folder}, </span>
))}
</div>
)}
>
{folders.map((folder) => (
<MenuItem key={folder.id} value={folder.name}>
{folder.name}
</MenuItem>
))}
<Button
sx={{
marginLeft: '150px',
marginTop: '10px',
backgroundColor: theme.colors.primary.main,
color: 'white',
'&:hover': {
backgroundColor: theme.colors.primary.dark
},
padding: '6px 8px'
}}
onClick={handleCloseFolder}
>
<FormattedMessage id="submit" defaultMessage="Submit" />
</Button>
</Select>
</FormControl>
</div>
<div>
<FormControl fullWidth sx={{ marginTop: 2, width: 390 }}>
<InputLabel
sx={{
fontSize: 14,
backgroundColor: 'white'
}}
>
<FormattedMessage
id="grantAccessToInstallations"
defaultMessage="Grant access to installations"
/>
</InputLabel>
<Select
multiple
value={selectedInstallationNames}
onChange={handleInstallationChange}
open={openInstallation}
onClose={handleCloseInstallation}
onOpen={handleOpenInstallation}
renderValue={(selected) => (
<div>
{selected.map((installation) => (
<span key={installation}>{installation}, </span>
))}
</div>
)}
>
{installations.map((installation) => (
<MenuItem
key={installation.id}
value={installation.name}
>
{installation.name}
</MenuItem>
))}
<Button
sx={{
marginLeft: '150px',
marginTop: '10px',
backgroundColor: theme.colors.primary.main,
color: 'white',
'&:hover': {
backgroundColor: theme.colors.primary.dark
},
padding: '6px 8px'
}}
onClick={handleCloseInstallation}
>
<FormattedMessage id="submit" defaultMessage="Submit" />
</Button>
</Select>
</FormControl>
</div>
<Button
sx={{
marginTop: '20px',
backgroundColor: theme.colors.primary.main,
color: 'white',
'&:hover': {
backgroundColor: theme.colors.primary.dark
},
padding: '6px 8px'
}}
onClick={handleSubmit}
>
<FormattedMessage id="submit" defaultMessage="Submit" />
</Button>
<Button
variant="contained"
onClick={handleCancel}
sx={{
marginTop: '20px',
marginLeft: '10px',
backgroundColor: theme.colors.primary.main,
color: 'white',
'&:hover': {
backgroundColor: theme.colors.primary.dark
},
padding: '6px 8px'
}}
>
<FormattedMessage id="cancel" defaultMessage="Cancel" />
</Button>
</Box>
</Box>
</Modal>
<Button
variant="contained"
onClick={handleGrantAccess}
sx={{
marginTop: '20px',
marginBottom: '20px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="grantAccess" defaultMessage="Grant Access" />
</Button>
</Grid>
<Grid item xs={12} md={12}>
{accessibleInstallationsForUser.map((installation, index) => {
const isLast = index === accessibleInstallationsForUser.length - 1;
return (
<Fragment key={installation.name}>
<ListItem
sx={{
mb: isLast ? 4 : 0 // Apply margin-bottom to the last item only
}}
secondaryAction={
currentUser.userType === UserType.admin && (
<IconButton
onClick={() => {
RevokeAccessFromResource(
'ToInstallation',
props.current_user.id,
'InstallationId',
installation.id,
props.current_user.name
);
fetchInstallationsForUser(props.current_user.id);
}}
edge="end"
>
<PersonRemoveIcon />
</IconButton>
)
}
>
<ListItemAvatar>
<Avatar>
<PersonIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={installation.name} />
</ListItem>
<Divider />
</Fragment>
);
})}
{accessibleInstallationsForUser.length == 0 && (
<Alert
severity="error"
sx={{
display: 'flex',
alignItems: 'center',
marginTop: '20px',
marginBottom: '20px'
}}
>
<FormattedMessage
id="theUserDoesNOtHaveAccessToAnyInstallation"
defaultMessage="The user does not have access to any installation "
/>
<IconButton color="inherit" size="small"></IconButton>
</Alert>
)}
</Grid>
</Grid>
</Container>
);
}
export default UserAccess;

View File

@ -24,34 +24,37 @@ function PvView(props: PvViewProps) {
if (props.values === null && props.connected == true) {
return null;
}
const currentLocation = useLocation();
const navigate = useNavigate();
const sortedPvView =
props.values != null && props.values.PvOnDc
? Object.entries(props.values.PvOnDc.Strings)
.map(([pvId, pv]) => {
return { pvId, pv }; // Here we return an object with the id and device
})
.sort((a, b) => parseInt(b.pvId) - parseInt(a.pvId))
: [];
// ✅ Flatten, sort, and assign unique displayId from 1-N
const sortedPvView = props.values?.PvOnDc
? Object.entries(props.values.PvOnDc)
.flatMap(([deviceId, device]) =>
Object.entries(device.Strings).map(([pvId, pv], index) => ({
pvId,
pv,
deviceId,
displayId: `CU${deviceId} -> AMPT ${index + 1}`
}))
)
.sort((a, b) => {
if (a.deviceId === b.deviceId) {
return parseInt(a.pvId) - parseInt(b.pvId);
}
return a.deviceId.localeCompare(b.deviceId);
})
: [];
const [loading, setLoading] = useState(sortedPvView.length == 0);
const [loading, setLoading] = useState(sortedPvView.length === 0);
const handleMainStatsButton = () => {
navigate(routes.mainstats);
};
// const findBatteryData = (batteryId: number) => {
// for (let i = 0; i < props.values.batteryView.length; i++) {
// if (props.values.batteryView[i].BatteryId == batteryId) {
// return props.values.batteryView[i];
// }
// }
// };
useEffect(() => {
if (sortedPvView.length == 0) {
if (sortedPvView.length === 0) {
setLoading(true);
} else {
setLoading(false);
@ -84,6 +87,7 @@ function PvView(props: PvViewProps) {
</Typography>
</Container>
)}
{loading && props.connected && (
<Container
maxWidth="xl"
@ -111,80 +115,72 @@ function PvView(props: PvViewProps) {
{!loading && props.connected && (
<Container maxWidth="xl">
<TableContainer
component={Paper}
sx={{
marginTop: '20px',
marginBottom: '20px'
}}
>
<Table sx={{ minWidth: 250 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell align="center">Pv</TableCell>
<TableCell align="center">Power</TableCell>
<TableCell align="center">Voltage</TableCell>
<TableCell align="center">Current</TableCell>
</TableRow>
</TableHead>
<TableBody>
{sortedPvView.map(({ pvId, pv }) => (
<TableRow
key={pvId}
style={{
height: '10px'
}}
>
<TableCell
component="th"
scope="row"
align="center"
sx={{ width: '10%', fontWeight: 'bold', color: 'black' }}
>
{'AMPT ' + pvId}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center',
fontWeight: 'bold',
backgroundColor:
pv.Current == 0 ? '#FF033E' : '#32CD32',
color: 'inherit'
}}
>
{pv.Power + ' W'}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center',
fontWeight: 'bold',
backgroundColor:
pv.Current == 0 ? '#FF033E' : '#32CD32',
color: 'inherit'
}}
>
{pv.Voltage + ' V'}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center',
fontWeight: 'bold',
backgroundColor:
pv.Current == 0 ? '#FF033E' : '#32CD32',
color: 'inherit'
}}
>
{pv.Current + ' A'}
</TableCell>
{Object.entries(
sortedPvView.reduce((acc, entry) => {
if (!acc[entry.deviceId]) acc[entry.deviceId] = [];
acc[entry.deviceId].push(entry);
return acc;
}, {} as Record<string, typeof sortedPvView>)
).map(([deviceId, entries]) => (
<TableContainer
key={deviceId}
component={Paper}
sx={{ marginTop: '30px', marginBottom: '40px', boxShadow: 3 }}
>
<Table sx={{ minWidth: 250 }} aria-label={`CU${deviceId} table`}>
<TableHead>
<TableRow>
<TableCell align="center">Pv</TableCell>
<TableCell align="center">Power</TableCell>
<TableCell align="center">Voltage</TableCell>
<TableCell align="center">Current</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</TableHead>
<TableBody>
{entries.map(({ displayId, pv }, index) => (
<TableRow key={index}>
<TableCell
align="center"
sx={{ fontWeight: 'bold', color: 'black' }}
>
{displayId}
</TableCell>
<TableCell
align="center"
sx={{
fontWeight: 'bold',
backgroundColor:
pv.Current === 0 ? '#FF033E' : '#32CD32'
}}
>
{pv.Power} W
</TableCell>
<TableCell
align="center"
sx={{
fontWeight: 'bold',
backgroundColor:
pv.Current === 0 ? '#FF033E' : '#32CD32'
}}
>
{pv.Voltage} V
</TableCell>
<TableCell
align="center"
sx={{
fontWeight: 'bold',
backgroundColor:
pv.Current === 0 ? '#FF033E' : '#32CD32'
}}
>
{pv.Current} A
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
))}
</Container>
)}
</>

View File

@ -55,7 +55,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
routes.installation +
`${installationID}` +
'/' +
routes.live,
routes.batteryview,
{
replace: true
}

View File

@ -19,8 +19,11 @@ import HistoryOfActions from '../History/History';
import BuildIcon from '@mui/icons-material/Build';
import AccessContextProvider from '../../../contexts/AccessContextProvider';
import Access from '../ManageAccess/Access';
import InformationSodioHome from '../Information/InformationSodioHome';
import CryptoJS from 'crypto-js';
import Information from '../Information/Information';
import { TimeSpan, UnixTime } from '../../../dataCache/time';
import { fetchDataJson } from '../Installations/fetchData';
import { FetchResult } from '../../../dataCache/dataCache';
import BatteryViewSodioHome from '../BatteryView/BatteryViewSodioHome';
interface singleInstallationProps {
current_installation?: I_Installation;
@ -31,6 +34,19 @@ function SodioHomeInstallation(props: singleInstallationProps) {
if (props.current_installation == undefined) {
return null;
}
const S3data = {
s3Region: props.current_installation.s3Region,
s3Provider: props.current_installation.s3Provider,
s3Key: props.current_installation.s3Key,
s3Secret: props.current_installation.s3Secret,
s3BucketId: props.current_installation.s3BucketId
};
const s3Bucket =
props.current_installation.s3BucketId.toString() +
'-' +
'e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa';
const context = useContext(UserContext);
const { currentUser } = context;
const location = useLocation().pathname;
@ -45,6 +61,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
const [connected, setConnected] = useState(true);
const [loading, setLoading] = useState(true);
const [fetchFunctionCalled, setFetchFunctionCalled] = useState(false);
const s3Credentials = { s3Bucket, ...S3data };
//In React, useRef creates a mutable object that persists across renders without triggering re-renders when its value changes.
//While fetching, we check the value of continueFetching, if its false, we break. This means that either the user changed tab or the object has been finished its execution (return)
@ -91,48 +108,75 @@ function SodioHomeInstallation(props: singleInstallationProps) {
}
const fetchDataPeriodically = async () => {
while (continueFetching.current) {
//Fetch data from Bitwatt cloud
console.log('Fetching from Bitwatt cloud');
var timeperiodToSearch = 200;
let res;
let timestampToFetch;
console.log(props.current_installation.serialNumber);
console.log(props.current_installation.s3WriteKey);
console.log(props.current_installation.s3WriteSecret);
const timeStamp = Date.now().toString();
// Encrypt timestamp using AES-ECB with PKCS7 padding
const key = CryptoJS.enc.Utf8.parse(
props.current_installation.s3WriteSecret
);
const encrypted = CryptoJS.AES.encrypt(timeStamp, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString();
// Set headers
const headers = {
'X-Signature': encrypted,
'X-AccessKey': props.current_installation.s3WriteKey,
'Content-Type': 'application/json'
};
// API URL
const url = `https://www.biwattpower.com/gateway/admin/open/device/currentEnergyFlowData/${props.current_installation.serialNumber}`;
for (var i = 0; i < timeperiodToSearch; i += 2) {
if (!continueFetching.current) {
return false;
}
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
try {
const response = await fetch(url, { method: 'GET', headers });
const result = await response.json();
console.log('API Response:', result);
} catch (error) {
console.error('Request failed:', error);
res = await fetchDataJson(timestampToFetch, s3Credentials, false);
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
break;
}
} catch (err) {
console.error('Error fetching data:', err);
return false;
}
}
if (i >= timeperiodToSearch) {
setConnected(false);
setLoading(false);
return false;
}
setConnected(true);
setLoading(false);
while (continueFetching.current) {
for (const timestamp of Object.keys(res)) {
if (!continueFetching.current) {
setFetchFunctionCalled(false);
return false;
}
console.log(`Timestamp: ${timestamp}`);
console.log(res[timestamp]);
setValues(res[timestamp]);
await timeout(2000);
}
// Wait for 2 seconds before fetching again
await timeout(200000);
console.log('ssssssssssssssssssssssssssssssssssssss');
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(60));
console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch);
for (i = 0; i < 30; i++) {
if (!continueFetching.current) {
return false;
}
try {
console.log('Trying to fetch timestamp ' + timestampToFetch);
res = await fetchDataJson(timestampToFetch, s3Credentials, false);
if (
res !== FetchResult.notAvailable &&
res !== FetchResult.tryLater
) {
break;
}
} catch (err) {
console.error('Error fetching data:', err);
return false;
}
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(1));
}
if (i == 30) {
return false;
}
}
setFetchFunctionCalled(false);
};
useEffect(() => {
@ -147,11 +191,16 @@ function SodioHomeInstallation(props: singleInstallationProps) {
}, [status]);
useEffect(() => {
console.log(currentTab);
if (currentTab == 'live' || location.includes('batteryview')) {
//Fetch periodically if the tab is live or batteryview
if (
currentTab == 'live' ||
currentTab == 'pvview' ||
currentTab == 'configuration' ||
location.includes('batteryview')
) {
//Fetch periodically if the tab is live, pvview or batteryview
if (
currentTab == 'live' ||
currentTab == 'pvview' ||
(location.includes('batteryview') && !location.includes('mainstats'))
) {
if (!continueFetching.current) {
@ -163,6 +212,10 @@ function SodioHomeInstallation(props: singleInstallationProps) {
}
}
}
//Fetch only one time in configuration tab
// if (currentTab == 'configuration') {
// fetchDataForOneTime();
// }
return () => {
continueFetching.current = false;
@ -322,10 +375,11 @@ function SodioHomeInstallation(props: singleInstallationProps) {
<Route
path={routes.information}
element={
<InformationSodioHome
<Information
values={props.current_installation}
s3Credentials={s3Credentials}
type={props.type}
></InformationSodioHome>
></Information>
}
/>
@ -351,28 +405,17 @@ function SodioHomeInstallation(props: singleInstallationProps) {
}
/>
{/*<Route*/}
{/* path={routes.overview}*/}
{/* element={*/}
{/* <SalidomoOverview*/}
{/* s3Credentials={s3Credentials}*/}
{/* id={props.current_installation.id}*/}
{/* ></SalidomoOverview>*/}
{/* }*/}
{/*/>*/}
{/*<Route*/}
{/* path={routes.batteryview + '*'}*/}
{/* element={*/}
{/* <BatteryViewSalidomo*/}
{/* values={values}*/}
{/* s3Credentials={s3Credentials}*/}
{/* installationId={props.current_installation.id}*/}
{/* productNum={props.current_installation.product}*/}
{/* connected={connected}*/}
{/* ></BatteryViewSalidomo>*/}
{/* }*/}
{/*></Route>*/}
<Route
path={routes.batteryview + '*'}
element={
<BatteryViewSodioHome
values={values}
s3Credentials={s3Credentials}
installationId={props.current_installation.id}
connected={connected}
></BatteryViewSodioHome>
}
></Route>
{currentUser.userType == UserType.admin && (
<Route

View File

@ -1,286 +0,0 @@
import React, { useContext, useState } from 'react';
import {
Alert,
Box,
CircularProgress,
IconButton,
Modal,
TextField,
useTheme
} from '@mui/material';
import Button from '@mui/material/Button';
import { Close as CloseIcon } from '@mui/icons-material';
import { I_Installation } from 'src/interfaces/InstallationTypes';
import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
import { FormattedMessage } from 'react-intl';
interface SodiohomeInstallationFormProps {
cancel: () => void;
submit: () => void;
parentid: number;
}
function SodiohomeInstallationForm(props: SodiohomeInstallationFormProps) {
const theme = useTheme();
const [open, setOpen] = useState(true);
const [formValues, setFormValues] = useState<Partial<I_Installation>>({
name: '',
region: '',
location: '',
country: '',
serialNumber: '',
s3WriteSecret: '',
s3WriteKey: ''
});
const requiredFields = ['name', 'location', 'country', 'serialNumber'];
const installationContext = useContext(InstallationsContext);
const { createInstallation, loading, setLoading, error, setError } =
installationContext;
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({
...formValues,
[name]: value
});
};
const handleSubmit = async (e) => {
setLoading(true);
formValues.parentId = props.parentid;
formValues.product = 2;
const responseData = await createInstallation(formValues);
props.submit();
};
const handleCancelSubmit = (e) => {
props.cancel();
};
const areRequiredFieldsFilled = () => {
for (const field of requiredFields) {
if (!formValues[field]) {
return false;
}
}
return true;
};
const isMobile = window.innerWidth <= 1490;
return (
<>
<Modal
open={open}
onClose={() => {}}
aria-labelledby="error-modal"
aria-describedby="error-modal-description"
>
<Box
sx={{
position: 'absolute',
top: isMobile ? '50%' : '40%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 500,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4
}}
>
<Box
component="form"
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center', // Center items horizontally
'& .MuiTextField-root': {
m: 1,
width: 390
}
}}
noValidate
autoComplete="off"
>
<div>
<TextField
label={
<FormattedMessage
id="installationName"
defaultMessage="Installation Name"
/>
}
name="name"
value={formValues.name}
onChange={handleChange}
required
error={formValues.name === ''}
/>
</div>
<div>
<TextField
label={<FormattedMessage id="region" defaultMessage="Region" />}
name="region"
value={formValues.region}
onChange={handleChange}
required
error={formValues.region === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage id="location" defaultMessage="Location" />
}
name="location"
value={formValues.location}
onChange={handleChange}
required
error={formValues.location === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage id="country" defaultMessage="Country" />
}
name="country"
value={formValues.country}
onChange={handleChange}
required
error={formValues.country === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="serialNumber"
defaultMessage="Serial Number"
/>
}
name="serialNumber"
value={formValues.serialNumber}
onChange={handleChange}
required
error={formValues.serialNumber === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="s3WriteKey"
defaultMessage="BitWatt Cloud Access Key"
/>
}
name="s3WriteKey"
value={formValues.s3WriteKey}
onChange={handleChange}
required
error={formValues.s3WriteKey === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="s3WriteSecret"
defaultMessage="BitWatt Cloud Secret Key"
/>
}
name="s3WriteSecret"
value={formValues.s3WriteSecret}
onChange={handleChange}
required
error={formValues.s3WriteSecret === ''}
/>
</div>
<div>
<TextField
label={
<FormattedMessage
id="Information"
defaultMessage="Information"
/>
}
name="information"
value={formValues.information}
onChange={handleChange}
/>
</div>
</Box>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
<Button
variant="contained"
onClick={handleSubmit}
sx={{
marginLeft: '20px'
}}
disabled={!areRequiredFieldsFilled()}
>
<FormattedMessage id="submit" defaultMessage="Submit" />
</Button>
<Button
variant="contained"
onClick={handleCancelSubmit}
sx={{
marginLeft: '10px'
}}
>
<FormattedMessage id="cancel" defaultMessage="Cancel" />
</Button>
{loading && (
<CircularProgress
sx={{
color: theme.palette.primary.main,
marginLeft: '20px'
}}
/>
)}
{error && (
<Alert
severity="error"
sx={{
ml: 1,
display: 'flex',
alignItems: 'center'
}}
>
<FormattedMessage
id="errorOccured"
defaultMessage="An error has occured"
/>
<IconButton
color="inherit"
size="small"
onClick={() => setError(false)}
sx={{ marginLeft: '4px' }}
>
<CloseIcon fontSize="small" />
</IconButton>
</Alert>
)}
</div>
</Box>
</Modal>
</>
);
}
export default SodiohomeInstallationForm;

View File

@ -14,12 +14,17 @@ import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
import { UserType } from '../../../interfaces/UserTypes';
import SodioHomeInstallation from './Installation';
function SodioHomeInstallationTabs() {
interface SodioHomeInstallationTabsProps {
product: number;
}
function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
const location = useLocation();
const context = useContext(UserContext);
const { currentUser } = context;
const tabList = [
'live',
'batteryview',
'information',
'manage',
'overview',
@ -53,26 +58,14 @@ function SodioHomeInstallationTabs() {
}, [location]);
useEffect(() => {
if (sodiohomeInstallations.length === 0 && fetchedInstallations === false) {
setProduct(props.product);
}, [props.product]);
useEffect(() => {
if (product == props.product) {
fetchAllSodiohomeInstallations();
setFetchedInstallations(true);
}
}, [sodiohomeInstallations]);
useEffect(() => {
if (sodiohomeInstallations && sodiohomeInstallations.length > 0) {
if (!socket) {
openSocket(2);
} else if (product != 2) {
closeSocket();
openSocket(2);
}
}
}, [sodiohomeInstallations]);
useEffect(() => {
setProduct(2);
}, []);
}, [product]);
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
setCurrentTab(value);
@ -101,14 +94,23 @@ function SodioHomeInstallationTabs() {
const singleInstallationTabs =
currentUser.userType == UserType.admin
? [
// {
// value: 'live',
// label: <FormattedMessage id="live" defaultMessage="Live" />
// },
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
value: 'batteryview',
label: (
<FormattedMessage
id="batteryview"
defaultMessage="Battery View"
/>
)
},
// {
// value: 'overview',
// label: <FormattedMessage id="overview" defaultMessage="Overview" />
// },
{
value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" />
@ -141,14 +143,14 @@ function SodioHomeInstallationTabs() {
}
]
: [
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
},
// {
// value: 'live',
// label: <FormattedMessage id="live" defaultMessage="Live" />
// },
// {
// value: 'overview',
// label: <FormattedMessage id="overview" defaultMessage="Overview" />
// },
{
value: 'information',
@ -172,14 +174,23 @@ function SodioHomeInstallationTabs() {
value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
},
// {
// value: 'live',
// label: <FormattedMessage id="live" defaultMessage="Live" />
// },
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />
value: 'batteryview',
label: (
<FormattedMessage
id="batteryview"
defaultMessage="Battery View"
/>
)
},
// {
// value: 'overview',
// label: <FormattedMessage id="overview" defaultMessage="Overview" />
// },
{
value: 'log',
label: <FormattedMessage id="log" defaultMessage="Log" />
@ -224,10 +235,10 @@ function SodioHomeInstallationTabs() {
value: 'tree',
icon: <AccountTreeIcon id="mode-toggle-button-tree-icon" />
},
{
value: 'live',
label: <FormattedMessage id="live" defaultMessage="Live" />
},
// {
// value: 'live',
// label: <FormattedMessage id="live" defaultMessage="Live" />
// },
{
value: 'overview',
label: <FormattedMessage id="overview" defaultMessage="Overview" />

View File

@ -29,7 +29,6 @@ function Topology(props: TopologyProps) {
const { product, setProduct } = useContext(ProductIdContext);
//console.log('product VALUE IS ', product);
const [showValues, setShowValues] = useState(false);
const handleSwitch = () => () => {
@ -38,6 +37,13 @@ function Topology(props: TopologyProps) {
const isMobile = window.innerWidth <= 1490;
const totalPvPower = props.values?.PvOnDc
? Object.values(props.values.PvOnDc).reduce(
(sum, device) => sum + (device?.Dc?.Power || 0),
0
)
: 0;
return (
<Container maxWidth="xl" style={{ backgroundColor: 'white' }}>
<Grid container>
@ -408,7 +414,7 @@ function Topology(props: TopologyProps) {
data: props.values?.PvOnDc
? [
{
value: props.values.PvOnDc.Dc.Power,
value: totalPvPower,
unit: 'W'
}
]
@ -421,15 +427,12 @@ function Topology(props: TopologyProps) {
position: 'top',
data: props.values?.PvOnDc
? {
value: props.values.PvOnDc.Dc.Power,
value: totalPvPower,
unit: 'W'
}
: undefined,
amount: props.values?.PvOnDc
? getAmount(
highestConnectionValue,
props.values.PvOnDc.Dc.Power
)
? getAmount(highestConnectionValue, totalPvPower)
: 0,
showValues: showValues
}}

View File

@ -26,7 +26,6 @@ import { InstallationsContext } from '../../../contexts/InstallationsContextProv
import { UserType } from '../../../interfaces/UserTypes';
import InstallationForm from '../Installations/installationForm';
import SalidomoInstallationForm from '../SalidomoInstallations/SalidomoInstallationForm';
import SodiohomeInstallationForm from '../SodiohomeInstallations/SodiohomeInstallationForm';
interface TreeInformationProps {
folder: I_Folder;
@ -65,7 +64,7 @@ function TreeInformation(props: TreeInformationProps) {
// console.log('Selected Product:', e.target.value);
};
const ProductTypes = ['Salimax', 'Salidomo', 'Sodiohome'];
const ProductTypes = ['Salimax', 'Salidomo', 'Sodiohome', 'SodistoreMax'];
const isMobile = window.innerWidth <= 1490;
@ -322,13 +321,17 @@ function TreeInformation(props: TreeInformationProps) {
</Box>
</Modal>
)}
{openModalInstallation && product == 'Salimax' && (
<InstallationForm
cancel={handleFormCancel}
submit={handleInstallationFormSubmit}
parentid={props.folder.id}
/>
)}
{openModalInstallation &&
(product == 'Salimax' ||
product == 'Sodiohome' ||
product == 'SodistoreMax') && (
<InstallationForm
cancel={handleFormCancel}
submit={handleInstallationFormSubmit}
parentid={props.folder.id}
productToInsert={ProductTypes.indexOf(product)}
/>
)}
{openModalInstallation && product == 'Salidomo' && (
<SalidomoInstallationForm
cancel={handleFormCancel}
@ -336,13 +339,13 @@ function TreeInformation(props: TreeInformationProps) {
parentid={props.folder.id}
/>
)}
{openModalInstallation && product == 'Sodiohome' && (
<SodiohomeInstallationForm
cancel={handleFormCancel}
submit={handleInstallationFormSubmit}
parentid={props.folder.id}
/>
)}
{/*{openModalInstallation && product == 'Sodiohome' && (*/}
{/* <SodiohomeInstallationForm*/}
{/* cancel={handleFormCancel}*/}
{/* submit={handleInstallationFormSubmit}*/}
{/* parentid={props.folder.id}*/}
{/* />*/}
{/*)}*/}
<Container maxWidth="xl">
<Grid

View File

@ -49,7 +49,7 @@ const FlatUsersView = (props: FlatUsersViewProps) => {
return (
<Grid container spacing={1} sx={{ marginTop: '1px' }}>
<Grid item xs={12} md={isMobile ? 5 : 4}>
<Grid item xs={6} md={5}>
<Card>
<Divider />
<TableContainer sx={{ maxHeight: '520px', overflowY: 'auto' }}>
@ -113,13 +113,14 @@ const FlatUsersView = (props: FlatUsersViewProps) => {
</TableContainer>
</Card>
</Grid>
{selectedUser && (
<User
current_user={findUser(selectedUser)}
fetchDataAgain={props.fetchDataAgain}
></User>
)}
<Grid item xs={6} md={7}>
{selectedUser && (
<User
current_user={findUser(selectedUser)}
fetchDataAgain={props.fetchDataAgain}
></User>
)}
</Grid>
</Grid>
);
};

View File

@ -26,6 +26,7 @@ import { InnovEnergyUser } from 'src/interfaces/UserTypes';
import { TokenContext } from 'src/contexts/tokenContext';
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
import { FormattedMessage } from 'react-intl';
import UserAccess from '../ManageAccess/UserAccess';
interface singleUserProps {
current_user: InnovEnergyUser;
@ -41,7 +42,10 @@ function User(props: singleUserProps) {
const [formValues, setFormValues] = useState(props.current_user);
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
const tabs = [{ value: 'user', label: 'User' }];
const tabs = [
{ value: 'user', label: 'User' },
{ value: 'manage', label: 'Access Management' }
];
const [openModalDeleteUser, setOpenModalDeleteUser] = useState(false);
const UserTypes = ['Client', 'Partner', 'Admin'];
@ -226,7 +230,7 @@ function User(props: singleUserProps) {
</Modal>
)}
<Grid item xs={12} md={isMobile ? 7 : 8}>
<Grid item xs={12} md={12}>
<TabsContainerWrapper>
<Tabs
onChange={handleTabsChange}
@ -301,7 +305,7 @@ function User(props: singleUserProps) {
<div>
<FormControl
fullWidth
sx={{ marginLeft: 1, marginTop: 1, width: 390 }}
sx={{ marginLeft: 1, marginTop: 1, width: 445 }}
>
<InputLabel
sx={{
@ -413,6 +417,9 @@ function User(props: singleUserProps) {
</Grid>
</Container>
)}
{currentTab === 'manage' && (
<UserAccess current_user={props.current_user}></UserAccess>
)}
</Grid>
</Card>
</Grid>

View File

@ -69,25 +69,38 @@ function userForm(props: userFormProps) {
const fetchInstallations = useCallback(async () => {
setLoading(true);
return axiosConfig
.get('/GetAllInstallations')
.then((res) => {
setInstallations(res.data);
setLoading(false);
})
.catch((err) => {
setLoading(false);
if (err.response && err.response.status == 401) {
removeToken();
}
});
try {
// fetch product 0
const res0 = await axiosConfig.get(
`/GetAllInstallationsFromProduct?product=0`
);
const installations0 = res0.data;
// fetch product 1
const res1 = await axiosConfig.get(
`/GetAllInstallationsFromProduct?product=3`
);
const installations1 = res1.data;
// aggregate
const combined = [...installations0, ...installations1];
// update
setInstallations(combined);
} catch (err) {
if (err.response && err.response.status === 401) {
removeToken();
}
} finally {
setLoading(false);
}
}, [setInstallations]);
useEffect(() => {
fetchFolders();
fetchInstallations();
}, []);
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({

View File

@ -12,8 +12,11 @@ import {
InnovEnergyUser
} from '../interfaces/UserTypes';
import { FormattedMessage } from 'react-intl';
import { I_Installation } from '../interfaces/InstallationTypes';
interface AccessContextProviderProps {
fetchInstallationsForUser: (userId: number) => void;
accessibleInstallationsForUser: I_Installation[];
availableUsers: InnovEnergyUser[];
fetchAvailableUsers: () => Promise<void>;
usersWithDirectAccess: InnovEnergyUser[];
@ -44,6 +47,8 @@ interface AccessContextProviderProps {
}
export const AccessContext = createContext<AccessContextProviderProps>({
fetchInstallationsForUser: () => Promise.resolve(),
accessibleInstallationsForUser: [],
availableUsers: [],
fetchAvailableUsers: () => {
return Promise.resolve();
@ -74,6 +79,8 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
InnovEnergyUser[]
>([]);
const [availableUsers, setAvailableUsers] = useState<InnovEnergyUser[]>([]);
const [accessibleInstallationsForUser, setAccessibleInstallationsForUser] =
useState<I_Installation[]>([]);
const [usersWithInheritedAccess, setUsersWithInheritedAccess] = useState<
I_UserWithInheritedAccess[]
@ -104,6 +111,26 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
[]
);
const fetchInstallationsForUser = useCallback(async (userId: number) => {
axiosConfig
.get(`/GetInstallationsTheUserHasAccess?userId=${userId}`)
.then((response) => {
if (response) {
setAccessibleInstallationsForUser(response.data);
}
})
.catch((error) => {
setError(true);
const message = (
<FormattedMessage
id="unableToLoadData"
defaultMessage="Unable to load data"
/>
).props.defaultMessage;
setErrorMessage(message);
});
}, []);
const fetchUsersWithInheritedAccessForResource = useCallback(
async (tempresourceType: string, id: number) => {
axiosConfig
@ -192,6 +219,8 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => {
return (
<AccessContext.Provider
value={{
fetchInstallationsForUser,
accessibleInstallationsForUser,
availableUsers,
fetchAvailableUsers,
usersWithDirectAccess,

View File

@ -121,7 +121,7 @@ const InstallationsContextProvider = ({
}
const tokenString = localStorage.getItem('token');
const token = tokenString !== null ? tokenString : '';
const urlWithToken = `wss://monitor.innov.energy/api/CreateWebSocket?authToken=${token}`;
const urlWithToken = `wss://monitor.inesco.energy/api/CreateWebSocket?authToken=${token}`;
const new_socket = new WebSocket(urlWithToken);
@ -200,9 +200,10 @@ const InstallationsContextProvider = ({
const fetchAllSodiohomeInstallations = useCallback(async () => {
axiosConfig
.get('/GetAllSodioHomeInstallations')
.then((res: AxiosResponse<I_Installation[]>) =>
setSodiohomeInstallations(res.data)
)
.then((res: AxiosResponse<I_Installation[]>) => {
setSodiohomeInstallations(res.data);
openSocket(res.data);
})
.catch((err: AxiosError) => {
if (err.response?.status === 401) {
removeToken();

View File

@ -320,7 +320,7 @@ export const transformInputToDailyDataJson = async (
//'Battery.Dc.Power' for salimax,
// 'Battery.Power',
'GridMeter.Ac.Power.Active',
'PvOnDc.Dc.Power',
'PvOnDc',
'DcDc.Dc.Link.Voltage',
'LoadOnAcGrid.Power.Active',
'LoadOnDc.Power'
@ -420,23 +420,35 @@ export const transformInputToDailyDataJson = async (
// eslint-disable-next-line @typescript-eslint/no-loop-func
pathsToSearch.forEach((path) => {
if (get(result, path) !== undefined) {
const value = path
.split('.')
.reduce((o, key) => (o ? o[key] : undefined), result);
let value: number | undefined = undefined;
if (value < chartOverview[categories[category_index]].min) {
chartOverview[categories[category_index]].min = value;
if (category_index === 4) {
// Custom logic for 'PvOnDc.Dc.Power'
value = Object.values(
result.PvOnDc as Record<string, { Dc?: { Power?: number } }>
).reduce((sum, device) => sum + (device.Dc?.Power || 0), 0);
} else if (get(result, path) !== undefined) {
// Default path-based extraction
value = path
.split('.')
.reduce((o, key) => (o ? o[key] : undefined), result);
}
if (value > chartOverview[categories[category_index]].max) {
chartOverview[categories[category_index]].max = value;
// Only push value if defined
if (value !== undefined) {
if (value < chartOverview[categories[category_index]].min) {
chartOverview[categories[category_index]].min = value;
}
if (value > chartOverview[categories[category_index]].max) {
chartOverview[categories[category_index]].max = value;
}
chartData[categories[category_index]].data.push([
adjustedTimestampArray[i],
value
]);
}
chartData[categories[category_index]].data.push([
adjustedTimestampArray[i],
value
]);
} else {
//data[path].push([adjustedTimestamp, null]);
}
category_index++;
});

View File

@ -213,7 +213,7 @@ function SidebarMenu() {
<Box sx={{ marginTop: '3px' }}>
<FormattedMessage
id="sodistore"
defaultMessage="Sodistore"
defaultMessage="SodistoreMax"
/>
</Box>
</Button>

View File

@ -1,7 +1,8 @@
import { useContext } from 'react';
import Scrollbar from 'src/components/Scrollbar';
import { SidebarContext } from 'src/contexts/SidebarContext';
import innovenergyLogo from 'src/Resources/images/innovenergy-Logo_Speichern-mit-Salz_R_color.svg';
import inescoLogo from 'src/Resources/images/inesco_logo.png';
import {
alpha,
Box,
@ -17,8 +18,8 @@ import SidebarMenu from './SidebarMenu';
const SidebarWrapper = styled(Box)(
({ theme }) => `
width: ${theme.sidebar.width};
min-width: ${theme.sidebar.width};
width: 280px; /* previously theme.sidebar.width */
min-width: 280px;
color: ${theme.colors.alpha.trueWhite[70]};
position: relative;
z-index: 7;
@ -54,16 +55,24 @@ function Sidebar() {
<Scrollbar>
<Box mt={3}>
<Box
mx={2}
sx={{
width: 52
px: 2, // Padding left & right
py: 1.5, // Optional: padding top & bottom
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
border: '2px solid white', // Optional: border around logo
borderRadius: '8px',
backgroundColor: '#fff', // Optional: white background
mx: 2 // Horizontal margin to avoid sticking to edge
}}
>
<img
src={innovenergyLogo}
alt="innovenergy logo"
src={inescoLogo}
alt="inesco logo"
style={{
width: '150px' // Width of the image
width: '160px',
objectFit: 'contain'
}}
/>
</Box>
@ -105,8 +114,8 @@ function Sidebar() {
}}
>
<img
src={innovenergyLogo}
alt="innovenergy logo"
src={inescoLogo}
alt="inesco logo"
style={{
width: '150px' // Width of the image
}}

View File

@ -58,7 +58,7 @@ const SidebarLayout = (props: SidebarLayoutProps) => {
flex: 1,
pt: `${theme.header.height}`,
[theme.breakpoints.up('lg')]: {
ml: `${theme.sidebar.width}`
ml: '260px'
}
}}
>

View File

@ -1,4 +1,4 @@
import { alpha, createTheme, lighten, darken } from '@mui/material';
import { alpha, createTheme, darken, lighten } from '@mui/material';
import '@mui/lab/themeAugmentation';
const themeColors = {