diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 614ef4f7..9998ea8d 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -8,6 +8,7 @@ requires java.prefs; requires org.json; requires java.base; + requires java.desktop; // Http requires java.net.http; @@ -43,6 +44,7 @@ opens org.chainoptim.desktop.core.user.controller to javafx.fxml, com.google.guice; opens org.chainoptim.desktop.core.user.service to com.google.guice; opens org.chainoptim.desktop.core.user.model to com.fasterxml.jackson.databind; + opens org.chainoptim.desktop.core.user.dto to com.fasterxml.jackson.databind; // - Organization opens org.chainoptim.desktop.core.organization.service to com.google.guice; diff --git a/src/main/java/org/chainoptim/desktop/core/main/service/NavigationServiceImpl.java b/src/main/java/org/chainoptim/desktop/core/main/service/NavigationServiceImpl.java index 03475b66..50ba5b76 100644 --- a/src/main/java/org/chainoptim/desktop/core/main/service/NavigationServiceImpl.java +++ b/src/main/java/org/chainoptim/desktop/core/main/service/NavigationServiceImpl.java @@ -49,7 +49,9 @@ public NavigationServiceImpl(FXMLLoaderService fxmlLoaderService, private final Map viewMap = Map.ofEntries( Map.entry("Overview", "/org/chainoptim/desktop/core/main/OverviewView.fxml"), + Map.entry("Organization", "/org/chainoptim/desktop/core/organization/OrganizationView.fxml"), + Map.entry("Add-New-Members", "/org/chainoptim/desktop/core/organization/AddNewMembersView.fxml"), Map.entry("Products", "/org/chainoptim/desktop/features/product/ProductsView.fxml"), Map.entry("Product", "/org/chainoptim/desktop/features/product/ProductView.fxml"), diff --git a/src/main/java/org/chainoptim/desktop/core/organization/controller/AddNewMembersController.java b/src/main/java/org/chainoptim/desktop/core/organization/controller/AddNewMembersController.java new file mode 100644 index 00000000..afae4f43 --- /dev/null +++ b/src/main/java/org/chainoptim/desktop/core/organization/controller/AddNewMembersController.java @@ -0,0 +1,90 @@ +package org.chainoptim.desktop.core.organization.controller; + +import org.chainoptim.desktop.core.abstraction.ControllerFactory; +import org.chainoptim.desktop.core.main.service.CurrentSelectionService; +import org.chainoptim.desktop.core.main.service.NavigationService; +import org.chainoptim.desktop.core.organization.service.OrganizationService; +import org.chainoptim.desktop.shared.fallback.FallbackManager; +import org.chainoptim.desktop.shared.util.resourceloader.FXMLLoaderService; +import com.google.inject.Inject; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.Node; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; + +import java.net.URL; +import java.util.ResourceBundle; + +public class AddNewMembersController implements Initializable { + + private final OrganizationService organizationService; + private final NavigationService navigationService; + private final CurrentSelectionService currentSelectionService; + private final FXMLLoaderService fxmlLoaderService; + private final ControllerFactory controllerFactory; + private final FallbackManager fallbackManager; + + private Integer organizationId; + + @FXML + private StackPane usersSelectionContainer; + @FXML + private StackPane fallbackContainer; + + @Inject + public AddNewMembersController(OrganizationService organizationService, + NavigationService navigationService, + CurrentSelectionService currentSelectionService, + FXMLLoaderService fxmlLoaderService, + ControllerFactory controllerFactory, + FallbackManager fallbackManager) { + this.organizationService = organizationService; + this.navigationService = navigationService; + this.currentSelectionService = currentSelectionService; + this.fxmlLoaderService = fxmlLoaderService; + this.controllerFactory = controllerFactory; + this.fallbackManager = fallbackManager; + } + + @Override + public void initialize(URL location, ResourceBundle resourceBundle) { + loadFallbackManager(); + loadUsersSearch(); + setupListeners(); + + Integer receivedOrganizationId = currentSelectionService.getSelectedId(); + if (receivedOrganizationId != null) { + System.out.println("Organization ID: " + receivedOrganizationId); + this.organizationId = receivedOrganizationId; + } else { + fallbackManager.setErrorMessage("Failed to load organization."); + } + } + + private void loadFallbackManager() { + // Load view into fallbackContainer + Node fallbackView = fxmlLoaderService.loadView( + "/org/chainoptim/desktop/shared/fallback/FallbackManagerView.fxml", + controllerFactory::createController + ); + fallbackContainer.getChildren().add(fallbackView); + } + + private void loadUsersSearch() { + Node usersSearchView = fxmlLoaderService.loadView( + "/org/chainoptim/desktop/core/user/PublicUsersSearchAndSelectionView.fxml", + controllerFactory::createController + ); + usersSelectionContainer.getChildren().add(usersSearchView); + } + + private void setupListeners() { + fallbackManager.isEmptyProperty().addListener((observable, oldValue, newValue) -> { + usersSelectionContainer.setVisible(newValue); + usersSelectionContainer.setManaged(newValue); + fallbackContainer.setVisible(!newValue); + fallbackContainer.setManaged(!newValue); + }); + } +} diff --git a/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationAssignBasicRoleController.java b/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationAssignBasicRoleController.java new file mode 100644 index 00000000..68b3fde0 --- /dev/null +++ b/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationAssignBasicRoleController.java @@ -0,0 +1,155 @@ +package org.chainoptim.desktop.core.organization.controller; + +import org.chainoptim.desktop.core.organization.model.CustomRole; +import org.chainoptim.desktop.core.organization.model.FeaturePermissions; +import org.chainoptim.desktop.core.user.model.User; +import org.chainoptim.desktop.shared.confirmdialog.controller.GenericConfirmDialogActionListener; +import org.chainoptim.desktop.shared.util.DataReceiver; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; +import javafx.util.Pair; +import lombok.Setter; + +import java.util.List; +import java.util.Objects; + +import static org.chainoptim.desktop.core.user.model.User.Role.ADMIN; + +public class OrganizationAssignBasicRoleController implements DataReceiver { + + // State + private User user; + private User.Role originalSelectedRole; + private User.Role selectedRole; + private boolean hasRenderedWarning; + + // Listeners + @Setter + private GenericConfirmDialogActionListener> actionListener; // userId, basic role + + // FXML + @FXML + private Label titleLabel; + @FXML + private VBox rolesVBox; + @FXML + private TextFlow messageTextFlowContainer; + @FXML + private VBox permissionsVBox; + @FXML + private Button confirmButton; + @FXML + private Button cancelButton; + + @Override + public void setData(User data) { + user = data; + originalSelectedRole = user.getRole(); + selectedRole = originalSelectedRole; + + initializeUI(); + } + + private void initializeUI() { + titleLabel.setText("Assign role to " + user.getUsername()); + + for (User.Role role : User.Role.values()) { + String label = role.toString(); + label = label.substring(0, 1).toUpperCase() + label.substring(1).toLowerCase(); + Button selectRoleButton = new Button(label); + boolean isSelected = role.equals(selectedRole); + styleSelectButton(selectRoleButton, isSelected); + selectRoleButton.setOnAction(this::selectRole); + + rolesVBox.getChildren().add(selectRoleButton); + } + } + + private void selectRole(ActionEvent event) { + Button source = (Button) event.getSource(); + + // Select role, accounting for None button + int roleIndex = rolesVBox.getChildren().indexOf(source); + selectedRole = User.Role.values()[roleIndex]; + styleSelectButton(source, true); + + // Deselect other roles + for (int i = 0; i < rolesVBox.getChildren().size(); i++) { + if (i != roleIndex) { + Node otherButton = rolesVBox.getChildren().get(i); + if (otherButton instanceof Button button) { + styleSelectButton(button, false); + } + } + } + + // Render permissions to be granted + renderPermissionsVBox(); + + // On first selection, render warning message + if (hasRenderedWarning) return; + String message = "Are you sure you want to assign this role to " + user.getUsername() + "? "; + if (user.getCustomRole() != null) { + message += "This will have no effect as the user already has the custom role " + user.getCustomRole().getName() + "."; + } else { + message += "This will grant them the following permissions:"; + } + messageTextFlowContainer.getChildren().clear(); + messageTextFlowContainer.getChildren().add(new Text(message)); + hasRenderedWarning = true; + } + + private void renderPermissionsVBox() { + if (selectedRole == null) return; + + permissionsVBox.getChildren().clear(); + + Label permissionsLabel = switch (selectedRole) { + case ADMIN -> new Label("To Read, Create, Update, Delete all features"); + case MEMBER -> new Label("To Read all features"); + case NONE -> new Label("No permissions granted"); + }; + + permissionsLabel.getStyleClass().add("general-label"); + permissionsVBox.getChildren().add(permissionsLabel); + } + + @FXML + private void onConfirmButtonClicked() { + if (selectedRole != null && Objects.equals(selectedRole, originalSelectedRole)) return; // Skip if already assigned + + Pair data = new Pair<>(user.getId(), selectedRole); + actionListener.onConfirmAction(data); + } + + @FXML + private void onCancelButtonClicked() { + actionListener.onCancelAction(); + } + + private void styleSelectButton(Button selectButton, boolean isSelected) { + if (isSelected) { + styleSelectedButton(selectButton); + } else { + styleUnselectedButton(selectButton); + } + } + + private void styleUnselectedButton(Button selectButton) { + selectButton.setMaxWidth(Double.MAX_VALUE); + selectButton.getStyleClass().clear(); + selectButton.getStyleClass().add("assign-role-element"); + } + + private void styleSelectedButton(Button selectButton) { + selectButton.setMaxWidth(Double.MAX_VALUE); + selectButton.getStyleClass().clear(); + selectButton.getStyleClass().add("assign-role-selected-element"); + } +} diff --git a/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationAssignRoleController.java b/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationAssignCustomRoleController.java similarity index 63% rename from src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationAssignRoleController.java rename to src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationAssignCustomRoleController.java index 207fd633..f974414a 100644 --- a/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationAssignRoleController.java +++ b/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationAssignCustomRoleController.java @@ -2,14 +2,13 @@ import org.chainoptim.desktop.core.organization.model.CustomRole; import org.chainoptim.desktop.core.organization.model.FeaturePermissions; -import org.chainoptim.desktop.core.organization.model.Permissions; import org.chainoptim.desktop.core.user.model.User; -import org.chainoptim.desktop.core.user.service.UserService; import org.chainoptim.desktop.shared.confirmdialog.controller.GenericConfirmDialogActionListener; import org.chainoptim.desktop.shared.util.DataReceiver; import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.VBox; @@ -19,12 +18,14 @@ import lombok.Setter; import java.util.List; +import java.util.Objects; -public class OrganizationAssignRoleController implements DataReceiver>> { +public class OrganizationAssignCustomRoleController implements DataReceiver, CustomRole>>> { // State private User user; private List customRoles; + private CustomRole originalSelectedRole; private CustomRole selectedRole; private boolean hasRenderedWarning; @@ -47,10 +48,11 @@ public class OrganizationAssignRoleController implements DataReceiver> data) { + public void setData(Pair, CustomRole>> data) { user = data.getKey(); - customRoles = data.getValue(); - System.out.println("Receiving data: " + user + " " + customRoles); + customRoles = data.getValue().getKey(); + originalSelectedRole = data.getValue().getValue(); + selectedRole = originalSelectedRole; initializeUI(); } @@ -60,26 +62,39 @@ private void initializeUI() { titleLabel.setText("Assign role to " + user.getUsername()); + // Add None Option + Button selectNoneButton = new Button("None"); + styleSelectButton(selectNoneButton, selectedRole == null); + selectNoneButton.setOnAction(this::selectRole); + rolesVBox.getChildren().add(selectNoneButton); + + // Add role buttons for (CustomRole customRole : customRoles) { - Button roleName = new Button(customRole.getName()); - roleName.setMaxWidth(Double.MAX_VALUE); - roleName.getStyleClass().add("assign-role-element"); - roleName.setOnAction(this::selectRole); - rolesVBox.getChildren().add(roleName); + Button selectRoleButton = new Button(customRole.getName()); + boolean isSelected = selectedRole != null && customRole.getId().equals(selectedRole.getId()); + styleSelectButton(selectRoleButton, isSelected); + selectRoleButton.setOnAction(this::selectRole); + + + rolesVBox.getChildren().add(selectRoleButton); } } private void selectRole(ActionEvent event) { Button source = (Button) event.getSource(); - selectedRole = customRoles.get(rolesVBox.getChildren().indexOf(source)); - source.getStyleClass().clear(); - source.getStyleClass().add("assign-role-selected-element"); + + // Select role, accounting for None button + int roleIndex = rolesVBox.getChildren().indexOf(source); + selectedRole = roleIndex > 0 ? customRoles.get(roleIndex - 1) : null; + styleSelectButton(source, true); // Deselect other roles for (int i = 0; i < rolesVBox.getChildren().size(); i++) { - if (i != rolesVBox.getChildren().indexOf(source)) { - rolesVBox.getChildren().get(i).getStyleClass().clear(); - rolesVBox.getChildren().get(i).getStyleClass().add("assign-role-element"); + if (i != roleIndex) { + Node otherButton = rolesVBox.getChildren().get(i); + if (otherButton instanceof Button button) { + styleSelectButton(button, false); + } } } @@ -137,9 +152,10 @@ private void renderFeaturePermissions(FeaturePermissions featurePermissions, Str @FXML private void onConfirmButtonClicked() { - if (selectedRole == null) return; + Integer selectedRoleId = selectedRole == null ? null : selectedRole.getId(); // Allow null = no custom role + if (selectedRole != null && originalSelectedRole != null && Objects.equals(selectedRole.getId(), originalSelectedRole.getId())) return; // Skip if already assigned - Pair data = new Pair<>(user.getId(), selectedRole.getId()); + Pair data = new Pair<>(user.getId(), selectedRoleId); actionListener.onConfirmAction(data); } @@ -147,4 +163,24 @@ private void onConfirmButtonClicked() { private void onCancelButtonClicked() { actionListener.onCancelAction(); } + + private void styleSelectButton(Button selectButton, boolean isSelected) { + if (isSelected) { + styleSelectedButton(selectButton); + } else { + styleUnselectedButton(selectButton); + } + } + + private void styleUnselectedButton(Button selectButton) { + selectButton.setMaxWidth(Double.MAX_VALUE); + selectButton.getStyleClass().clear(); + selectButton.getStyleClass().add("assign-role-element"); + } + + private void styleSelectedButton(Button selectButton) { + selectButton.setMaxWidth(Double.MAX_VALUE); + selectButton.getStyleClass().clear(); + selectButton.getStyleClass().add("assign-role-selected-element"); + } } diff --git a/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationOverviewController.java b/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationOverviewController.java index 49c1dc45..a0b5581f 100644 --- a/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationOverviewController.java +++ b/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationOverviewController.java @@ -1,10 +1,15 @@ package org.chainoptim.desktop.core.organization.controller; import org.chainoptim.desktop.core.abstraction.ControllerFactory; +import org.chainoptim.desktop.core.main.service.CurrentSelectionService; +import org.chainoptim.desktop.core.main.service.NavigationService; +import org.chainoptim.desktop.core.organization.model.CustomRole; import org.chainoptim.desktop.core.organization.model.OrganizationViewData; import org.chainoptim.desktop.core.user.model.User; import org.chainoptim.desktop.core.user.service.UserService; +import org.chainoptim.desktop.shared.confirmdialog.controller.GenericConfirmDialogController; import org.chainoptim.desktop.shared.confirmdialog.controller.RunnableConfirmDialogActionListener; +import org.chainoptim.desktop.shared.confirmdialog.model.ConfirmDialogInput; import org.chainoptim.desktop.shared.fallback.FallbackManager; import org.chainoptim.desktop.shared.util.DataReceiver; import org.chainoptim.desktop.shared.util.resourceloader.FXMLLoaderService; @@ -26,25 +31,32 @@ import javafx.util.Pair; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.format.DateTimeFormatter; import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.function.Consumer; +import java.awt.Desktop; public class OrganizationOverviewController implements DataReceiver { // Services private final UserService userService; + private final NavigationService navigationService; + private final CurrentSelectionService currentSelectionService; private final FXMLLoaderService fxmlLoaderService; private final ControllerFactory controllerFactory; // Listeners - private RunnableConfirmDialogActionListener> confirmDialogUpdateListener; // Pair (userId, customRoleId) + private RunnableConfirmDialogActionListener> customRoleUpdateDialogListener; // Pair (userId, customRoleId) + private RunnableConfirmDialogActionListener> basicRoleUpdateDialogListener; // Custom Role name + private RunnableConfirmDialogActionListener> removeMemberDialogListener; // Pair (userId, organizationId) // State private OrganizationViewData organizationViewData; private boolean isInitialRender = true; - private int editedUserRowId = -1; // Marker for no edit + private int editedUserRowIndex = -1; // Marker for no edit private boolean isDeleteMode = false; private final FallbackManager fallbackManager; @@ -57,11 +69,16 @@ public class OrganizationOverviewController implements DataReceiver toggleDeleteMode()); styleAddNewRoleButton(addNewMemberButton); - addNewMemberButton.setOnAction(event -> {}); + addNewMemberButton.setOnAction(event -> handleAddNewMembers()); } private void setupListeners() { - Consumer> onConfirmUpdate = this::assignRole; - Runnable onCancelUpdate = this::cancelAssignRole; + // Assign Custom Role + Consumer> onConfirmUpdate = this::assignCustomRole; + Runnable onCancelUpdate = this::cancelAssignCustomRole; + + customRoleUpdateDialogListener = new RunnableConfirmDialogActionListener<>(onConfirmUpdate, onCancelUpdate); + + // Assign Basic Role + Consumer> onConfirmBasicRoleUpdate = this::assignBasicRole; + Runnable onCancelBasicRoleUpdate = this::cancelAssignBasicRole; + + basicRoleUpdateDialogListener = new RunnableConfirmDialogActionListener<>(onConfirmBasicRoleUpdate, onCancelBasicRoleUpdate); + + // Remove organization member + Consumer> onConfirmDelete = this::removeOrganizationMember; + Runnable onCancelDelete = this::cancelRemoveMember; + + removeMemberDialogListener = new RunnableConfirmDialogActionListener<>(onConfirmDelete, onCancelDelete); - confirmDialogUpdateListener = new RunnableConfirmDialogActionListener<>(onConfirmUpdate, onCancelUpdate); } + // Render UI private void renderMembersGrid() { membersGridPane.getChildren().clear(); @@ -139,123 +175,340 @@ private void renderMembersGrid() { for (User user : organizationViewData.getOrganization().getUsers()) { for (int i = 0; i < headers.length; i++) { Node node = getDisplayedPropertyByHeader(user, row, i); - if (i > 0) applyStandardMargin(node); + if (i > 0 && i != 2) applyStandardMargin(node); // Skip for username and email membersGridPane.add(node, i, row); GridPane.setHalignment(node, HPos.CENTER); } + + addRemoveMemberButton(user, row); + row++; } } private Node getDisplayedPropertyByHeader(User user, int rowIndex, int headerIndex) { String header = headers[headerIndex]; - if (header.equals("Custom Role")) { - if (user.getCustomRole() != null) { - return new Label(user.getCustomRole().getName()); - } - Button button = new Button("Assign"); - button.getStyleClass().add("pseudo-link"); - button.setOnAction(event -> { - System.out.println("Assigning role to user: " + user.getUsername() + " rowId: " + rowIndex + " headerIndex: " + headerIndex); - editedUserRowId = rowIndex; - loadAssignRoleDialog(user, event); - }); - return button; - } return switch (header) { - case "Username" -> new Label(user.getUsername()); - case "Role" -> new Label(user.getRole().toString()); - case "Joined At" -> new Label(user.getCreatedAt().toString()); - case "Email" -> new Label(user.getEmail()); + case "Username" -> { + Label label = new Label(user.getUsername()); + label.getStyleClass().add("parent-row"); + yield label; + } + case "Joined At" -> { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd MMMM yyyy"); + String formattedDate = user.getCreatedAt().format(formatter); + yield new Label(formattedDate); + } + case "Email" -> addEmailHyperlink(user); + case "Role" -> addBasicRoleButton(user, rowIndex); + case "Custom Role" -> addCustomRoleButton(user, rowIndex); default -> null; }; } - private void loadAssignRoleDialog(User user, ActionEvent event) { + private Hyperlink addEmailHyperlink(User user) { + Hyperlink emailLink = new Hyperlink(user.getEmail()); + emailLink.getStyleClass().clear(); + emailLink.getStyleClass().add("pseudo-link"); + emailLink.setOnAction(event -> { + // Open default email client + try { + Desktop.getDesktop().mail(new URI("mailto:" + user.getEmail())); + } catch (IOException | URISyntaxException e) { + e.printStackTrace(); + } + }); + return emailLink; + } + + private Button addBasicRoleButton(User user, Integer rowIndex) { + String label = user.getRole() != null ? user.getRole().toString() : "Assign"; + // Decapitalize except first letter + label = label.substring(0, 1).toUpperCase() + label.substring(1).toLowerCase(); + Button button = new Button(label); + button.setOnAction(event -> { + editedUserRowIndex = rowIndex; + loadAssignBasicRoleDialog(user, event); + }); + button.getStyleClass().add("pseudo-link"); + return button; + } + + private Button addCustomRoleButton(User user, Integer rowIndex) { + String label = user.getCustomRole() != null ? user.getCustomRole().getName() : "Assign"; + Button button = new Button(label); + button.getStyleClass().add("pseudo-link"); + button.setOnAction(event -> { + editedUserRowIndex = rowIndex; + loadAssignCustomRoleDialog(user, event, user.getCustomRole()); + }); + return button; + } + + private void addRemoveMemberButton(User user, int rowIndex) { + Button removeButton = new Button(); + styleDeleteRoleButton(removeButton); + removeButton.setOnAction(event -> loadConfirmDeleteDialog(user)); + applyStandardMargin(removeButton); + removeButton.setVisible(false); // Hide it initially + membersGridPane.add(removeButton, DELETE_COLUMN_INDEX, rowIndex); + GridPane.setHalignment(removeButton, HPos.CENTER); + } + + // Event handlers + // - Assign Basic Role + private void loadAssignBasicRoleDialog(User user, ActionEvent event) { FXMLLoader loader = fxmlLoaderService.setUpLoader( - "/org/chainoptim/desktop/core/organization/OrganizationAssignRoleView.fxml", + "/org/chainoptim/desktop/core/organization/OrganizationAssignBasicRoleView.fxml", controllerFactory::createController ); try { Node content = loader.load(); - OrganizationAssignRoleController controller = loader.getController(); - controller.setData(new Pair<>(user, organizationViewData.getCustomRoles())); - controller.setActionListener(confirmDialogUpdateListener); + OrganizationAssignBasicRoleController controller = loader.getController(); + controller.setData(user); + controller.setActionListener(basicRoleUpdateDialogListener); - assignRolePopup = new Popup(); - assignRolePopup.getContent().add(content); - assignRolePopup.setAutoHide(true); + assingBasicRolePopup = new Popup(); + assingBasicRolePopup.getContent().add(content); + assingBasicRolePopup.setAutoHide(true); Button sourceButton = (Button) event.getSource(); // Safe cast, source is a Button - double x = sourceButton.localToScreen(sourceButton.getBoundsInLocal()).getMinX() - 120; // Center the popup + double x = sourceButton.localToScreen(sourceButton.getBoundsInLocal()).getMaxX(); // Open to the left double y = sourceButton.localToScreen(sourceButton.getBoundsInLocal()).getMaxY(); - assignRolePopup.show(sourceButton, x, y); + assingBasicRolePopup.show(sourceButton, x, y); } catch (IOException ex) { ex.printStackTrace(); } + } + private void assignBasicRole(Pair userIdRole) { + fallbackManager.reset(); + fallbackManager.setLoading(true); + + userService.assignBasicRoleToUser(userIdRole.getKey(), userIdRole.getValue()) + .thenApply(this::handleBasicRoleResponse) + .exceptionally(this::handleBasicRoleException); + } + + private Optional handleBasicRoleResponse(Optional userOptional) { + Platform.runLater(() -> { + if (userOptional.isEmpty()) { + fallbackManager.setErrorMessage("Failed to assign basic role."); + return; + } + fallbackManager.setLoading(false); + + // Update Users list with updated user + User updatedUser = userOptional.get(); + organizationViewData.getOrganization().getUsers().stream() + .filter(user -> user.getId().equals(updatedUser.getId())) + .findFirst().ifPresent(user -> user.setRole(updatedUser.getRole())); + + // Update Custom Role button + Node assignNode = membersGridPane.getChildren().stream() + .filter(node -> GridPane.getRowIndex(node) == editedUserRowIndex && GridPane.getColumnIndex(node) == ASSIGN_BASIC_ROLE_COLUMN_INDEX) + .findFirst().orElse(null); + if (assignNode == null) return; + + membersGridPane.getChildren().remove(assignNode); + Button button = addBasicRoleButton(userOptional.get(), editedUserRowIndex); + applyStandardMargin(button); + membersGridPane.add(button, ASSIGN_BASIC_ROLE_COLUMN_INDEX, editedUserRowIndex); + GridPane.setHalignment(button, HPos.CENTER); + + editedUserRowIndex = -1; + }); + return userOptional; } - private void assignRole(Pair userIdCustomRoleId) { + private Optional handleBasicRoleException(Throwable throwable) { + fallbackManager.setErrorMessage("Failed to assign basic role."); + return Optional.empty(); + } + + private void cancelAssignBasicRole() { + assingBasicRolePopup.hide(); + } + + // - Assign Custom Role + private void loadAssignCustomRoleDialog(User user, ActionEvent event, CustomRole selectedRole) { + FXMLLoader loader = fxmlLoaderService.setUpLoader( + "/org/chainoptim/desktop/core/organization/OrganizationAssignCustomRoleView.fxml", + controllerFactory::createController + ); + try { + Node content = loader.load(); + OrganizationAssignCustomRoleController controller = loader.getController(); + controller.setData(new Pair<>(user, new Pair<>(organizationViewData.getCustomRoles(), selectedRole))); + controller.setActionListener(customRoleUpdateDialogListener); + + assignCustomRolePopup = new Popup(); + assignCustomRolePopup.getContent().add(content); + assignCustomRolePopup.setAutoHide(true); + + Button sourceButton = (Button) event.getSource(); // Safe cast, source is a Button + double x = sourceButton.localToScreen(sourceButton.getBoundsInLocal()).getMaxX(); // Open to the left + double y = sourceButton.localToScreen(sourceButton.getBoundsInLocal()).getMaxY(); + assignCustomRolePopup.show(sourceButton, x, y); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + private void assignCustomRole(Pair userIdCustomRoleId) { fallbackManager.reset(); fallbackManager.setLoading(true); userService.assignCustomRoleToUser(userIdCustomRoleId.getKey(), userIdCustomRoleId.getValue()) - .thenApply(this::handleRoleResponse) - .exceptionally(this::handleRoleException); + .thenApply(this::handleCustomRoleResponse) + .exceptionally(this::handleCustomRoleException); } - private Optional handleRoleResponse(Optional userOptional) { + private Optional handleCustomRoleResponse(Optional userOptional) { Platform.runLater(() -> { if (userOptional.isEmpty()) { - fallbackManager.setErrorMessage("Failed to assign role"); + fallbackManager.setErrorMessage("Failed to assign custom role."); return; } fallbackManager.setLoading(false); - // Turn Assign button into Custom Role label + // Update users list with updated user + User updatedUser = userOptional.get(); + organizationViewData.getOrganization().getUsers().stream() + .filter(user -> user.getId().equals(updatedUser.getId())) + .findFirst().ifPresent(user -> user.setCustomRole(updatedUser.getCustomRole())); + + // Update Custom Role button Node assignNode = membersGridPane.getChildren().stream() - .filter(node -> GridPane.getRowIndex(node) == editedUserRowId && GridPane.getColumnIndex(node) == ASSIGN_ROLE_COLUMN_INDEX) + .filter(node -> GridPane.getRowIndex(node) == editedUserRowIndex && GridPane.getColumnIndex(node) == ASSIGN_ROLE_COLUMN_INDEX) .findFirst().orElse(null); if (assignNode == null) return; membersGridPane.getChildren().remove(assignNode); - Label label = new Label(userOptional.get().getCustomRole().getName()); - applyStandardMargin(label); - membersGridPane.add(label, ASSIGN_ROLE_COLUMN_INDEX, editedUserRowId); - GridPane.setHalignment(label, HPos.CENTER); + Button button = addCustomRoleButton(userOptional.get(), editedUserRowIndex); + applyStandardMargin(button); + membersGridPane.add(button, ASSIGN_ROLE_COLUMN_INDEX, editedUserRowIndex); + GridPane.setHalignment(button, HPos.CENTER); - editedUserRowId = -1; + editedUserRowIndex = -1; }); return userOptional; } - private Optional handleRoleException(Throwable throwable) { - fallbackManager.setErrorMessage("Failed to assign role"); + private Optional handleCustomRoleException(Throwable throwable) { + fallbackManager.setErrorMessage("Failed to assign custom role."); return Optional.empty(); } - private void cancelAssignRole() { - assignRolePopup.hide(); + private void cancelAssignCustomRole() { + assignCustomRolePopup.hide(); } + // - Delete private void toggleDeleteMode() { isDeleteMode = !isDeleteMode; + for (int row = 1; row < membersGridPane.getRowCount(); row++) { + Node removeButton = getNodeByRowColumnIndex(row, DELETE_COLUMN_INDEX); + if (removeButton instanceof Button button) { + button.setVisible(isDeleteMode); + } + } + } + + private void loadConfirmDeleteDialog(User user) { + // Load confirm dialog + FXMLLoader loader = fxmlLoaderService.setUpLoader("/org/chainoptim/desktop/shared/confirmdialog/GenericConfirmDialogView.fxml", controllerFactory::createController); + + ConfirmDialogInput confirmDialogInput = new ConfirmDialogInput("Confirm Member Removal", "Are you sure you want to remove " + user.getUsername() + "?", null); + + try { + Node view = loader.load(); + GenericConfirmDialogController> controller = loader.getController(); + controller.setData(new Pair<>(user.getId(), organizationViewData.getOrganization().getId()), confirmDialogInput); + controller.setActionListener(removeMemberDialogListener); + removeConfirmDialogPane.getChildren().add(view); + toggleRemoveDialog(true); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + private void removeOrganizationMember(Pair userIdOrganizationId) { + fallbackManager.reset(); + fallbackManager.setLoading(true); + + userService.removeUserFromOrganization(userIdOrganizationId.getKey(), userIdOrganizationId.getValue()) + .thenApply(this::handleRemoveMemberResponse) + .exceptionally(this::handleRemoveMemberException); + } + + private Optional handleRemoveMemberResponse(Optional userOptional) { + Platform.runLater(() -> { + if (userOptional.isEmpty()) { + fallbackManager.setErrorMessage("Failed to remove member."); + return; + } + User user = userOptional.get(); + + toggleRemoveDialog(false); + fallbackManager.setLoading(false); + toggleDeleteMode(); + + // Remove user from organization + organizationViewData.getOrganization().getUsers().removeIf(u -> u.getId().equals(user.getId())); + + // Re-render members grid - necessary here, at least from that user down + renderMembersGrid(); + }); + return userOptional; + } + + private Optional handleRemoveMemberException(Throwable throwable) { + fallbackManager.setErrorMessage("Failed to remove member."); + return Optional.empty(); } + private void cancelRemoveMember() { + toggleRemoveDialog(false); + } + + private void toggleRemoveDialog(boolean isActive) { + removeConfirmDialogPane.setVisible(isActive); + removeConfirmDialogPane.setManaged(isActive); + } + + // Add New Members + private void handleAddNewMembers() { + Integer organizationId = organizationViewData.getOrganization().getId(); + currentSelectionService.setSelectedId(organizationId); + navigationService.switchView("Add-New-Members?id=" + organizationId, true); + } + + // Utils + private Node getNodeByRowColumnIndex(final int row, final int column) { + Node result = null; + for (Node node : membersGridPane.getChildren()) { + if(GridPane.getRowIndex(node) == row && GridPane.getColumnIndex(node) == column) { + result = node; + break; + } + } + return result; + } + // Styling // TODO: Take this into separate class and reuse private void styleAddNewRoleButton(Button button) { button.getStyleClass().add("standard-write-button"); button.setGraphic(createImageView(plusImage)); - button.setText("Add New Member"); + button.setText("Add New Members"); applyStandardMargin(button); } private void styleDeleteRoleButton(Button button) { button.getStyleClass().add("standard-delete-button"); button.setGraphic(createImageView(trashImage)); - button.setTooltip(new Tooltip("Delete Member")); + button.setTooltip(new Tooltip("Remove Member")); GridPane.setMargin(button, new Insets(12)); } diff --git a/src/main/java/org/chainoptim/desktop/core/organization/service/OrganizationService.java b/src/main/java/org/chainoptim/desktop/core/organization/service/OrganizationService.java index 612f4015..40408cd9 100644 --- a/src/main/java/org/chainoptim/desktop/core/organization/service/OrganizationService.java +++ b/src/main/java/org/chainoptim/desktop/core/organization/service/OrganizationService.java @@ -1,6 +1,7 @@ package org.chainoptim.desktop.core.organization.service; import org.chainoptim.desktop.core.organization.model.Organization; +import org.chainoptim.desktop.core.user.model.User; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -8,4 +9,5 @@ public interface OrganizationService { CompletableFuture> getOrganizationById(Integer organizationId, boolean includeUsers); + } diff --git a/src/main/java/org/chainoptim/desktop/core/organization/service/OrganizationServiceImpl.java b/src/main/java/org/chainoptim/desktop/core/organization/service/OrganizationServiceImpl.java index 383f18cf..9a2c49d2 100644 --- a/src/main/java/org/chainoptim/desktop/core/organization/service/OrganizationServiceImpl.java +++ b/src/main/java/org/chainoptim/desktop/core/organization/service/OrganizationServiceImpl.java @@ -1,6 +1,7 @@ package org.chainoptim.desktop.core.organization.service; import org.chainoptim.desktop.core.organization.model.Organization; +import org.chainoptim.desktop.core.user.model.User; import org.chainoptim.desktop.core.user.util.TokenManager; import org.chainoptim.desktop.shared.util.JsonUtil; diff --git a/src/main/java/org/chainoptim/desktop/core/user/controller/PublicUsersSearchAndSelectionController.java b/src/main/java/org/chainoptim/desktop/core/user/controller/PublicUsersSearchAndSelectionController.java new file mode 100644 index 00000000..b3e01fc2 --- /dev/null +++ b/src/main/java/org/chainoptim/desktop/core/user/controller/PublicUsersSearchAndSelectionController.java @@ -0,0 +1,167 @@ +package org.chainoptim.desktop.core.user.controller; + +import org.chainoptim.desktop.core.user.dto.UserSearchResultDTO; +import org.chainoptim.desktop.core.user.service.UserService; +import org.chainoptim.desktop.shared.search.model.PaginatedResults; +import com.google.inject.Inject; +import javafx.application.Platform; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.effect.ColorAdjust; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import lombok.Getter; + +import java.net.URL; +import java.util.*; + +public class PublicUsersSearchAndSelectionController implements Initializable { + + private final UserService userService; + + // State + private final SimpleIntegerProperty currentPage = new SimpleIntegerProperty(1); + private int totalCount = 0; + @Getter + private List selectedUsers = new ArrayList<>(); + + // Constants + private static final int PAGE_SIZE = 1; + + @FXML + private VBox userResultsVBox; + @FXML + private TextField searchInput; + @FXML + private Button searchButton; + @FXML + private VBox selectedUsersVBox; + + // Icons + private Image removeIcon; + + @Inject + public PublicUsersSearchAndSelectionController(UserService userService) { + this.userService = userService; + } + + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + setupListeners(); + initializeUI(); + searchForUsers(); + } + + private void setupListeners() { + currentPage.addListener((observable, oldValue, newValue) -> searchForUsers()); + searchInput.setOnAction(event -> { + currentPage.set(1); + searchForUsers(); + }); + } + + private void initializeUI() { + // Search Icon + Image image = new Image(Objects.requireNonNull(getClass().getResourceAsStream("/img/search.png"))); + ImageView searchIconView = new ImageView(image); + ColorAdjust colorAdjust = new ColorAdjust(); + colorAdjust.setBrightness(1); + searchIconView.setEffect(colorAdjust); + searchButton.setGraphic(searchIconView); + searchButton.getStyleClass().add("search-button"); + searchButton.setOnAction(event -> { + currentPage.set(1); + searchForUsers(); + }); + + // Remove icon + removeIcon = new Image(Objects.requireNonNull(getClass().getResourceAsStream("/img/xmark-solid.png"))); + } + + private void searchForUsers() { + this.userService.searchPublicUsers(searchInput.getText(), currentPage.get(), PAGE_SIZE) + .thenApply(this::handleSearchResponse) + .exceptionally(this::handleSearchException); + } + + private Optional> handleSearchResponse(Optional> optionalPaginatedResults) { + Platform.runLater(() -> { + if (optionalPaginatedResults.isEmpty()) { + return; + } + PaginatedResults paginatedResults = optionalPaginatedResults.get(); + totalCount = (int) paginatedResults.getTotalCount(); + + // Render Users List + Next Page Button + userResultsVBox.getChildren().clear(); + + if (paginatedResults.getResults().isEmpty()) { + Label noResultsLabel = new Label("No results found."); + noResultsLabel.getStyleClass().add("general-label"); + userResultsVBox.getChildren().add(noResultsLabel); + return; + } + + for (UserSearchResultDTO user : paginatedResults.getResults()) { + Button userButton = new Button(user.getUsername()); + userButton.getStyleClass().add("general-label"); + userButton.setOnAction(event -> selectUser(user)); + userResultsVBox.getChildren().add(userButton); + } + + Button nextPageButton = new Button("Load More"); + nextPageButton.getStyleClass().add("pseudo-link"); + nextPageButton.setStyle("-fx-font-weight: bold; -fx-padding: 10px 0px;"); + nextPageButton.setOnAction(event -> currentPage.set(currentPage.get() + 1)); + userResultsVBox.getChildren().add(nextPageButton); + if (isAtTheEndOfResults()) { + nextPageButton.setDisable(true); + nextPageButton.setVisible(false); + } + }); + return optionalPaginatedResults; + } + + private Optional> handleSearchException(Throwable throwable) { + System.out.println("Error searching for users: " + throwable.getMessage()); + return Optional.empty(); + } + + private void selectUser(UserSearchResultDTO user) { + if (selectedUsers.stream().map(UserSearchResultDTO::getId).toList().contains(user.getId())) { + return; + } + + HBox selectedUserHBox = new HBox(); + Label selectedUserLabel = new Label(user.getUsername()); + selectedUserLabel.getStyleClass().add("general-label"); + + Button removeButton = new Button(); + ImageView removeIconView = new ImageView(removeIcon); + removeIconView.setFitWidth(12); + removeIconView.setFitHeight(12); + removeButton.setGraphic(removeIconView); + removeButton.getStyleClass().add("cancel-edit-button"); + removeButton.setOnAction(event -> { + selectedUsersVBox.getChildren().remove(selectedUserHBox); + selectedUsers.remove(user); + }); + + selectedUserHBox.getChildren().addAll(selectedUserLabel, removeButton); + selectedUserHBox.setAlignment(Pos.CENTER_LEFT); + selectedUserHBox.setSpacing(8); + selectedUsers.add(user); + selectedUsersVBox.getChildren().add(selectedUserHBox); + } + + private boolean isAtTheEndOfResults() { + return currentPage.get() >= Math.ceil((double) totalCount / PAGE_SIZE); + } +} diff --git a/src/main/java/org/chainoptim/desktop/core/user/dto/AssignBasicRoleDTO.java b/src/main/java/org/chainoptim/desktop/core/user/dto/AssignBasicRoleDTO.java new file mode 100644 index 00000000..14b790c3 --- /dev/null +++ b/src/main/java/org/chainoptim/desktop/core/user/dto/AssignBasicRoleDTO.java @@ -0,0 +1,13 @@ +package org.chainoptim.desktop.core.user.dto; + +import org.chainoptim.desktop.core.user.model.User; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AssignBasicRoleDTO { + private User.Role role; +} diff --git a/src/main/java/org/chainoptim/desktop/core/user/dto/AssignCustomRoleDTO.java b/src/main/java/org/chainoptim/desktop/core/user/dto/AssignCustomRoleDTO.java new file mode 100644 index 00000000..64521029 --- /dev/null +++ b/src/main/java/org/chainoptim/desktop/core/user/dto/AssignCustomRoleDTO.java @@ -0,0 +1,12 @@ +package org.chainoptim.desktop.core.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class AssignCustomRoleDTO { + private Integer roleId; +} diff --git a/src/main/java/org/chainoptim/desktop/core/user/dto/UserSearchResultDTO.java b/src/main/java/org/chainoptim/desktop/core/user/dto/UserSearchResultDTO.java new file mode 100644 index 00000000..b4116406 --- /dev/null +++ b/src/main/java/org/chainoptim/desktop/core/user/dto/UserSearchResultDTO.java @@ -0,0 +1,16 @@ +package org.chainoptim.desktop.core.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class UserSearchResultDTO { + private String id; + private String username; + private String email; +} diff --git a/src/main/java/org/chainoptim/desktop/core/user/service/UserService.java b/src/main/java/org/chainoptim/desktop/core/user/service/UserService.java index af4b158e..7d99a282 100644 --- a/src/main/java/org/chainoptim/desktop/core/user/service/UserService.java +++ b/src/main/java/org/chainoptim/desktop/core/user/service/UserService.java @@ -1,13 +1,20 @@ package org.chainoptim.desktop.core.user.service; +import org.chainoptim.desktop.core.user.dto.UserSearchResultDTO; import org.chainoptim.desktop.core.user.model.User; +import org.chainoptim.desktop.shared.search.model.PaginatedResults; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; public interface UserService { + // Read CompletableFuture> getUserByUsername(String username); CompletableFuture>> getUsersByCustomRoleId(Integer customRoleId); + CompletableFuture>> searchPublicUsers(String searchQuery, int page, int itemsPerPage); + // Write + CompletableFuture> assignBasicRoleToUser(String userId, User.Role role); CompletableFuture> assignCustomRoleToUser(String userId, Integer roleId); + CompletableFuture> removeUserFromOrganization(String userId, Integer organizationId); } diff --git a/src/main/java/org/chainoptim/desktop/core/user/service/UserServiceImpl.java b/src/main/java/org/chainoptim/desktop/core/user/service/UserServiceImpl.java index 5cfa3901..76d02c1e 100644 --- a/src/main/java/org/chainoptim/desktop/core/user/service/UserServiceImpl.java +++ b/src/main/java/org/chainoptim/desktop/core/user/service/UserServiceImpl.java @@ -1,12 +1,15 @@ package org.chainoptim.desktop.core.user.service; +import org.chainoptim.desktop.core.user.dto.AssignBasicRoleDTO; +import org.chainoptim.desktop.core.user.dto.AssignCustomRoleDTO; +import org.chainoptim.desktop.core.user.dto.UserSearchResultDTO; import org.chainoptim.desktop.core.user.model.User; +import org.chainoptim.desktop.shared.search.model.PaginatedResults; import org.chainoptim.desktop.shared.util.JsonUtil; import org.chainoptim.desktop.core.user.util.TokenManager; import com.fasterxml.jackson.core.type.TypeReference; -import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.URI; import java.net.URLEncoder; @@ -24,10 +27,11 @@ public class UserServiceImpl implements UserService { private static final String HEADER_KEY = "Authorization"; private static final String HEADER_VALUE_PREFIX = "Bearer "; + private static final String BASE_PATH = "http://localhost:8080/api/v1/users"; public CompletableFuture> getUserByUsername(String username) { String encodedUsername = URLEncoder.encode(username, StandardCharsets.UTF_8); - String routeAddress = "http://localhost:8080/api/v1/users/username/" + encodedUsername; + String routeAddress = BASE_PATH + "/username/" + encodedUsername; String jwtToken = TokenManager.getToken(); if (jwtToken == null) return new CompletableFuture<>(); @@ -54,7 +58,7 @@ public CompletableFuture> getUserByUsername(String username) { } public CompletableFuture>> getUsersByCustomRoleId(Integer customRoleId) { - String routeAddress = "http://localhost:8080/api/v1/users/search/custom-role/" + customRoleId; + String routeAddress = BASE_PATH + "/search/custom-role/" + customRoleId; String jwtToken = TokenManager.getToken(); if (jwtToken == null) return new CompletableFuture<>(); @@ -82,17 +86,91 @@ public CompletableFuture>> getUsersByCustomRoleId(Integer cu }); } + public CompletableFuture>> searchPublicUsers(String searchQuery, int page, int itemsPerPage) { + String encodedSearchQuery = URLEncoder.encode(searchQuery, StandardCharsets.UTF_8); + String routeAddress = BASE_PATH + "/search/public?searchQuery=" + encodedSearchQuery + "&page=" + page + "&itemsPerPage=" + itemsPerPage; + + String jwtToken = TokenManager.getToken(); + if (jwtToken == null) return new CompletableFuture<>(); + String headerValue = HEADER_VALUE_PREFIX + jwtToken; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(routeAddress)) + .headers(HEADER_KEY, headerValue) + .GET() + .build(); + + return client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(response -> { + if (response.statusCode() != HttpURLConnection.HTTP_OK) return Optional.>empty(); + try { + PaginatedResults userSearchResultDTO = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference>() {}); + return Optional.of(userSearchResultDTO); + } catch (Exception e) { + e.printStackTrace(); + } + return Optional.>empty(); + }); + } + + // Write + public CompletableFuture> assignBasicRoleToUser(String userId, User.Role role) { + String routeAddress = BASE_PATH + "/" + userId + "/assign-basic-role"; + + String jwtToken = TokenManager.getToken(); + if (jwtToken == null) return new CompletableFuture<>(); + String headerValue = HEADER_VALUE_PREFIX + jwtToken; + + // Serialize DTO + String requestBody = null; + try { + requestBody = JsonUtil.getObjectMapper().writeValueAsString(new AssignBasicRoleDTO(role)); + } catch (Exception e) { + e.printStackTrace(); + } + assert requestBody != null; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(routeAddress)) + .PUT(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8)) + .headers(HEADER_KEY, headerValue) + .headers("Content-Type", "application/json") + .build(); + + return client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(response -> { + if (response.statusCode() != HttpURLConnection.HTTP_OK) return Optional.empty(); + try { + User user = JsonUtil.getObjectMapper().readValue(response.body(), User.class); + return Optional.of(user); + } catch (Exception e) { + e.printStackTrace(); + } + return Optional.empty(); + }); + } + public CompletableFuture> assignCustomRoleToUser(String userId, Integer roleId) { - String routeAddress = "http://localhost:8080/api/v1/users/" + userId + "/assign-role/" + roleId; + String routeAddress = BASE_PATH + "/" + userId + "/assign-custom-role"; String jwtToken = TokenManager.getToken(); if (jwtToken == null) return new CompletableFuture<>(); String headerValue = HEADER_VALUE_PREFIX + jwtToken; + // Serialize DTO + String requestBody = null; + try { + requestBody = JsonUtil.getObjectMapper().writeValueAsString(new AssignCustomRoleDTO(roleId)); + } catch (Exception e) { + e.printStackTrace(); + } + assert requestBody != null; + HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(routeAddress)) + .PUT(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8)) .headers(HEADER_KEY, headerValue) - .PUT(HttpRequest.BodyPublishers.noBody()) + .headers("Content-Type", "application/json") .build(); return client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) @@ -107,5 +185,31 @@ public CompletableFuture> assignCustomRoleToUser(String userId, I return Optional.empty(); }); } + + public CompletableFuture> removeUserFromOrganization(String userId, Integer organizationId) { + String routeAddress = BASE_PATH + "/" + userId + "/remove-from-organization/" + organizationId.toString(); + + String jwtToken = TokenManager.getToken(); + if (jwtToken == null) return new CompletableFuture<>(); + String headerValue = HEADER_VALUE_PREFIX + jwtToken; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(routeAddress)) + .PUT(HttpRequest.BodyPublishers.noBody()) + .headers(HEADER_KEY, headerValue) + .build(); + + return client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(response -> { + if (response.statusCode() != HttpURLConnection.HTTP_OK) return Optional.empty(); + try { + User user = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference() {}); + return Optional.of(user); + } catch (Exception e) { + e.printStackTrace(); + return Optional.empty(); + } + }); + } } diff --git a/src/main/resources/css/common-elements.css b/src/main/resources/css/common-elements.css index 90ed8a20..e247373d 100644 --- a/src/main/resources/css/common-elements.css +++ b/src/main/resources/css/common-elements.css @@ -5,7 +5,7 @@ .pseudo-link { -fx-background-color: transparent; - -fx-text-fill: #007bff; + -fx-text-fill: #007aee; -fx-border-width: 0; -fx-padding: 0; -fx-focus-color: transparent; diff --git a/src/main/resources/org/chainoptim/desktop/core/organization/AddNewMembersView.fxml b/src/main/resources/org/chainoptim/desktop/core/organization/AddNewMembersView.fxml new file mode 100644 index 00000000..259322b8 --- /dev/null +++ b/src/main/resources/org/chainoptim/desktop/core/organization/AddNewMembersView.fxml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/src/main/resources/org/chainoptim/desktop/core/organization/OrganizationAssignRoleView.fxml b/src/main/resources/org/chainoptim/desktop/core/organization/OrganizationAssignBasicRoleView.fxml similarity index 95% rename from src/main/resources/org/chainoptim/desktop/core/organization/OrganizationAssignRoleView.fxml rename to src/main/resources/org/chainoptim/desktop/core/organization/OrganizationAssignBasicRoleView.fxml index 0d7642b8..fd929e4a 100644 --- a/src/main/resources/org/chainoptim/desktop/core/organization/OrganizationAssignRoleView.fxml +++ b/src/main/resources/org/chainoptim/desktop/core/organization/OrganizationAssignBasicRoleView.fxml @@ -7,12 +7,12 @@