mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-23 16:05:09 +00:00
Compare commits
4 Commits
5027e12f34
...
6dd17a5561
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6dd17a5561 | ||
![]() |
bbdbfa67a0 | ||
![]() |
d43e3fb9fc | ||
![]() |
1fac660bf8 |
@ -74,7 +74,7 @@ sourceSets {
|
|||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = 'stirling.software'
|
group = 'stirling.software'
|
||||||
version = '0.47.0'
|
version = '1.0.0'
|
||||||
|
|
||||||
configurations.configureEach {
|
configurations.configureEach {
|
||||||
exclude group: 'commons-logging', module: 'commons-logging'
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
package stirling.software.proprietary.model.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class TeamWithUserCountDTO {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private Long userCount;
|
||||||
|
|
||||||
|
// Constructor for JPQL projection
|
||||||
|
public TeamWithUserCountDTO(Long id, String name, Long userCount) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.userCount = userCount;
|
||||||
|
}
|
||||||
|
}
|
@ -45,6 +45,7 @@ import stirling.software.proprietary.security.model.SessionEntity;
|
|||||||
import stirling.software.proprietary.security.model.User;
|
import stirling.software.proprietary.security.model.User;
|
||||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||||
|
import stirling.software.proprietary.security.service.TeamService;
|
||||||
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@ -226,14 +227,27 @@ public class AccountWebController {
|
|||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
User user = iterator.next();
|
User user = iterator.next();
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
|
boolean shouldRemove = false;
|
||||||
|
|
||||||
|
// Check if user is an INTERNAL_API_USER
|
||||||
for (Authority authority : user.getAuthorities()) {
|
for (Authority authority : user.getAuthorities()) {
|
||||||
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
||||||
iterator.remove();
|
shouldRemove = true;
|
||||||
roleDetails.remove(Role.INTERNAL_API_USER.getRoleId());
|
roleDetails.remove(Role.INTERNAL_API_USER.getRoleId());
|
||||||
// Break out of the inner loop once the user is removed
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also check if user is part of the Internal team
|
||||||
|
if (user.getTeam() != null && user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||||
|
shouldRemove = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the user if either condition is true
|
||||||
|
if (shouldRemove) {
|
||||||
|
iterator.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// Determine the user's session status and last request time
|
// Determine the user's session status and last request time
|
||||||
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
|
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
|
||||||
boolean hasActiveSession = false;
|
boolean hasActiveSession = false;
|
||||||
@ -336,7 +350,11 @@ public class AccountWebController {
|
|||||||
model.addAttribute("activeUsers", activeUsers);
|
model.addAttribute("activeUsers", activeUsers);
|
||||||
model.addAttribute("disabledUsers", disabledUsers);
|
model.addAttribute("disabledUsers", disabledUsers);
|
||||||
|
|
||||||
List<Team> allTeams = teamRepository.findAll();
|
// Get all teams but filter out the Internal team
|
||||||
|
List<Team> allTeams = teamRepository.findAll()
|
||||||
|
.stream()
|
||||||
|
.filter(team -> !team.getName().equals(stirling.software.proprietary.security.service.TeamService.INTERNAL_TEAM_NAME))
|
||||||
|
.toList();
|
||||||
model.addAttribute("teams", allTeams);
|
model.addAttribute("teams", allTeams);
|
||||||
|
|
||||||
model.addAttribute("maxPaidUsers", applicationProperties.getPremium().getMaxUsers());
|
model.addAttribute("maxPaidUsers", applicationProperties.getPremium().getMaxUsers());
|
||||||
|
@ -17,7 +17,9 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import stirling.software.proprietary.model.Team;
|
import stirling.software.proprietary.model.Team;
|
||||||
import stirling.software.proprietary.security.config.PremiumEndpoint;
|
import stirling.software.proprietary.security.config.PremiumEndpoint;
|
||||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||||
|
import stirling.software.proprietary.security.model.User;
|
||||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||||
|
import stirling.software.proprietary.security.service.TeamService;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/api/v1/team")
|
@RequestMapping("/api/v1/team")
|
||||||
@ -54,6 +56,12 @@ public class TeamController {
|
|||||||
return new RedirectView("/adminSettings?messageType=teamNameExists");
|
return new RedirectView("/adminSettings?messageType=teamNameExists");
|
||||||
}
|
}
|
||||||
Team team = existing.get();
|
Team team = existing.get();
|
||||||
|
|
||||||
|
// Prevent renaming the Internal team
|
||||||
|
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||||
|
return new RedirectView("/adminSettings?messageType=internalTeamNotAccessible");
|
||||||
|
}
|
||||||
|
|
||||||
team.setName(newName);
|
team.setName(newName);
|
||||||
teamRepository.save(team);
|
teamRepository.save(team);
|
||||||
return new RedirectView("/adminSettings?messageType=teamRenamed");
|
return new RedirectView("/adminSettings?messageType=teamRenamed");
|
||||||
@ -69,6 +77,12 @@ public class TeamController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Team team = teamOpt.get();
|
Team team = teamOpt.get();
|
||||||
|
|
||||||
|
// Prevent deleting the Internal team
|
||||||
|
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||||
|
return new RedirectView("/adminSettings?messageType=internalTeamNotAccessible");
|
||||||
|
}
|
||||||
|
|
||||||
long memberCount = userRepository.countByTeam(team);
|
long memberCount = userRepository.countByTeam(team);
|
||||||
if (memberCount > 0) {
|
if (memberCount > 0) {
|
||||||
return new RedirectView("/adminSettings?messageType=teamHasUsers");
|
return new RedirectView("/adminSettings?messageType=teamHasUsers");
|
||||||
@ -77,4 +91,37 @@ public class TeamController {
|
|||||||
teamRepository.delete(team);
|
teamRepository.delete(team);
|
||||||
return new RedirectView("/adminSettings?messageType=teamDeleted");
|
return new RedirectView("/adminSettings?messageType=teamDeleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@PostMapping("/addUser")
|
||||||
|
@Transactional
|
||||||
|
public RedirectView addUserToTeam(
|
||||||
|
@RequestParam("teamId") Long teamId,
|
||||||
|
@RequestParam("userId") Long userId) {
|
||||||
|
|
||||||
|
// Find the team
|
||||||
|
Team team = teamRepository.findById(teamId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Team not found"));
|
||||||
|
|
||||||
|
// Prevent adding users to the Internal team
|
||||||
|
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||||
|
return new RedirectView("/teams?error=internalTeamNotAccessible");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the user
|
||||||
|
User user = userRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
|
|
||||||
|
// Check if user is in the Internal team - prevent moving them
|
||||||
|
if (user.getTeam() != null && user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||||
|
return new RedirectView("/teams/" + teamId + "?error=cannotMoveInternalUsers");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign user to team
|
||||||
|
user.setTeam(team);
|
||||||
|
userRepository.save(user);
|
||||||
|
|
||||||
|
// Redirect back to team details page
|
||||||
|
return new RedirectView("/teams/" + teamId + "?messageType=userAdded");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ import stirling.software.common.model.ApplicationProperties;
|
|||||||
import stirling.software.common.model.enumeration.Role;
|
import stirling.software.common.model.enumeration.Role;
|
||||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||||
import stirling.software.proprietary.model.Team;
|
import stirling.software.proprietary.model.Team;
|
||||||
|
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||||
import stirling.software.proprietary.security.model.User;
|
import stirling.software.proprietary.security.model.User;
|
||||||
import stirling.software.proprietary.security.model.api.user.UsernameAndPass;
|
import stirling.software.proprietary.security.model.api.user.UsernameAndPass;
|
||||||
@ -54,7 +55,7 @@ public class UserController {
|
|||||||
private final SessionPersistentRegistry sessionRegistry;
|
private final SessionPersistentRegistry sessionRegistry;
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private final TeamRepository teamRepository;
|
private final TeamRepository teamRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public String register(@ModelAttribute UsernameAndPass requestModel, Model model)
|
public String register(@ModelAttribute UsernameAndPass requestModel, Model model)
|
||||||
@ -210,6 +211,7 @@ public class UserController {
|
|||||||
@RequestParam(name = "username", required = true) String username,
|
@RequestParam(name = "username", required = true) String username,
|
||||||
@RequestParam(name = "password", required = false) String password,
|
@RequestParam(name = "password", required = false) String password,
|
||||||
@RequestParam(name = "role") String role,
|
@RequestParam(name = "role") String role,
|
||||||
|
@RequestParam(name = "teamId", required = false) Long teamId,
|
||||||
@RequestParam(name = "authType") String authType,
|
@RequestParam(name = "authType") String authType,
|
||||||
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
||||||
boolean forceChange)
|
boolean forceChange)
|
||||||
@ -243,14 +245,29 @@ public class UserController {
|
|||||||
// If the role ID is not valid, redirect with an error message
|
// If the role ID is not valid, redirect with an error message
|
||||||
return new RedirectView("/adminSettings?messageType=invalidRole", true);
|
return new RedirectView("/adminSettings?messageType=invalidRole", true);
|
||||||
}
|
}
|
||||||
Team team = teamRepository.findByName(TeamService.DEFAULT_TEAM_NAME).orElse(null);
|
|
||||||
|
// Use teamId if provided, otherwise use default team
|
||||||
|
Long effectiveTeamId = teamId;
|
||||||
|
if (effectiveTeamId == null) {
|
||||||
|
Team defaultTeam = teamRepository.findByName(TeamService.DEFAULT_TEAM_NAME).orElse(null);
|
||||||
|
if (defaultTeam != null) {
|
||||||
|
effectiveTeamId = defaultTeam.getId();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check if the selected team is Internal - prevent assigning to it
|
||||||
|
Team selectedTeam = teamRepository.findById(effectiveTeamId).orElse(null);
|
||||||
|
if (selectedTeam != null && selectedTeam.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||||
|
return new RedirectView("/adminSettings?messageType=internalTeamNotAccessible", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
|
if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
|
||||||
userService.saveUser(username, AuthenticationType.SSO, team, role);
|
userService.saveUser(username, AuthenticationType.SSO, effectiveTeamId, role);
|
||||||
} else {
|
} else {
|
||||||
if (password.isBlank()) {
|
if (password.isBlank()) {
|
||||||
return new RedirectView("/adminSettings?messageType=invalidPassword", true);
|
return new RedirectView("/adminSettings?messageType=invalidPassword", true);
|
||||||
}
|
}
|
||||||
userService.saveUser(username, password, team, role, forceChange);
|
userService.saveUser(username, password, effectiveTeamId, role, forceChange);
|
||||||
}
|
}
|
||||||
return new RedirectView(
|
return new RedirectView(
|
||||||
"/adminSettings", // Redirect to account page after adding the user
|
"/adminSettings", // Redirect to account page after adding the user
|
||||||
@ -262,6 +279,7 @@ public class UserController {
|
|||||||
public RedirectView changeRole(
|
public RedirectView changeRole(
|
||||||
@RequestParam(name = "username") String username,
|
@RequestParam(name = "username") String username,
|
||||||
@RequestParam(name = "role") String role,
|
@RequestParam(name = "role") String role,
|
||||||
|
@RequestParam(name = "teamId", required = false) Long teamId,
|
||||||
Authentication authentication)
|
Authentication authentication)
|
||||||
throws SQLException, UnsupportedProviderException {
|
throws SQLException, UnsupportedProviderException {
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
@ -289,6 +307,26 @@ public class UserController {
|
|||||||
return new RedirectView("/adminSettings?messageType=invalidRole", true);
|
return new RedirectView("/adminSettings?messageType=invalidRole", true);
|
||||||
}
|
}
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
|
|
||||||
|
// Update the team if a teamId is provided
|
||||||
|
if (teamId != null) {
|
||||||
|
Team team = teamRepository.findById(teamId).orElse(null);
|
||||||
|
if (team != null) {
|
||||||
|
// Prevent assigning to Internal team
|
||||||
|
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||||
|
return new RedirectView("/adminSettings?messageType=internalTeamNotAccessible", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent moving users from Internal team
|
||||||
|
if (user.getTeam() != null && user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||||
|
return new RedirectView("/adminSettings?messageType=cannotMoveInternalUsers", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
user.setTeam(team);
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
userService.changeRole(user, role);
|
userService.changeRole(user, role);
|
||||||
return new RedirectView(
|
return new RedirectView(
|
||||||
"/adminSettings", // Redirect to account page after adding the user
|
"/adminSettings", // Redirect to account page after adding the user
|
||||||
|
@ -2,7 +2,6 @@ package stirling.software.proprietary.security.controller.web;
|
|||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -14,27 +13,36 @@ import org.springframework.web.bind.annotation.PathVariable;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.proprietary.model.Team;
|
import stirling.software.proprietary.model.Team;
|
||||||
|
import stirling.software.proprietary.model.dto.TeamWithUserCountDTO;
|
||||||
import stirling.software.proprietary.security.database.repository.SessionRepository;
|
import stirling.software.proprietary.security.database.repository.SessionRepository;
|
||||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||||
import stirling.software.proprietary.security.model.User;
|
import stirling.software.proprietary.security.model.User;
|
||||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||||
|
import stirling.software.proprietary.security.service.TeamService;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/teams")
|
@RequestMapping("/teams")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
public class TeamWebController {
|
public class TeamWebController {
|
||||||
|
|
||||||
private final TeamRepository teamRepository;
|
private final TeamRepository teamRepository;
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final SessionRepository sessionRepository;
|
private final SessionRepository sessionRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
public String listTeams(Model model) {
|
public String listTeams(Model model) {
|
||||||
// Get all teams with their users
|
// Get teams with user counts using a DTO projection
|
||||||
List<Team> teams = teamRepository.findAllWithUsers();
|
List<TeamWithUserCountDTO> allTeamsWithCounts = teamRepository.findAllTeamsWithUserCount();
|
||||||
|
|
||||||
|
// Filter out the Internal team
|
||||||
|
List<TeamWithUserCountDTO> teamsWithCounts = allTeamsWithCounts.stream()
|
||||||
|
.filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME))
|
||||||
|
.toList();
|
||||||
|
|
||||||
// Get the latest activity for each team
|
// Get the latest activity for each team
|
||||||
List<Object[]> teamActivities = sessionRepository.findLatestActivityByTeam();
|
List<Object[]> teamActivities = sessionRepository.findLatestActivityByTeam();
|
||||||
@ -42,46 +50,56 @@ public class TeamWebController {
|
|||||||
// Convert the query results to a map for easy access in the view
|
// Convert the query results to a map for easy access in the view
|
||||||
Map<Long, Date> teamLastRequest = new HashMap<>();
|
Map<Long, Date> teamLastRequest = new HashMap<>();
|
||||||
for (Object[] result : teamActivities) {
|
for (Object[] result : teamActivities) {
|
||||||
// For JPQL query with aliases
|
|
||||||
Long teamId = (Long) result[0]; // teamId alias
|
Long teamId = (Long) result[0]; // teamId alias
|
||||||
Date lastActivity = (Date) result[1]; // lastActivity alias
|
Date lastActivity = (Date) result[1]; // lastActivity alias
|
||||||
|
|
||||||
teamLastRequest.put(teamId, lastActivity);
|
teamLastRequest.put(teamId, lastActivity);
|
||||||
}
|
}
|
||||||
|
|
||||||
model.addAttribute("teams", teams);
|
// Add data to the model
|
||||||
|
model.addAttribute("teamsWithCounts", teamsWithCounts);
|
||||||
model.addAttribute("teamLastRequest", teamLastRequest);
|
model.addAttribute("teamLastRequest", teamLastRequest);
|
||||||
return "enterprise/teams";
|
|
||||||
|
return "accounts/teams";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
public String viewTeamDetails(@PathVariable("id") Long id, Model model) {
|
public String viewTeamDetails(@PathVariable("id") Long id, Model model) {
|
||||||
// Get the team with its users
|
// Get the team
|
||||||
Team team =
|
Team team = teamRepository.findById(id)
|
||||||
teamRepository
|
.orElseThrow(() -> new RuntimeException("Team not found"));
|
||||||
.findById(id)
|
|
||||||
.orElseThrow(() -> new RuntimeException("Team not found"));
|
|
||||||
|
|
||||||
List<User> members = userRepository.findAllByTeam(team);
|
// Prevent access to Internal team
|
||||||
team.setUsers(new HashSet<>(members));
|
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||||
|
return "redirect:/teams?error=internalTeamNotAccessible";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get users for this team directly using the direct query
|
||||||
|
List<User> teamUsers = userRepository.findAllByTeamId(id);
|
||||||
|
|
||||||
|
// Get all users not in this team for the Add User to Team dropdown
|
||||||
|
// Exclude users that are in the Internal team
|
||||||
|
List<User> allUsers = userRepository.findAllWithTeam();
|
||||||
|
List<User> availableUsers = allUsers.stream()
|
||||||
|
.filter(user -> (user.getTeam() == null || !user.getTeam().getId().equals(id)) &&
|
||||||
|
(user.getTeam() == null || !user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)))
|
||||||
|
.toList();
|
||||||
|
|
||||||
// Get the latest session for each user in the team
|
// Get the latest session for each user in the team
|
||||||
List<Object[]> userSessions = sessionRepository.findLatestSessionByTeamId(id);
|
List<Object[]> userSessions = sessionRepository.findLatestSessionByTeamId(id);
|
||||||
|
|
||||||
// Create a map of username to last request date
|
// Create a map of username to last request date
|
||||||
Map<String, Date> userLastRequest = new HashMap<>();
|
Map<String, Date> userLastRequest = new HashMap<>();
|
||||||
|
|
||||||
// Process results from JPQL query
|
|
||||||
for (Object[] result : userSessions) {
|
for (Object[] result : userSessions) {
|
||||||
String username = (String) result[0]; // username alias
|
String username = (String) result[0]; // username alias
|
||||||
Date lastRequest = (Date) result[1]; // lastRequest alias
|
Date lastRequest = (Date) result[1]; // lastRequest alias
|
||||||
|
|
||||||
userLastRequest.put(username, lastRequest);
|
userLastRequest.put(username, lastRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
model.addAttribute("team", team);
|
model.addAttribute("team", team);
|
||||||
|
model.addAttribute("teamUsers", teamUsers);
|
||||||
|
model.addAttribute("availableUsers", availableUsers);
|
||||||
model.addAttribute("userLastRequest", userLastRequest);
|
model.addAttribute("userLastRequest", userLastRequest);
|
||||||
return "enterprise/team-details";
|
return "accounts/team-details";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,9 +27,12 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
|||||||
@Query("SELECT u FROM User u WHERE u.team IS NULL")
|
@Query("SELECT u FROM User u WHERE u.team IS NULL")
|
||||||
List<User> findAllWithoutTeam();
|
List<User> findAllWithoutTeam();
|
||||||
|
|
||||||
@Query("SELECT u FROM User u LEFT JOIN FETCH u.team")
|
@Query(value = "SELECT u FROM User u LEFT JOIN FETCH u.team")
|
||||||
List<User> findAllWithTeam();
|
List<User> findAllWithTeam();
|
||||||
|
|
||||||
|
@Query("SELECT u FROM User u JOIN FETCH u.authorities JOIN FETCH u.team WHERE u.team.id = :teamId")
|
||||||
|
List<User> findAllByTeamId(@Param("teamId") Long teamId);
|
||||||
|
|
||||||
long countByTeam(Team team);
|
long countByTeam(Team team);
|
||||||
|
|
||||||
List<User> findAllByTeam(Team team);
|
List<User> findAllByTeam(Team team);
|
||||||
|
@ -5,16 +5,19 @@ import java.util.Optional;
|
|||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import stirling.software.proprietary.model.Team;
|
import stirling.software.proprietary.model.Team;
|
||||||
|
import stirling.software.proprietary.model.dto.TeamWithUserCountDTO;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface TeamRepository extends JpaRepository<Team, Long> {
|
public interface TeamRepository extends JpaRepository<Team, Long> {
|
||||||
Optional<Team> findByName(String name);
|
Optional<Team> findByName(String name);
|
||||||
|
|
||||||
@Query("SELECT t FROM Team t LEFT JOIN FETCH t.users")
|
@Query("SELECT new stirling.software.proprietary.model.dto.TeamWithUserCountDTO(t.id, t.name, COUNT(u)) " +
|
||||||
List<Team> findAllWithUsers();
|
"FROM Team t LEFT JOIN t.users u GROUP BY t.id, t.name")
|
||||||
|
List<TeamWithUserCountDTO> findAllTeamsWithUserCount();
|
||||||
|
|
||||||
boolean existsByNameIgnoreCase(String name);
|
boolean existsByNameIgnoreCase(String name);
|
||||||
}
|
}
|
||||||
|
@ -330,6 +330,32 @@
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal close button styling */
|
||||||
|
.data-btn-close {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--md-sys-color-surface-variant);
|
||||||
|
color: var(--md-sys-color-on-surface-variant);
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-btn-close:hover {
|
||||||
|
background-color: var(--md-sys-color-surface-container-high);
|
||||||
|
color: var(--md-sys-color-on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-btn-close .material-symbols-rounded {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.data-modal-body {
|
.data-modal-body {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,196 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<th:block th:insert="~{fragments/common :: head(title=#{team.details.title}, header=#{team.details.header})}"></th:block>
|
||||||
|
<link rel="stylesheet" th:href="@{/css/modern-tables.css}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||||
|
<div id="page-container">
|
||||||
|
<div id="content-wrap">
|
||||||
|
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||||
|
|
||||||
|
<div class="data-container">
|
||||||
|
<div class="data-panel">
|
||||||
|
<div class="data-header">
|
||||||
|
<h1 class="data-title">
|
||||||
|
<span class="data-icon">
|
||||||
|
<span class="material-symbols-rounded">group</span>
|
||||||
|
</span>
|
||||||
|
<span th:text="'Team: ' + ${team.name}">Team Name</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="data-body">
|
||||||
|
<div class="data-stats">
|
||||||
|
<div class="data-stat-card">
|
||||||
|
<div class="data-stat-label">Total Members:</div>
|
||||||
|
<div class="data-stat-value" th:text="${teamUsers.size()}">1</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="data-actions data-actions-start">
|
||||||
|
<a th:href="@{'/teams'}" class="data-btn data-btn-secondary">
|
||||||
|
<span class="material-symbols-rounded">arrow_back</span>
|
||||||
|
<span th:text="#{team.back}">Back to Teams</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="data-section-title">Members</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th scope="col" th:title="${@runningProOrHigher} ? #{adminUserSettings.lastRequest} : 'Pro feature'" class="text-overflow" th:text="#{adminUserSettings.lastRequest}">Last Request</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="user : ${teamUsers}">
|
||||||
|
<td th:text="${user.id}">1</td>
|
||||||
|
<td th:text="${user.username}">username</td>
|
||||||
|
<td th:text="#{${user.roleName}}">Role</td>
|
||||||
|
<td th:text="${@runningProOrHigher} ? (${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}) : 'hidden'">2023-01-01 12:00:00</td>
|
||||||
|
<td>
|
||||||
|
<span th:if="${user.enabled}" class="data-status data-status-success">
|
||||||
|
<span class="material-symbols-rounded">person</span>
|
||||||
|
Enabled
|
||||||
|
</span>
|
||||||
|
<span th:unless="${user.enabled}" class="data-status data-status-danger">
|
||||||
|
<span class="material-symbols-rounded">person_off</span>
|
||||||
|
Disabled
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty state for when there are no team members -->
|
||||||
|
<div th:if="${teamUsers.empty}" class="data-empty">
|
||||||
|
<span class="material-symbols-rounded data-empty-icon">person_off</span>
|
||||||
|
<p class="data-empty-text">This team has no members yet.</p>
|
||||||
|
<button data-bs-toggle="modal" data-bs-target="#addUserToTeamModal" class="data-btn data-btn-primary">
|
||||||
|
<span class="material-symbols-rounded">person_add</span>
|
||||||
|
<span th:text="#{team.addUser}">Add User to Team</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add button for non-empty teams too -->
|
||||||
|
<div th:if="${!teamUsers.empty}" class="data-actions data-mt-3">
|
||||||
|
<button data-bs-toggle="modal" data-bs-target="#addUserToTeamModal" class="data-btn data-btn-primary">
|
||||||
|
<span class="material-symbols-rounded">person_add</span>
|
||||||
|
<span th:text="#{team.addUser}">Add User to Team</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JavaScript for team warning -->
|
||||||
|
<script th:inline="javascript">
|
||||||
|
function checkUserTeam(userId) {
|
||||||
|
// Clear any existing warning
|
||||||
|
const warningDiv = document.getElementById('teamChangeWarning');
|
||||||
|
const warningMessage = document.getElementById('warningMessage');
|
||||||
|
const submitButton = document.getElementById('addUserSubmitBtn');
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
warningDiv.style.display = 'none';
|
||||||
|
submitButton.onclick = null;
|
||||||
|
|
||||||
|
// Get the selected option
|
||||||
|
const selectedOption = document.querySelector('#userId option[value="' + userId + '"]');
|
||||||
|
if (!selectedOption) return;
|
||||||
|
|
||||||
|
// Get team data
|
||||||
|
const currentTeam = selectedOption.getAttribute('data-team');
|
||||||
|
const currentTeamId = selectedOption.getAttribute('data-team-id');
|
||||||
|
const newTeamName = /*[[${team.name}]]*/ 'Current Team';
|
||||||
|
|
||||||
|
// If user is already in a team, show warning
|
||||||
|
if (currentTeam && currentTeam.length > 0) {
|
||||||
|
// Use internationalized message
|
||||||
|
const warningTemplate = /*[[#{team.warning.moveUser}]]*/ 'Warning: This will move the user from "{0}" team to "{1}" team. Are you sure?';
|
||||||
|
const formattedWarning = warningTemplate.replace('{0}', currentTeam).replace('{1}', newTeamName);
|
||||||
|
warningMessage.textContent = formattedWarning;
|
||||||
|
warningDiv.style.display = 'block';
|
||||||
|
|
||||||
|
// Add confirmation to submit button
|
||||||
|
submitButton.onclick = function(e) {
|
||||||
|
// Use internationalized message
|
||||||
|
const confirmTemplate = /*[[#{team.confirm.moveUser}]]*/ 'Are you sure you want to move this user from "{0}" team to "{1}" team?';
|
||||||
|
const formattedConfirm = confirmTemplate.replace('{0}', currentTeam).replace('{1}', newTeamName);
|
||||||
|
if (!confirm(formattedConfirm)) {
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Add User to Team Modal -->
|
||||||
|
<div class="modal fade" id="addUserToTeamModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<form th:action="@{'/api/v1/team/addUser'}" method="post" class="modal-content data-modal">
|
||||||
|
<div class="data-modal-header">
|
||||||
|
<h5 class="data-modal-title">
|
||||||
|
<span class="data-icon">
|
||||||
|
<span class="material-symbols-rounded">person_add</span>
|
||||||
|
</span>
|
||||||
|
<span th:text="#{team.addUser}">Add User to Team</span>
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||||
|
<span class="material-symbols-rounded">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="data-modal-body">
|
||||||
|
<input type="hidden" name="teamId" th:value="${team.id}" />
|
||||||
|
|
||||||
|
<div class="data-form-group">
|
||||||
|
<label for="userId" class="data-form-label" th:text="#{team.selectUser}">Select User</label>
|
||||||
|
<select name="userId" id="userId" class="data-form-control" required onchange="checkUserTeam(this.value)">
|
||||||
|
<option value="" disabled selected th:text="#{selectFillter}">-- Select User --</option>
|
||||||
|
<option th:each="user : ${availableUsers}"
|
||||||
|
th:value="${user.id}"
|
||||||
|
th:text="${user.username}"
|
||||||
|
th:data-team="${user.team != null ? user.team.name : ''}"
|
||||||
|
th:data-team-id="${user.team != null ? user.team.id : ''}">
|
||||||
|
Username
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Warning message for users being moved between teams -->
|
||||||
|
<div id="teamChangeWarning" class="alert alert-warning mt-3" style="display: none;">
|
||||||
|
<span class="material-symbols-rounded">warning</span>
|
||||||
|
<span id="warningMessage">Warning: This will move the user from their current team to this team.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="data-form-actions">
|
||||||
|
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
|
||||||
|
<span class="material-symbols-rounded">close</span>
|
||||||
|
<span th:text="#{cancel}">Cancel</span>
|
||||||
|
</button>
|
||||||
|
<button type="submit" id="addUserSubmitBtn" class="data-btn data-btn-primary">
|
||||||
|
<span class="material-symbols-rounded">check</span>
|
||||||
|
<span th:text="#{team.addUser}">Add User</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,4 +1,3 @@
|
|||||||
<!-- templates/enterprise/teams.html -->
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||||
<head>
|
<head>
|
||||||
@ -23,6 +22,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="data-body">
|
<div class="data-body">
|
||||||
|
<!-- Back Button -->
|
||||||
|
<div class="data-actions data-actions-start">
|
||||||
|
<a href="/adminSettings" class="data-btn data-btn-secondary">
|
||||||
|
<span class="material-symbols-rounded">arrow_back</span>
|
||||||
|
<span th:text="#{back.toSettings}">Back to Settings</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Create New Team Button -->
|
<!-- Create New Team Button -->
|
||||||
<div class="data-actions">
|
<div class="data-actions">
|
||||||
<a href="#"
|
<a href="#"
|
||||||
@ -47,18 +54,19 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="team : ${teams}">
|
<!-- Try approach 1 - DTO projection -->
|
||||||
<td th:text="${team.name}"></td>
|
<tr th:each="teamDto : ${teamsWithCounts}">
|
||||||
<td th:text="${team.users.size()}"></td>
|
<td th:text="${teamDto.name}"></td>
|
||||||
<td th:text="${@runningProOrHigher} ? (${teamLastRequest[team.id] != null ? #dates.format(teamLastRequest[team.id], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}) : 'hidden'"></td>
|
<td th:text="${teamDto.userCount}"></td>
|
||||||
|
<td th:text="${@runningProOrHigher} ? (${teamLastRequest[teamDto.id] != null ? #dates.format(teamLastRequest[teamDto.id], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}) : 'hidden'"></td>
|
||||||
<td>
|
<td>
|
||||||
<div class="data-action-cell">
|
<div class="data-action-cell">
|
||||||
<a th:href="@{'/teams/' + ${team.id}}" class="data-btn data-btn-secondary data-btn-sm" th:title="#{adminUserSettings.viewTeam}">
|
<a th:href="@{'/teams/' + ${teamDto.id}}" class="data-btn data-btn-secondary data-btn-sm" th:title="#{adminUserSettings.viewTeam}">
|
||||||
<span class="material-symbols-rounded">search</span> <span th:text="#{view}">View</span>
|
<span class="material-symbols-rounded">search</span> <span th:text="#{view}">View</span>
|
||||||
</a>
|
</a>
|
||||||
<form th:action="@{'/api/v1/team/delete'}" method="post" style="display:inline-block"
|
<form th:action="@{'/api/v1/team/delete'}" method="post" style="display:inline-block"
|
||||||
onsubmit="return confirmDeleteTeam()">
|
onsubmit="return confirmDeleteTeam()">
|
||||||
<input type="hidden" name="teamId" th:value="${team.id}" />
|
<input type="hidden" name="teamId" th:value="${teamDto.id}" />
|
||||||
<button type="submit" class="data-btn data-btn-danger data-btn-sm"
|
<button type="submit" class="data-btn data-btn-danger data-btn-sm"
|
||||||
th:disabled="${!@runningProOrHigher}"
|
th:disabled="${!@runningProOrHigher}"
|
||||||
th:classappend="${!@runningProOrHigher} ? 'disabled' : ''"
|
th:classappend="${!@runningProOrHigher} ? 'disabled' : ''"
|
@ -1,95 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
|
||||||
<head>
|
|
||||||
<th:block th:insert="~{fragments/common :: head(title=#{team.details.title}, header=#{team.details.header})}"></th:block>
|
|
||||||
<link rel="stylesheet" th:href="@{/css/modern-tables.css}">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
|
||||||
<div id="page-container">
|
|
||||||
<div id="content-wrap">
|
|
||||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
|
||||||
|
|
||||||
<div class="data-container">
|
|
||||||
<div class="data-panel">
|
|
||||||
<div class="data-header">
|
|
||||||
<h1 class="data-title">
|
|
||||||
<span class="data-icon">
|
|
||||||
<span class="material-symbols-rounded">group</span>
|
|
||||||
</span>
|
|
||||||
<span th:text="'Team: ' + ${team.name}">Team Name</span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="data-body">
|
|
||||||
<div class="data-stats">
|
|
||||||
<div class="data-stat-card">
|
|
||||||
<div class="data-stat-label">Team ID:</div>
|
|
||||||
<div class="data-stat-value" th:text="${team.id}">1</div>
|
|
||||||
</div>
|
|
||||||
<div class="data-stat-card">
|
|
||||||
<div class="data-stat-label">Total Members:</div>
|
|
||||||
<div class="data-stat-value" th:text="${team.users.size()}">1</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="data-actions data-actions-start">
|
|
||||||
<a th:href="@{'/teams'}" class="data-btn data-btn-secondary">
|
|
||||||
<span class="material-symbols-rounded">arrow_back</span>
|
|
||||||
<span>Back to Teams</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="data-section-title">Members</div>
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Username</th>
|
|
||||||
<th>Role</th>
|
|
||||||
<th scope="col" th:title="${@runningProOrHigher} ? #{adminUserSettings.lastRequest} : 'Pro feature'" class="text-overflow" th:text="#{adminUserSettings.lastRequest}">Last Request</th>
|
|
||||||
<th>Status</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr th:each="user : ${team.users}">
|
|
||||||
<td th:text="${user.id}">1</td>
|
|
||||||
<td th:text="${user.username}">username</td>
|
|
||||||
<td th:text="#{${user.roleName}}">Role</td>
|
|
||||||
<td th:text="${@runningProOrHigher} ? (${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}) : 'hidden'">2023-01-01 12:00:00</td>
|
|
||||||
<td>
|
|
||||||
<span th:if="${user.enabled}" class="data-status data-status-success">
|
|
||||||
<span class="material-symbols-rounded">person</span>
|
|
||||||
Enabled
|
|
||||||
</span>
|
|
||||||
<span th:unless="${user.enabled}" class="data-status data-status-danger">
|
|
||||||
<span class="material-symbols-rounded">person_off</span>
|
|
||||||
Disabled
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Empty state for when there are no team members -->
|
|
||||||
<div th:if="${team.users.empty}" class="data-empty">
|
|
||||||
<span class="material-symbols-rounded data-empty-icon">person_off</span>
|
|
||||||
<p class="data-empty-text">This team has no members yet.</p>
|
|
||||||
<a th:href="@{'/admin/users'}" class="data-btn data-btn-primary">
|
|
||||||
<span class="material-symbols-rounded">person_add</span>
|
|
||||||
<span>Add Users to Team</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,43 +1,3 @@
|
|||||||
account.adminTitle=Administrator Tools
|
|
||||||
account.adminNotif=You have admin privileges. Access system settings and user management.
|
|
||||||
view=View
|
|
||||||
cancel=Cancel
|
|
||||||
adminUserSettings.teams=View/Edit Teams
|
|
||||||
adminUserSettings.team=Team
|
|
||||||
adminUserSettings.manageTeams=Manage Teams
|
|
||||||
adminUserSettings.createTeam=Create Team
|
|
||||||
adminUserSettings.viewTeam=View Team
|
|
||||||
adminUserSettings.deleteTeam=Delete Team
|
|
||||||
adminUserSettings.teamName=Team Name
|
|
||||||
adminUserSettings.teamExists=Team already exists
|
|
||||||
adminUserSettings.teamCreated=Team created successfully
|
|
||||||
adminUserSettings.teamChanged=User's team was updated
|
|
||||||
adminUserSettings.totalMembers=Total Members
|
|
||||||
|
|
||||||
teamCreated=Team created successfully
|
|
||||||
teamExists=A team with that name already exists
|
|
||||||
teamNameExists=Another team with that name already exists
|
|
||||||
teamNotFound=Team not found
|
|
||||||
teamDeleted=Team deleted
|
|
||||||
teamHasUsers=Cannot delete a team with users assigned
|
|
||||||
teamRenamed=Team renamed successfully
|
|
||||||
merge.generateToc=Generate table of contents in the merged file?
|
|
||||||
# Table of Contents Feature
|
|
||||||
home.editTableOfContents.title=Edit Table of Contents
|
|
||||||
home.editTableOfContents.desc=Add or edit bookmarks and table of contents in PDF documents
|
|
||||||
editTableOfContents.tags=bookmarks,toc,navigation,index,table of contents,chapters,sections,outline
|
|
||||||
editTableOfContents.title=Edit Table of Contents
|
|
||||||
editTableOfContents.header=Add or Edit PDF Table of Contents
|
|
||||||
editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to append to existing)
|
|
||||||
editTableOfContents.editorTitle=Bookmark Editor
|
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
|
||||||
editTableOfContents.submit=Apply Table of Contents
|
|
||||||
|
|
||||||
|
|
||||||
###########
|
###########
|
||||||
# Generic #
|
# Generic #
|
||||||
###########
|
###########
|
||||||
@ -259,6 +219,12 @@ addToDoc=Add to Document
|
|||||||
reset=Reset
|
reset=Reset
|
||||||
apply=Apply
|
apply=Apply
|
||||||
noFileSelected=No file selected. Please upload one.
|
noFileSelected=No file selected. Please upload one.
|
||||||
|
view=View
|
||||||
|
cancel=Cancel
|
||||||
|
|
||||||
|
back.toSettings=Back to Settings
|
||||||
|
back.toHome=Back to Home
|
||||||
|
back.toAdmin=Back to Admin
|
||||||
|
|
||||||
legal.privacy=Privacy Policy
|
legal.privacy=Privacy Policy
|
||||||
legal.terms=Terms and Conditions
|
legal.terms=Terms and Conditions
|
||||||
@ -380,6 +346,8 @@ account.property=Property
|
|||||||
account.webBrowserSettings=Web Browser Setting
|
account.webBrowserSettings=Web Browser Setting
|
||||||
account.syncToBrowser=Sync Account -> Browser
|
account.syncToBrowser=Sync Account -> Browser
|
||||||
account.syncToAccount=Sync Account <- Browser
|
account.syncToAccount=Sync Account <- Browser
|
||||||
|
account.adminTitle=Administrator Tools
|
||||||
|
account.adminNotif=You have admin privileges. Access system settings and user management.
|
||||||
|
|
||||||
|
|
||||||
adminUserSettings.title=User Control Settings
|
adminUserSettings.title=User Control Settings
|
||||||
@ -411,6 +379,39 @@ adminUserSettings.disabledUsers=Disabled Users:
|
|||||||
adminUserSettings.totalUsers=Total Users:
|
adminUserSettings.totalUsers=Total Users:
|
||||||
adminUserSettings.lastRequest=Last Request
|
adminUserSettings.lastRequest=Last Request
|
||||||
adminUserSettings.usage=View Usage
|
adminUserSettings.usage=View Usage
|
||||||
|
adminUserSettings.teams=View/Edit Teams
|
||||||
|
adminUserSettings.team=Team
|
||||||
|
adminUserSettings.manageTeams=Manage Teams
|
||||||
|
adminUserSettings.createTeam=Create Team
|
||||||
|
adminUserSettings.viewTeam=View Team
|
||||||
|
adminUserSettings.deleteTeam=Delete Team
|
||||||
|
adminUserSettings.teamName=Team Name
|
||||||
|
adminUserSettings.teamExists=Team already exists
|
||||||
|
adminUserSettings.teamCreated=Team created successfully
|
||||||
|
adminUserSettings.teamChanged=User's team was updated
|
||||||
|
adminUserSettings.totalMembers=Total Members
|
||||||
|
adminUserSettings.confirmDeleteTeam=Are you sure you want to delete this team?
|
||||||
|
|
||||||
|
teamCreated=Team created successfully
|
||||||
|
teamExists=A team with that name already exists
|
||||||
|
teamNameExists=Another team with that name already exists
|
||||||
|
teamNotFound=Team not found
|
||||||
|
teamDeleted=Team deleted
|
||||||
|
teamHasUsers=Cannot delete a team with users assigned
|
||||||
|
teamRenamed=Team renamed successfully
|
||||||
|
|
||||||
|
# Team user management
|
||||||
|
team.addUser=Add User to Team
|
||||||
|
team.selectUser=Select User
|
||||||
|
team.warning.moveUser=Warning: This will move the user from "{0}" team to "{1}" team. Are you sure?
|
||||||
|
team.confirm.moveUser=Are you sure you want to move this user from "{0}" team to "{1}" team?
|
||||||
|
team.userAdded=User successfully added to team
|
||||||
|
team.back=Back to Teams
|
||||||
|
team.internal=Internal Team
|
||||||
|
team.internalTeamNotAccessible=The Internal team is a system team and cannot be accessed
|
||||||
|
team.cannotMoveInternalUsers=Users in the Internal team cannot be moved to other teams
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
endpointStatistics.title=Endpoint Statistics
|
endpointStatistics.title=Endpoint Statistics
|
||||||
endpointStatistics.header=Endpoint Statistics
|
endpointStatistics.header=Endpoint Statistics
|
||||||
@ -1199,6 +1200,7 @@ merge.header=Merge multiple PDFs (2+)
|
|||||||
merge.sortByName=Sort by name
|
merge.sortByName=Sort by name
|
||||||
merge.sortByDate=Sort by date
|
merge.sortByDate=Sort by date
|
||||||
merge.removeCertSign=Remove digital signature in the merged file?
|
merge.removeCertSign=Remove digital signature in the merged file?
|
||||||
|
merge.generateToc=Generate table of contents in the merged file?
|
||||||
merge.submit=Merge
|
merge.submit=Merge
|
||||||
|
|
||||||
|
|
||||||
@ -1683,3 +1685,22 @@ fakeScan.blur=Blur
|
|||||||
fakeScan.noise=Noise
|
fakeScan.noise=Noise
|
||||||
fakeScan.yellowish=Yellowish (simulate old paper)
|
fakeScan.yellowish=Yellowish (simulate old paper)
|
||||||
fakeScan.resolution=Resolution (DPI)
|
fakeScan.resolution=Resolution (DPI)
|
||||||
|
|
||||||
|
|
||||||
|
# Table of Contents Feature
|
||||||
|
home.editTableOfContents.title=Edit Table of Contents
|
||||||
|
home.editTableOfContents.desc=Add or edit bookmarks and table of contents in PDF documents
|
||||||
|
|
||||||
|
editTableOfContents.tags=bookmarks,toc,navigation,index,table of contents,chapters,sections,outline
|
||||||
|
editTableOfContents.title=Edit Table of Contents
|
||||||
|
editTableOfContents.header=Add or Edit PDF Table of Contents
|
||||||
|
editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to append to existing)
|
||||||
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
editTableOfContents.submit=Apply Table of Contents
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user