Add history button

This commit is contained in:
Noe 2024-06-18 16:19:40 +02:00
parent 5fdc7de448
commit 48de0805b9
6 changed files with 473 additions and 291 deletions

View File

@ -564,12 +564,20 @@ public class Controller : ControllerBase
return Ok(); return Ok();
} }
[HttpPost(nameof(InsertNewAction))]
[HttpPost(nameof(EditInstallationConfig))] public async Task<ActionResult<IEnumerable<Object>>> InsertNewAction([FromBody] UserAction action, Token authToken)
public async Task<ActionResult<IEnumerable<Object>>> EditInstallationConfig([FromBody] Configuration config, Int64 installationId, Token authToken) {
var session = Db.GetSession(authToken);
var actionSuccess = await session.RecordUserAction(action);
return actionSuccess ? Ok() : Unauthorized();
}
[HttpPost(nameof(EditInstallationConfig))]
public async Task<ActionResult<IEnumerable<Object>>> EditInstallationConfig([FromBody] Configuration config, Int64 installationId,Token authToken)
{ {
var session = Db.GetSession(authToken); var session = Db.GetSession(authToken);
//Console.WriteLine(config.GridSetPoint);
// Send configuration changes // Send configuration changes
var success = await session.SendInstallationConfig(installationId, config); var success = await session.SendInstallationConfig(installationId, config);
@ -577,7 +585,15 @@ public class Controller : ControllerBase
// Record configuration change // Record configuration change
if (success) if (success)
{ {
var actionSuccess = await session.RecordUserAction(installationId, config); // Create a new UserAction object
var action = new UserAction
{
InstallationId = installationId,
Timestamp = DateTime.Now,
Description = config.GetConfigurationString()
};
var actionSuccess = await session.RecordUserAction(action);
return actionSuccess?Ok():Unauthorized(); return actionSuccess?Ok():Unauthorized();
} }

View File

@ -102,22 +102,14 @@ public static class SessionMethods
&& await installation.SendConfig(configuration); && await installation.SendConfig(configuration);
} }
public static async Task<Boolean> RecordUserAction(this Session? session, Int64 installationId, Configuration newConfiguration) public static async Task<Boolean> RecordUserAction(this Session? session, UserAction action)
{ {
var user = session?.User; var user = session?.User;
var timestamp = DateTime.Now;
if (user is null || user.UserType == 0) if (user is null || user.UserType == 0)
return false; return false;
// Create a new UserAction object action.UserName = user.Name;
var action = new UserAction
{
UserName = user.Name,
InstallationId = installationId,
Timestamp = timestamp,
Description = newConfiguration.GetConfigurationString()
};
// Save the configuration change to the database // Save the configuration change to the database
Db.HandleAction(action); Db.HandleAction(action);

View File

@ -88,7 +88,7 @@ public static partial class Db
} }
else else
{ {
Console.WriteLine("---------------Added the new Error to the database-----------------"); Console.WriteLine("---------------Added the new Action to the database-----------------");
Create(newAction); Create(newAction);
} }
} }

View File

@ -59,23 +59,8 @@ public static class RabbitMqManager
//Consumer received a message //Consumer received a message
if (receivedStatusMessage != null) if (receivedStatusMessage != null)
{ {
Console.WriteLine("----------------------------------------------"); Installation installation = Db.Installations.FirstOrDefault(f => f.Product == receivedStatusMessage.Product && f.S3BucketId == receivedStatusMessage.InstallationId);
int installationId = (int )installation.Id;
int installationId = (int)Db.Installations.Where(f => f.Product == receivedStatusMessage.Product && f.S3BucketId == receivedStatusMessage.InstallationId).Select(f => f.Id).FirstOrDefault();
string installationName = (string)Db.Installations.Where(f => f.Product == receivedStatusMessage.Product && f.S3BucketId == receivedStatusMessage.InstallationId).Select(f => f.InstallationName).FirstOrDefault();
int bucketId = (int)Db.Installations.Where(f => f.Product == receivedStatusMessage.Product && f.S3BucketId == receivedStatusMessage.InstallationId).Select(f => f.S3BucketId).FirstOrDefault();
int productId = (int)Db.Installations.Where(f => f.Product == receivedStatusMessage.Product && f.S3BucketId == receivedStatusMessage.InstallationId).Select(f => f.Product).FirstOrDefault();
string monitorLink = "";
if (productId == 0)
{
monitorLink =
$"https://monitor.innov.energy/installations/list/installation/{bucketId}/batteryview";
}
else
{
monitorLink =
$"https://monitor.innov.energy/salidomo_installations/list/installation/{bucketId}/batteryview";
}
Console.WriteLine("Received a message from installation: " + installationId + " , product is: "+receivedStatusMessage.Product+ " and status is: " + receivedStatusMessage.Status); Console.WriteLine("Received a message from installation: " + installationId + " , product is: "+receivedStatusMessage.Product+ " and status is: " + receivedStatusMessage.Status);
//This is a heartbit message, just update the timestamp for this installation. //This is a heartbit message, just update the timestamp for this installation.
@ -113,17 +98,32 @@ public static class RabbitMqManager
if (receivedStatusMessage.Alarms != null) if (receivedStatusMessage.Alarms != null)
{ {
string monitorLink;
if (installation.Product == 0)
{
monitorLink =
$"https://monitor.innov.energy/installations/list/installation/{installation.S3BucketId}/batteryview";
}
else
{
monitorLink =
$"https://monitor.innov.energy/salidomo_installations/list/installation/{installation.S3BucketId}/batteryview";
}
foreach (var alarm in receivedStatusMessage.Alarms) foreach (var alarm in receivedStatusMessage.Alarms)
{ {
Error newError = new Error Error newError = new Error
{ {
InstallationId = installationId, InstallationId = installation.Id,
Description = alarm.Description, Description = alarm.Description,
Date = alarm.Date, Date = alarm.Date,
Time = alarm.Time, Time = alarm.Time,
DeviceCreatedTheMessage = alarm.CreatedBy, DeviceCreatedTheMessage = alarm.CreatedBy,
Seen = false Seen = false
}; Console.WriteLine("Add an alarm for installation "+installationId); };
Console.WriteLine("Add an alarm for installation "+installationId);
// Send replace battery email to support team if this alarm is "NeedToReplaceBattery" // Send replace battery email to support team if this alarm is "NeedToReplaceBattery"
if (alarm.Description == "NeedToReplaceBattery" || alarm.Description == "2 or more string are disabled") if (alarm.Description == "NeedToReplaceBattery" || alarm.Description == "2 or more string are disabled")
{ {
@ -132,7 +132,7 @@ public static class RabbitMqManager
string subject = "Battery Alarm: 2 or more strings broken"; string subject = "Battery Alarm: 2 or more strings broken";
string text = $"Dear InnovEnergy Support Team,\n" + string text = $"Dear InnovEnergy Support Team,\n" +
$"\n"+ $"\n"+
$"Installation Name: {installationName}\n"+ $"Installation Name: {installation.InstallationName}\n"+
$"\n"+ $"\n"+
$"Installation Monitor Link: {monitorLink}\n"+ $"Installation Monitor Link: {monitorLink}\n"+
$"\n"+ $"\n"+

View File

@ -1,11 +1,14 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { import {
Alert, Alert,
Box,
Card, Card,
Container, Container,
Divider, Divider,
Grid, Grid,
IconButton, IconButton,
Modal,
TextField,
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
@ -16,6 +19,10 @@ import routes from '../../../Resources/routes.json';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { TokenContext } from '../../../contexts/tokenContext'; import { TokenContext } from '../../../contexts/tokenContext';
import { Action } from '../../../interfaces/S3Types'; import { Action } from '../../../interfaces/S3Types';
import Button from '@mui/material/Button';
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import dayjs from 'dayjs';
interface HistoryProps { interface HistoryProps {
errorLoadingS3Data: boolean; errorLoadingS3Data: boolean;
@ -29,7 +36,61 @@ function HistoryOfActions(props: HistoryProps) {
const [history, setHistory] = useState<Action[]>([]); const [history, setHistory] = useState<Action[]>([]);
const navigate = useNavigate(); const navigate = useNavigate();
const tokencontext = useContext(TokenContext); const tokencontext = useContext(TokenContext);
const [actionDate, setActionDate] = useState(dayjs());
const { removeToken } = tokencontext; const { removeToken } = tokencontext;
const [openModalAddAction, setOpenModalAddAction] = useState(false);
const requiredFields = ['description', 'timestamp'];
const [newAction, setNewAction] = useState<Partial<Action>>({
installationId: props.id,
timestamp: actionDate.toDate(),
description: ''
});
const handleDateChange = (newdate) => {
setActionDate(newdate);
setNewAction({
...newAction,
['timestamp']: newdate
});
};
const handleChange = (e) => {
const { name, value } = e.target;
setNewAction({
...newAction,
[name]: value
});
};
const handleAddActionButton = () => {
setOpenModalAddAction(!openModalAddAction);
};
const SumbitNewAction = () => {
const res = axiosConfig.post(`/InsertNewAction`, newAction).catch((err) => {
if (err.response) {
// setError(true);
// setLoading(false);
}
});
if (res) {
setOpenModalAddAction(!openModalAddAction);
}
};
const deleteUserModalHandleCancel = (e) => {
setOpenModalAddAction(false);
};
const areRequiredFieldsFilled = () => {
for (const field of requiredFields) {
if (!newAction[field]) {
return false;
}
}
return true;
};
useEffect(() => { useEffect(() => {
axiosConfig axiosConfig
@ -43,265 +104,378 @@ function HistoryOfActions(props: HistoryProps) {
navigate(routes.login); navigate(routes.login);
} }
}); });
}, []); }, [openModalAddAction]);
return ( return (
<Container maxWidth="xl"> <>
<Grid container> {openModalAddAction && (
<Grid item xs={12} md={12}> <Modal
{history.length > 0 && ( open={openModalAddAction}
<Card sx={{ marginTop: '10px' }}> aria-labelledby="error-modal"
<Divider /> aria-describedby="error-modal-description"
<div> >
<div <LocalizationProvider dateAdapter={AdapterDayjs}>
style={{ <Box
height: '40px',
marginBottom: '10px',
display: 'flex',
alignItems: 'center'
}}
>
<div
style={{
flex: 1,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
color="dimgrey"
fontWeight="bold"
fontSize="1rem"
gutterBottom
noWrap
>
<FormattedMessage id="user" defaultMessage="User" />
</Typography>
</div>
<div
style={{
flex: 1,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
color="dimgrey"
fontWeight="bold"
fontSize="1rem"
gutterBottom
noWrap
>
<FormattedMessage id="date" defaultMessage="Date" />
</Typography>
</div>
<div
style={{
flex: 1,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
color="dimgrey"
fontWeight="bold"
fontSize="1rem"
gutterBottom
noWrap
>
<FormattedMessage id="time" defaultMessage="Time" />
</Typography>
</div>
<div
style={{
flex: 6,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
color="dimgrey"
fontWeight="bold"
fontSize="1rem"
gutterBottom
noWrap
>
<FormattedMessage
id="description"
defaultMessage="Description"
/>
</Typography>
</div>
</div>
<Divider />
<div style={{ maxHeight: '400px', overflowY: 'auto' }}>
{history.map((action, index) => {
// Parse the timestamp string to a Date object
const date = new Date(action.timestamp);
// Extract the date part (e.g., "2023-05-31")
const datePart = date.toLocaleDateString();
// Extract the time part (e.g., "12:34:56")
const timePart = date.toLocaleTimeString();
return (
<React.Fragment key={index}>
<Divider />
<div
style={{
minHeight: '40px',
marginBottom: '10px',
display: 'flex',
alignItems: 'center'
}}
>
<div
style={{
flex: 1,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
>
{action.userName}
</Typography>
</div>
<div
style={{
flex: 1,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
>
{datePart}
</Typography>
</div>
<div
style={{
flex: 1,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
>
{timePart}
</Typography>
</div>
<div
style={{
flex: 6,
display: 'flex',
marginTop: '15px',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
style={{
whiteSpace: 'normal',
wordBreak: 'break-word'
}}
>
{action.description}
</Typography>
</div>
</div>
</React.Fragment>
);
})}
</div>
</div>
</Card>
)}
{!props.errorLoadingS3Data && history.length == 0 && (
<Alert
severity="error"
sx={{ sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 500,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4,
display: 'flex', display: 'flex',
alignItems: 'center', flexDirection: 'column',
marginTop: '20px' alignItems: 'center'
}} }}
> >
<FormattedMessage <div>
id="nohistory" <DateTimePicker
defaultMessage="There is no history of actions" label="Select Action Date"
/> name="timestamp"
<IconButton value={actionDate}
color="inherit" onChange={(newDate) => handleDateChange(newDate.toDate())}
size="small" sx={{
sx={{ marginLeft: '4px' }} width: 450,
></IconButton> marginTop: 2
</Alert> }}
)} />
</Grid>
</Grid>
<Grid item xs={12} md={12} style={{ marginBottom: '20px' }}> <TextField
{props.errorLoadingS3Data && ( label="Description"
<Alert variant="outlined"
severity="error" name="description"
sx={{ value={newAction.description}
display: 'flex', onChange={handleChange}
alignItems: 'center', fullWidth
marginTop: '20px' multiline
}} rows={4} // Adding rows prop to make it a multiline field with more space
> sx={{
<FormattedMessage marginBottom: 2,
id="cannotloadloggingdata" marginTop: 2,
defaultMessage="Cannot load logging data" height: 'auto'
/> }} // 'auto' height works better with multiline fields
<IconButton />
color="inherit" </div>
size="small"
sx={{ marginLeft: '4px' }} <div
></IconButton> style={{
</Alert> display: 'flex',
)} alignItems: 'center'
</Grid> }}
</Container> >
<Button
sx={{
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={SumbitNewAction}
disabled={!areRequiredFieldsFilled()}
>
Submit
</Button>
<Button
sx={{
marginLeft: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={deleteUserModalHandleCancel}
>
Cancel
</Button>
</div>
</Box>
</LocalizationProvider>
</Modal>
)}
{!openModalAddAction && (
<Container maxWidth="xl">
<Grid container>
<Grid container>
<Grid item xs={6} md={6}>
<Button
variant="contained"
onClick={handleAddActionButton}
sx={{
marginTop: '20px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage
id="add_action"
defaultMessage="Add New Action"
/>
</Button>
</Grid>
</Grid>
<Grid item xs={12} md={12}>
{history.length > 0 && (
<Card sx={{ marginTop: '10px' }}>
<Divider />
<div>
<div
style={{
height: '40px',
marginBottom: '10px',
display: 'flex',
alignItems: 'center'
}}
>
<div
style={{
flex: 1,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
color="dimgrey"
fontWeight="bold"
fontSize="1rem"
gutterBottom
noWrap
>
<FormattedMessage id="user" defaultMessage="User" />
</Typography>
</div>
<div
style={{
flex: 1,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
color="dimgrey"
fontWeight="bold"
fontSize="1rem"
gutterBottom
noWrap
>
<FormattedMessage id="date" defaultMessage="Date" />
</Typography>
</div>
<div
style={{
flex: 1,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
color="dimgrey"
fontWeight="bold"
fontSize="1rem"
gutterBottom
noWrap
>
<FormattedMessage id="time" defaultMessage="Time" />
</Typography>
</div>
<div
style={{
flex: 6,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
color="dimgrey"
fontWeight="bold"
fontSize="1rem"
gutterBottom
noWrap
>
<FormattedMessage
id="description"
defaultMessage="Description"
/>
</Typography>
</div>
</div>
<Divider />
<div style={{ maxHeight: '400px', overflowY: 'auto' }}>
{history.map((action, index) => {
// Parse the timestamp string to a Date object
const date = new Date(action.timestamp);
// Extract the date part (e.g., "2023-05-31")
const datePart = date.toLocaleDateString();
// Extract the time part (e.g., "12:34:56")
const timePart = date.toLocaleTimeString();
return (
<React.Fragment key={index}>
<Divider />
<div
style={{
minHeight: '40px',
marginBottom: '10px',
display: 'flex',
alignItems: 'center'
}}
>
<div
style={{
flex: 1,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
>
{action.userName}
</Typography>
</div>
<div
style={{
flex: 1,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
>
{datePart}
</Typography>
</div>
<div
style={{
flex: 1,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
>
{timePart}
</Typography>
</div>
<div
style={{
flex: 6,
display: 'flex',
marginTop: '15px',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
style={{
whiteSpace: 'normal',
wordBreak: 'break-word'
}}
>
{action.description}
</Typography>
</div>
</div>
</React.Fragment>
);
})}
</div>
</div>
</Card>
)}
{!props.errorLoadingS3Data && history.length == 0 && (
<Alert
severity="error"
sx={{
display: 'flex',
alignItems: 'center',
marginTop: '20px'
}}
>
<FormattedMessage
id="nohistory"
defaultMessage="There is no history of actions"
/>
<IconButton
color="inherit"
size="small"
sx={{ marginLeft: '4px' }}
></IconButton>
</Alert>
)}
</Grid>
</Grid>
<Grid item xs={12} md={12} style={{ marginBottom: '20px' }}>
{props.errorLoadingS3Data && (
<Alert
severity="error"
sx={{
display: 'flex',
alignItems: 'center',
marginTop: '20px'
}}
>
<FormattedMessage
id="cannotloadloggingdata"
defaultMessage="Cannot load logging data"
/>
<IconButton
color="inherit"
size="small"
sx={{ marginLeft: '4px' }}
></IconButton>
</Alert>
)}
</Grid>
</Container>
)}
</>
); );
} }

View File

@ -21,6 +21,6 @@ export interface Action {
id: number; id: number;
userName: string; userName: string;
installationId: number; installationId: number;
timestamp: string; timestamp: Date;
description: String; description: string;
} }