add link to partner/client's installationticket page from users page
This commit is contained in:
parent
444a0a29f2
commit
de73bc9211
|
|
@ -522,7 +522,7 @@ public class Controller : ControllerBase
|
|||
return Unauthorized();
|
||||
|
||||
return user.DirectlyAccessibleInstallations()
|
||||
.Select(i => new { i.Id, i.Name })
|
||||
.Select(i => new { i.Id, i.Name, i.Product })
|
||||
.ToList<Object>();
|
||||
}
|
||||
|
||||
|
|
@ -542,6 +542,24 @@ public class Controller : ControllerBase
|
|||
.ToList<Object>();
|
||||
}
|
||||
|
||||
[HttpGet(nameof(GetInstallationsUnderFolder))]
|
||||
public ActionResult<IEnumerable<Object>> GetInstallationsUnderFolder(Int64 folderId, Token authToken)
|
||||
{
|
||||
var sessionUser = Db.GetSession(authToken)?.User;
|
||||
if (sessionUser == null)
|
||||
return Unauthorized();
|
||||
|
||||
var folder = Db.GetFolderById(folderId);
|
||||
if (folder == null || !sessionUser.HasAccessTo(folder))
|
||||
return Unauthorized();
|
||||
|
||||
return folder
|
||||
.DescendantFoldersAndSelf() // self + all nested subfolders
|
||||
.SelectMany(f => f.ChildInstallations()) // installations directly in each
|
||||
.Select(i => new { i.Id, i.Name, i.Product })
|
||||
.ToList<Object>();
|
||||
}
|
||||
|
||||
[HttpGet(nameof(GetUsersWithInheritedAccessToInstallation))]
|
||||
public ActionResult<IEnumerable<Object>> GetUsersWithInheritedAccessToInstallation(Int64 id, Token authToken)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,13 +10,18 @@ import {
|
|||
Alert,
|
||||
Autocomplete,
|
||||
Box,
|
||||
CircularProgress,
|
||||
Collapse,
|
||||
Container,
|
||||
Divider,
|
||||
FormControl,
|
||||
Grid,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
Link,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListSubheader,
|
||||
MenuItem,
|
||||
Modal,
|
||||
|
|
@ -32,6 +37,9 @@ import Avatar from '@mui/material/Avatar';
|
|||
import ListItemText from '@mui/material/ListItemText';
|
||||
import PersonIcon from '@mui/icons-material/Person';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import Button from '@mui/material/Button';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
import { AccessContext } from 'src/contexts/AccessContextProvider';
|
||||
|
|
@ -43,6 +51,8 @@ import {
|
|||
I_Installation
|
||||
} from '../../../interfaces/InstallationTypes';
|
||||
import axiosConfig from '../../../Resources/axiosConfig';
|
||||
import routes from 'src/Resources/routes.json';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const PRODUCT_GROUP_ORDER: number[] = [2, 5, 4, 3, 0, 1];
|
||||
const PRODUCT_NAMES: Record<number, string> = {
|
||||
|
|
@ -91,7 +101,18 @@ function UserAccess(props: UserAccessProps) {
|
|||
|
||||
// Direct grants for this user
|
||||
const [directFolders, setDirectFolders] = useState<{ id: number; name: string }[]>([]);
|
||||
const [directInstallations, setDirectInstallations] = useState<{ id: number; name: string }[]>([]);
|
||||
const [directInstallations, setDirectInstallations] = useState<
|
||||
{ id: number; name: string; product: number }[]
|
||||
>([]);
|
||||
|
||||
// Installations under each folder (lazy-loaded + cached), with expand/loading state
|
||||
const navigate = useNavigate();
|
||||
const [expandedFolderId, setExpandedFolderId] = useState<number | null>(null);
|
||||
const [folderInstallations, setFolderInstallations] = useState<
|
||||
Record<number, { id: number; name: string; product: number }[]>
|
||||
>({});
|
||||
const [loadingFolderId, setLoadingFolderId] = useState<number | null>(null);
|
||||
const [folderError, setFolderError] = useState<Record<number, boolean>>({});
|
||||
|
||||
const accessContext = useContext(AccessContext);
|
||||
const {
|
||||
|
|
@ -169,6 +190,49 @@ function UserAccess(props: UserAccessProps) {
|
|||
});
|
||||
};
|
||||
|
||||
// Build the route to an installation from its product (mirrors CustomTreeItem).
|
||||
// Targets the Tickets tab; the installation router redirects to `live` when the
|
||||
// Tickets tab isn't available (non-admin), via its catch-all <Navigate to live>.
|
||||
const installationLink = (inst: { id: number; product: number }): string => {
|
||||
const base =
|
||||
inst.product === 0
|
||||
? routes.installations
|
||||
: inst.product === 1
|
||||
? routes.salidomo_installations
|
||||
: inst.product === 2
|
||||
? routes.sodiohome_installations
|
||||
: inst.product === 4
|
||||
? routes.sodistoregrid_installations
|
||||
: inst.product === 5
|
||||
? routes.sodistorepro_installations
|
||||
: routes.sodistore_installations;
|
||||
return base + routes.tree + routes.installation + inst.id + '/' + routes.installationTickets;
|
||||
};
|
||||
|
||||
const fetchFolderInstallations = async (folderId: number) => {
|
||||
setLoadingFolderId(folderId);
|
||||
setFolderError((prev) => ({ ...prev, [folderId]: false }));
|
||||
try {
|
||||
const res = await axiosConfig.get(`/GetInstallationsUnderFolder?folderId=${folderId}`);
|
||||
setFolderInstallations((prev) => ({ ...prev, [folderId]: res.data }));
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) removeToken();
|
||||
else setFolderError((prev) => ({ ...prev, [folderId]: true }));
|
||||
} finally {
|
||||
setLoadingFolderId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleFolder = (folderId: number) => {
|
||||
if (expandedFolderId === folderId) {
|
||||
setExpandedFolderId(null);
|
||||
return;
|
||||
}
|
||||
setExpandedFolderId(folderId);
|
||||
if (folderInstallations[folderId]) return; // already cached
|
||||
fetchFolderInstallations(folderId);
|
||||
};
|
||||
|
||||
const handleRevokeInstallation = async (installationId: number) => {
|
||||
axiosConfig
|
||||
.post(`/RevokeUserAccessToInstallation?UserId=${props.current_user.id}&InstallationId=${installationId}`)
|
||||
|
|
@ -357,16 +421,27 @@ function UserAccess(props: UserAccessProps) {
|
|||
</Typography>
|
||||
{directFolders.map((folder, index) => {
|
||||
const isLast = index === directFolders.length - 1;
|
||||
const isExpanded = expandedFolderId === folder.id;
|
||||
const installations = folderInstallations[folder.id];
|
||||
return (
|
||||
<Fragment key={folder.id}>
|
||||
<ListItem
|
||||
sx={{ mb: isLast ? 1 : 0 }}
|
||||
sx={{ mb: isLast && !isExpanded ? 1 : 0 }}
|
||||
secondaryAction={
|
||||
currentUser.userType === UserType.admin && (
|
||||
<IconButton onClick={() => handleRevokeFolder(folder.id, folder.name)} edge="end">
|
||||
<PersonRemoveIcon />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<IconButton
|
||||
onClick={() => handleToggleFolder(folder.id)}
|
||||
edge="end"
|
||||
aria-label={intl.formatMessage({ id: 'viewInstallations' })}
|
||||
>
|
||||
{isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
</IconButton>
|
||||
)
|
||||
{currentUser.userType === UserType.admin && (
|
||||
<IconButton onClick={() => handleRevokeFolder(folder.id, folder.name)} edge="end">
|
||||
<PersonRemoveIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
|
|
@ -376,6 +451,60 @@ function UserAccess(props: UserAccessProps) {
|
|||
</ListItemAvatar>
|
||||
<ListItemText primary={folder.name} />
|
||||
</ListItem>
|
||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||
{loadingFolderId === folder.id ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 1.5 }}>
|
||||
<CircularProgress size={20} />
|
||||
</Box>
|
||||
) : folderError[folder.id] ? (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{ ml: 6, mr: 2, my: 1 }}
|
||||
action={
|
||||
<Button
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => fetchFolderInstallations(folder.id)}
|
||||
>
|
||||
<FormattedMessage id="retry" defaultMessage="Retry" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="couldNotLoadInstallations"
|
||||
defaultMessage="Could not load installations"
|
||||
/>
|
||||
</Alert>
|
||||
) : installations && installations.length > 0 ? (
|
||||
<List disablePadding sx={{ pl: 6 }}>
|
||||
{installations.map((inst) => (
|
||||
<ListItemButton
|
||||
key={inst.id}
|
||||
onClick={() => navigate(installationLink(inst))}
|
||||
sx={{ py: 0.25 }}
|
||||
>
|
||||
<ListItemAvatar sx={{ minWidth: 36 }}>
|
||||
<InsertDriveFileIcon fontSize="small" color="action" />
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Link component="span" underline="hover">
|
||||
{inst.name}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
) : installations ? (
|
||||
<Typography variant="body2" sx={{ pl: 6, py: 1, color: 'text.secondary' }}>
|
||||
<FormattedMessage
|
||||
id="noInstallationsInFolder"
|
||||
defaultMessage="No installations in this folder"
|
||||
/>
|
||||
</Typography>
|
||||
) : null}
|
||||
</Collapse>
|
||||
<Divider />
|
||||
</Fragment>
|
||||
);
|
||||
|
|
@ -411,7 +540,19 @@ function UserAccess(props: UserAccessProps) {
|
|||
<PersonIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={installation.name} />
|
||||
<ListItemText
|
||||
primary={
|
||||
<Link
|
||||
component="button"
|
||||
type="button"
|
||||
underline="hover"
|
||||
align="left"
|
||||
onClick={() => navigate(installationLink(installation))}
|
||||
>
|
||||
{installation.name}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
</Fragment>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
useTheme
|
||||
} from '@mui/material';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { InnovEnergyUser } from 'src/interfaces/UserTypes';
|
||||
import User from './User';
|
||||
|
||||
|
|
@ -22,14 +23,25 @@ interface FlatUsersViewProps {
|
|||
}
|
||||
|
||||
const FlatUsersView = (props: FlatUsersViewProps) => {
|
||||
const [selectedUser, setSelectedUser] = useState<number>(-1);
|
||||
// Selected user is kept in the URL (?userId=) so navigating away to an
|
||||
// installation and pressing Back restores this user's detail pane.
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const userIdParam = searchParams.get('userId');
|
||||
const [selectedUser, setSelectedUser] = useState<number>(
|
||||
userIdParam ? Number(userIdParam) : -1
|
||||
);
|
||||
|
||||
const handleSelectOneUser = (userID: number): void => {
|
||||
if (selectedUser != userID) {
|
||||
setSelectedUser(userID);
|
||||
const next = selectedUser !== userID ? userID : -1;
|
||||
setSelectedUser(next);
|
||||
const params = new URLSearchParams(searchParams);
|
||||
if (next === -1) {
|
||||
params.delete('userId');
|
||||
params.delete('tab');
|
||||
} else {
|
||||
setSelectedUser(-1);
|
||||
params.set('userId', String(next));
|
||||
}
|
||||
setSearchParams(params, { replace: true });
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { TokenContext } from 'src/contexts/tokenContext';
|
|||
import { UserContext } from 'src/contexts/userContext';
|
||||
import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import UserAccess from '../ManageAccess/UserAccess';
|
||||
|
||||
interface singleUserProps {
|
||||
|
|
@ -41,7 +42,11 @@ function User(props: singleUserProps) {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [updated, setUpdated] = useState(false);
|
||||
const [currentTab, setCurrentTab] = useState<string>('user');
|
||||
// Active tab is kept in the URL (?tab=) alongside ?userId= so Back restores it.
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [currentTab, setCurrentTab] = useState<string>(
|
||||
searchParams.get('tab') === 'manage' ? 'manage' : 'user'
|
||||
);
|
||||
const [formValues, setFormValues] = useState(props.current_user);
|
||||
const tokencontext = useContext(TokenContext);
|
||||
const { removeToken } = tokencontext;
|
||||
|
|
@ -86,6 +91,9 @@ function User(props: singleUserProps) {
|
|||
const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => {
|
||||
setCurrentTab(value);
|
||||
setError(false);
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set('tab', value);
|
||||
setSearchParams(params, { replace: true });
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
|
|
|
|||
|
|
@ -157,6 +157,10 @@
|
|||
"errorOccured": "Ein Fehler ist aufgetreten",
|
||||
"successfullyUpdated": "Erfolgreich aktualisiert",
|
||||
"grantAccess": "Zugriff gewähren",
|
||||
"viewInstallations": "Anlagen anzeigen",
|
||||
"noInstallationsInFolder": "Keine Anlagen in diesem Ordner",
|
||||
"couldNotLoadInstallations": "Anlagen konnten nicht geladen werden",
|
||||
"retry": "Erneut versuchen",
|
||||
"UserswithDirectAccess": "Benutzer mit direktem Zugriff",
|
||||
"UserswithInheritedAccess": "Benutzer mit vererbtem Zugriff",
|
||||
"noerrors": "Keine Fehler",
|
||||
|
|
|
|||
|
|
@ -139,6 +139,10 @@
|
|||
"errorOccured": "An error has occurred",
|
||||
"successfullyUpdated": "Successfully updated",
|
||||
"grantAccess": "Grant Access",
|
||||
"viewInstallations": "View installations",
|
||||
"noInstallationsInFolder": "No installations in this folder",
|
||||
"couldNotLoadInstallations": "Could not load installations",
|
||||
"retry": "Retry",
|
||||
"UserswithDirectAccess": "Users with Direct Access",
|
||||
"UserswithInheritedAccess": "Users with Inherited Access",
|
||||
"noerrors": "There are no errors",
|
||||
|
|
|
|||
|
|
@ -151,6 +151,10 @@
|
|||
"errorOccured": "Une erreur s'est produite",
|
||||
"successfullyUpdated": "Mise à jour réussie",
|
||||
"grantAccess": "Accorder l'accès",
|
||||
"viewInstallations": "Voir les installations",
|
||||
"noInstallationsInFolder": "Aucune installation dans ce dossier",
|
||||
"couldNotLoadInstallations": "Impossible de charger les installations",
|
||||
"retry": "Réessayer",
|
||||
"UserswithDirectAccess": "Utilisateurs avec accès direct",
|
||||
"UserswithInheritedAccess": "Utilisateurs avec accès hérité",
|
||||
"noerrors": "Il n'y a pas d'erreurs",
|
||||
|
|
|
|||
|
|
@ -139,6 +139,10 @@
|
|||
"errorOccured": "Si è verificato un errore",
|
||||
"successfullyUpdated": "Aggiornamento riuscito",
|
||||
"grantAccess": "Concedi accesso",
|
||||
"viewInstallations": "Mostra installazioni",
|
||||
"noInstallationsInFolder": "Nessuna installazione in questa cartella",
|
||||
"couldNotLoadInstallations": "Impossibile caricare le installazioni",
|
||||
"retry": "Riprova",
|
||||
"UserswithDirectAccess": "Utenti con accesso diretto",
|
||||
"UserswithInheritedAccess": "Utenti con accesso ereditato",
|
||||
"noerrors": "Non ci sono errori",
|
||||
|
|
|
|||
Loading…
Reference in New Issue