From 2bf8952bb3d2de005023b4c35ea28100386fad59 Mon Sep 17 00:00:00 2001
From: TudorOrban <130213626+TudorOrban@users.noreply.github.com>
Date: Thu, 25 Apr 2024 11:29:30 +0300
Subject: [PATCH 01/10] Finish refactoring services

---
 .../core/main/controller/AppController.java   |  19 +-
 .../NotificationPersistenceService.java       |   5 +-
 .../NotificationPersistenceServiceImpl.java   |  86 +++----
 .../controller/OrganizationController.java    |  27 ++-
 .../OrganizationCustomRolesController.java    |  21 +-
 .../OrganizationOverviewController.java       |  41 ++--
 .../service/CustomRoleService.java            |   9 +-
 .../service/CustomRoleServiceImpl.java        | 152 ++++---------
 .../service/OrganizationService.java          |   3 +-
 .../service/OrganizationServiceImpl.java      |  56 ++---
 .../controller/OverviewController.java        |  25 +--
 .../service/SupplyChainSnapshotService.java   |   3 +-
 .../SupplyChainSnapshotServiceImpl.java       |  55 ++---
 .../controller/SettingsController.java        |  25 ++-
 .../settings/service/UserSettingsService.java |   5 +-
 .../service/UserSettingsServiceImpl.java      |  91 +++-----
 ...blicUsersSearchAndSelectionController.java |  11 +-
 .../UsersListByCustomRoleController.java      |  13 +-
 .../core/user/service/UserService.java        |  13 +-
 .../core/user/service/UserServiceImpl.java    | 210 ++++--------------
 .../SelectOrCreateLocationController.java     |  13 +-
 .../location/service/LocationService.java     |   9 +-
 .../location/service/LocationServiceImpl.java | 156 ++++---------
 23 files changed, 343 insertions(+), 705 deletions(-)

diff --git a/src/main/java/org/chainoptim/desktop/core/main/controller/AppController.java b/src/main/java/org/chainoptim/desktop/core/main/controller/AppController.java
index 7da2cd46..01a641c5 100644
--- a/src/main/java/org/chainoptim/desktop/core/main/controller/AppController.java
+++ b/src/main/java/org/chainoptim/desktop/core/main/controller/AppController.java
@@ -18,6 +18,7 @@
 import org.chainoptim.desktop.core.user.service.AuthenticationService;
 import org.chainoptim.desktop.core.user.service.UserService;
 import org.chainoptim.desktop.core.user.util.TokenManager;
+import org.chainoptim.desktop.shared.httphandling.Result;
 
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -77,36 +78,34 @@ public void initialize() {
     private void fetchAndSetUser(String username) {
         userService.getUserByUsername(username)
                 .thenApply(this::handleUserResponse)
-                .exceptionally(ex -> Optional.empty());
+                .exceptionally(ex -> new Result<>());
     }
 
-    private Optional<User> handleUserResponse(Optional<User> userOptional) {
+    private Result<User> handleUserResponse(Result<User> result) {
         Platform.runLater(() -> {
-            System.out.println("User loaded: " + userOptional);
-            if (userOptional.isEmpty()) {
+            if (result.getError() != null) {
                 return;
             }
 
-            User user = userOptional.get();
+            User user = result.getData();
 
             user.getOrganization().setSubscriptionPlanTier(PRO);
-            System.out.println("Subscription plan: " + user.getOrganization().getSubscriptionPlanTier().toString());
 
             // Set user to TenantContext for reuse throughout the app
             TenantContext.setCurrentUser(user);
 
             // Fetch user settings and set them to TenantSettingsContext
             userSettingsService.getUserSettings(user.getId())
-                    .thenAccept(userSettingsOptional -> {
-                        if (userSettingsOptional.isEmpty()) return;
-                        TenantSettingsContext.setCurrentUserSettings(userSettingsOptional.get());
+                    .thenAccept(result1 -> {
+                        if (result1.getError() != null) return;
+                        TenantSettingsContext.setCurrentUserSettings(result1.getData());
                     });
 
             // Start WebSocket connection
             startWebSocket(user);
         });
 
-        return userOptional;
+        return result;
     }
 
     private void startWebSocket(User user) {
diff --git a/src/main/java/org/chainoptim/desktop/core/notification/service/NotificationPersistenceService.java b/src/main/java/org/chainoptim/desktop/core/notification/service/NotificationPersistenceService.java
index 255ace67..46020193 100644
--- a/src/main/java/org/chainoptim/desktop/core/notification/service/NotificationPersistenceService.java
+++ b/src/main/java/org/chainoptim/desktop/core/notification/service/NotificationPersistenceService.java
@@ -1,6 +1,7 @@
 package org.chainoptim.desktop.core.notification.service;
 
 import org.chainoptim.desktop.core.notification.model.NotificationUser;
+import org.chainoptim.desktop.shared.httphandling.Result;
 import org.chainoptim.desktop.shared.search.model.PaginatedResults;
 import org.chainoptim.desktop.shared.search.model.SearchParams;
 
@@ -10,7 +11,7 @@
 
 public interface NotificationPersistenceService {
 
-    CompletableFuture<Optional<List<NotificationUser>>> getNotificationsByUserId(String userId);
+    CompletableFuture<Result<List<NotificationUser>>> getNotificationsByUserId(String userId);
 
-    CompletableFuture<Optional<PaginatedResults<NotificationUser>>> getNotificationsByUserIdAdvanced(String userId, SearchParams searchParams);
+    CompletableFuture<Result<PaginatedResults<NotificationUser>>> getNotificationsByUserIdAdvanced(String userId, SearchParams searchParams);
 }
diff --git a/src/main/java/org/chainoptim/desktop/core/notification/service/NotificationPersistenceServiceImpl.java b/src/main/java/org/chainoptim/desktop/core/notification/service/NotificationPersistenceServiceImpl.java
index 1af477ea..1dd44b9e 100644
--- a/src/main/java/org/chainoptim/desktop/core/notification/service/NotificationPersistenceServiceImpl.java
+++ b/src/main/java/org/chainoptim/desktop/core/notification/service/NotificationPersistenceServiceImpl.java
@@ -1,100 +1,66 @@
 package org.chainoptim.desktop.core.notification.service;
 
-import org.chainoptim.desktop.core.notification.model.Notification;
 import org.chainoptim.desktop.core.notification.model.NotificationUser;
-import org.chainoptim.desktop.core.user.util.TokenManager;
-import org.chainoptim.desktop.features.factory.dto.FactoriesSearchDTO;
-import org.chainoptim.desktop.features.supplier.model.Supplier;
+import org.chainoptim.desktop.core.user.service.TokenManager;
 import org.chainoptim.desktop.shared.caching.CacheKeyBuilder;
 import org.chainoptim.desktop.shared.caching.CachingService;
+import org.chainoptim.desktop.shared.httphandling.RequestBuilder;
+import org.chainoptim.desktop.shared.httphandling.RequestHandler;
+import org.chainoptim.desktop.shared.httphandling.Result;
 import org.chainoptim.desktop.shared.search.model.PaginatedResults;
 import org.chainoptim.desktop.shared.search.model.SearchParams;
-import org.chainoptim.desktop.shared.util.JsonUtil;
+
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.google.inject.Inject;
 
 import java.net.HttpURLConnection;
-import java.net.URI;
-import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
 import java.util.List;
-import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 
 public class NotificationPersistenceServiceImpl implements NotificationPersistenceService {
 
     private final CachingService<PaginatedResults<NotificationUser>> cachingService;
-    private final HttpClient client = HttpClient.newHttpClient();
+    private final RequestHandler requestHandler;
+    private final RequestBuilder requestBuilder;
+    private final TokenManager tokenManager;
 
-    private static final String HEADER_KEY = "Authorization";
-    private static final String HEADER_VALUE_PREFIX = "Bearer ";
     private static final int STALE_TIME = 300;
 
     @Inject
-    public NotificationPersistenceServiceImpl(CachingService<PaginatedResults<NotificationUser>> cachingService) {
+    public NotificationPersistenceServiceImpl(
+            CachingService<PaginatedResults<NotificationUser>> cachingService,
+            RequestHandler requestHandler,
+            RequestBuilder requestBuilder,
+            TokenManager tokenManager) {
         this.cachingService = cachingService;
+        this.requestHandler = requestHandler;
+        this.requestBuilder = requestBuilder;
+        this.tokenManager = tokenManager;
     }
 
-    public CompletableFuture<Optional<List<NotificationUser>>> getNotificationsByUserId(String userId) {
+    public CompletableFuture<Result<List<NotificationUser>>> getNotificationsByUserId(String userId) {
         String routeAddress = "http://localhost:8080/api/v1/notifications/user/" + userId;
 
-        String jwtToken = TokenManager.getToken();
-        if (jwtToken == null) return new CompletableFuture<>();
-        String headerValue = HEADER_VALUE_PREFIX + jwtToken;
-
-        HttpRequest request = HttpRequest.newBuilder()
-                .uri(URI.create(routeAddress))
-                .GET()
-                .headers(HEADER_KEY, headerValue)
-                .build();
+        HttpRequest request = requestBuilder.buildReadRequest(routeAddress, tokenManager.getToken());
 
-        return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
-                .thenApply(response -> {
-                    if (response.statusCode() != HttpURLConnection.HTTP_OK) return Optional.<List<NotificationUser>>empty();
-                    try {
-                        List<NotificationUser> notifications = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference<List<NotificationUser>>() {});
-                        return Optional.of(notifications);
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                        return Optional.<List<NotificationUser>>empty();
-                    }
-                });
+        return requestHandler.sendRequest(request, new TypeReference<List<NotificationUser>>() {});
     }
 
-    public CompletableFuture<Optional<PaginatedResults<NotificationUser>>> getNotificationsByUserIdAdvanced(String userId, SearchParams searchParams) {
+    public CompletableFuture<Result<PaginatedResults<NotificationUser>>> getNotificationsByUserIdAdvanced(String userId, SearchParams searchParams) {
         String rootAddress = "http://localhost:8080/api/v1/";
         String cacheKey = CacheKeyBuilder.buildAdvancedSearchKey("notifications", "user", userId, searchParams);
         String routeAddress = rootAddress + cacheKey;
 
-        String jwtToken = TokenManager.getToken();
-        if (jwtToken == null) return new CompletableFuture<>();
-        String headerValue = HEADER_VALUE_PREFIX + jwtToken;
-
-        HttpRequest request = HttpRequest.newBuilder()
-                .uri(URI.create(routeAddress))
-                .GET()
-                .headers(HEADER_KEY, headerValue)
-                .build();
+        HttpRequest request = requestBuilder.buildReadRequest(routeAddress, tokenManager.getToken());
 
         if (cachingService.isCached(cacheKey) && !cachingService.isStale(cacheKey)) {
-            return CompletableFuture.completedFuture(Optional.of(cachingService.get(cacheKey)));
+            return CompletableFuture.completedFuture(new Result<>(cachingService.get(cacheKey), null, HttpURLConnection.HTTP_OK));
         }
 
-        return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
-                .thenApply(response -> {
-                    if (response.statusCode() != HttpURLConnection.HTTP_OK) return Optional.<PaginatedResults<NotificationUser>>empty();
-                    try {
-                        PaginatedResults<NotificationUser> notifications = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference<PaginatedResults<NotificationUser>>() {});
-
-                        cachingService.remove(cacheKey); // Ensure there isn't a stale cache entry
-                        cachingService.add(cacheKey, notifications, STALE_TIME);
-
-                        return Optional.of(notifications);
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                        return Optional.<PaginatedResults<NotificationUser>>empty();
-                    }
-                });
+        return requestHandler.sendRequest(request, new TypeReference<PaginatedResults<NotificationUser>>() {}, notifications -> {
+            cachingService.remove(cacheKey); // Ensure there isn't a stale cache entry
+            cachingService.add(cacheKey, notifications, STALE_TIME);
+        });
     }
 }
diff --git a/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationController.java b/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationController.java
index 1b728639..3b86f7d3 100644
--- a/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationController.java
+++ b/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationController.java
@@ -9,6 +9,7 @@
 import org.chainoptim.desktop.core.organization.service.OrganizationService;
 import org.chainoptim.desktop.core.user.model.User;
 import org.chainoptim.desktop.shared.fallback.FallbackManager;
+import org.chainoptim.desktop.shared.httphandling.Result;
 import org.chainoptim.desktop.shared.util.DataReceiver;
 import org.chainoptim.desktop.shared.util.resourceloader.FXMLLoaderService;
 
@@ -160,18 +161,16 @@ private void loadOrganization() {
                 .exceptionally(this::handleOrganizationException);
     }
 
-    private Optional<Organization> handleOrganizationResponse(Optional<Organization> organizationOptional) {
+    private Result<Organization> handleOrganizationResponse(Result<Organization> result) {
         Platform.runLater(() -> {
-            if (organizationOptional.isEmpty()) {
+            if (result.getError() != null) {
                 fallbackManager.setErrorMessage("Failed to load organization.");
                 return;
             }
-            organizationViewData.setOrganization(organizationOptional.get());
+            organizationViewData.setOrganization(result.getData());
 
             organizationViewData.getOrganization().setSubscriptionPlanTier(PRO);
 
-            System.out.println("Organization: " + organizationViewData.getOrganization());
-
             initializeUI();
 
             organizationOverviewController.setData(organizationViewData); // Feed organization data to overview tab
@@ -181,7 +180,7 @@ private Optional<Organization> handleOrganizationResponse(Optional<Organization>
             fallbackManager.setLoading(false);
         });
 
-        return organizationOptional;
+        return result;
     }
 
     private void loadCustomRoles() {
@@ -191,24 +190,24 @@ private void loadCustomRoles() {
                 .thenRun(() -> Platform.runLater(() -> fallbackManager.setLoading(false)));
     }
 
-    private Optional<List<CustomRole>> handleCustomRolesResponse(Optional<List<CustomRole>> customRolesOptional) {
+    private Result<List<CustomRole>> handleCustomRolesResponse(Result<List<CustomRole>> result) {
         Platform.runLater(() -> {
-            if (customRolesOptional.isEmpty()) {
+            if (result.getError() != null) {
                 fallbackManager.setErrorMessage("Failed to load custom roles.");
                 return;
             }
-            organizationViewData.setCustomRoles(customRolesOptional.get());
+            organizationViewData.setCustomRoles(result.getData());
             if (organizationOverviewController != null) {
                 organizationOverviewController.setData(organizationViewData);
             }
             System.out.println("Custom Roles: " + organizationViewData.getCustomRoles());
         });
-        return customRolesOptional;
+        return result;
     }
 
-    private Optional<List<CustomRole>> handleCustomRolesException(Throwable ex) {
+    private Result<List<CustomRole>> handleCustomRolesException(Throwable ex) {
         Platform.runLater(() -> fallbackManager.setErrorMessage("Failed to load custom roles."));
-        return Optional.empty();
+        return new Result<>();
     }
 
     private void initializeUI() {
@@ -217,9 +216,9 @@ private void initializeUI() {
         planLabel.setText("Subscription Plan: " + organizationViewData.getOrganization().getSubscriptionPlanTier().toString());
     }
 
-    private Optional<Organization> handleOrganizationException(Throwable ex) {
+    private Result<Organization> handleOrganizationException(Throwable ex) {
         Platform.runLater(() -> fallbackManager.setErrorMessage("Failed to load organization."));
-        return Optional.empty();
+        return new Result<>();
     }
 
     @FXML
diff --git a/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationCustomRolesController.java b/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationCustomRolesController.java
index e3bc5dde..470da321 100644
--- a/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationCustomRolesController.java
+++ b/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationCustomRolesController.java
@@ -9,6 +9,7 @@
 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.httphandling.Result;
 import org.chainoptim.desktop.shared.util.DataReceiver;
 import org.chainoptim.desktop.shared.util.resourceloader.FXMLLoaderService;
 
@@ -505,13 +506,13 @@ private UpdateCustomRoleDTO gatherUpdatedCustomRole(CustomRole customRole, int s
         return new UpdateCustomRoleDTO(customRole.getId(), roleName, permissions);
     }
 
-    private void handleSuccessfulUpdate(Optional<CustomRole> updatedRoleOptional, int rowIndex) {
+    private void handleSuccessfulUpdate(Result<CustomRole> result, int rowIndex) {
         Platform.runLater(() -> {
-            if (updatedRoleOptional.isEmpty()) {
+            if (result.getError() != null) {
                 fallbackManager.setErrorMessage("Failed to update custom role.");
                 return;
             }
-            CustomRole updatedRole = updatedRoleOptional.get();
+            CustomRole updatedRole = result.getData();
             closeConfirmUpdateDialog();
 
             // Update customRoles
@@ -570,9 +571,9 @@ private void handleDeleteRole(CustomRole customRole) {
                 .thenAccept(deletedRoleIdOptional -> handleSuccessfulDelete(deletedRoleIdOptional, currentToBeDeletedRowIndex));
     }
 
-    private void handleSuccessfulDelete(Optional<Integer> deletedRoleIdOptional, int rowIndex) {
+    private void handleSuccessfulDelete(Result<Integer> result, int rowIndex) {
         Platform.runLater(() -> {
-            if (deletedRoleIdOptional.isEmpty()) {
+            if (result.getError() != null) {
                 fallbackManager.setErrorMessage("Failed to delete custom role.");
                 return;
             }
@@ -603,17 +604,17 @@ private void handleAddNewRole() {
                 .thenApply(this::handleNewRoleResponse)
                 .exceptionally(ex -> {
                     Platform.runLater(() -> fallbackManager.setErrorMessage("Failed to create new custom role."));
-                    return Optional.empty();
+                    return new Result<>();
                 });
     }
 
-    private Optional<CustomRole> handleNewRoleResponse(Optional<CustomRole> newRoleOptional) {
+    private Result<CustomRole> handleNewRoleResponse(Result<CustomRole> result) {
         Platform.runLater(() -> {
-            if (newRoleOptional.isEmpty()) {
+            if (result.getError() != null) {
                 fallbackManager.setErrorMessage("Failed to create new custom role.");
                 return;
             }
-            CustomRole newRole = newRoleOptional.get();
+            CustomRole newRole = result.getData();
             customRoles.add(newRole);
             tabTitle.setText("Custom Roles (" + customRoles.size() + ")");
 
@@ -625,7 +626,7 @@ private Optional<CustomRole> handleNewRoleResponse(Optional<CustomRole> newRoleO
                 addFeaturePermissionRow(feature, newRole, newRowIndex++);
             }
         });
-        return newRoleOptional;
+        return result;
     }
 
     // Utils
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 a0b5581f..de219293 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
@@ -11,6 +11,7 @@
 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.httphandling.Result;
 import org.chainoptim.desktop.shared.util.DataReceiver;
 import org.chainoptim.desktop.shared.util.resourceloader.FXMLLoaderService;
 
@@ -290,16 +291,16 @@ private void assignBasicRole(Pair<String, User.Role> userIdRole) {
                 .exceptionally(this::handleBasicRoleException);
     }
 
-    private Optional<User> handleBasicRoleResponse(Optional<User> userOptional) {
+    private Result<User> handleBasicRoleResponse(Result<User> result) {
         Platform.runLater(() -> {
-            if (userOptional.isEmpty()) {
+            if (result.getError() != null) {
                 fallbackManager.setErrorMessage("Failed to assign basic role.");
                 return;
             }
             fallbackManager.setLoading(false);
 
             // Update Users list with updated user
-            User updatedUser = userOptional.get();
+            User updatedUser = result.getData();
             organizationViewData.getOrganization().getUsers().stream()
                     .filter(user -> user.getId().equals(updatedUser.getId()))
                     .findFirst().ifPresent(user -> user.setRole(updatedUser.getRole()));
@@ -311,19 +312,19 @@ private Optional<User> handleBasicRoleResponse(Optional<User> userOptional) {
             if (assignNode == null) return;
 
             membersGridPane.getChildren().remove(assignNode);
-            Button button = addBasicRoleButton(userOptional.get(), editedUserRowIndex);
+            Button button = addBasicRoleButton(updatedUser, editedUserRowIndex);
             applyStandardMargin(button);
             membersGridPane.add(button, ASSIGN_BASIC_ROLE_COLUMN_INDEX, editedUserRowIndex);
             GridPane.setHalignment(button, HPos.CENTER);
 
             editedUserRowIndex = -1;
         });
-        return userOptional;
+        return result;
     }
 
-    private Optional<User> handleBasicRoleException(Throwable throwable) {
+    private Result<User> handleBasicRoleException(Throwable throwable) {
         fallbackManager.setErrorMessage("Failed to assign basic role.");
-        return Optional.empty();
+        return new Result<>();
     }
 
     private void cancelAssignBasicRole() {
@@ -364,16 +365,16 @@ private void assignCustomRole(Pair<String, Integer> userIdCustomRoleId) {
                 .exceptionally(this::handleCustomRoleException);
     }
 
-    private Optional<User> handleCustomRoleResponse(Optional<User> userOptional) {
+    private Result<User> handleCustomRoleResponse(Result<User> result) {
         Platform.runLater(() -> {
-            if (userOptional.isEmpty()) {
+            if (result.getError() != null) {
                 fallbackManager.setErrorMessage("Failed to assign custom role.");
                 return;
             }
             fallbackManager.setLoading(false);
 
             // Update users list with updated user
-            User updatedUser = userOptional.get();
+            User updatedUser = result.getData();
             organizationViewData.getOrganization().getUsers().stream()
                     .filter(user -> user.getId().equals(updatedUser.getId()))
                     .findFirst().ifPresent(user -> user.setCustomRole(updatedUser.getCustomRole()));
@@ -385,19 +386,19 @@ private Optional<User> handleCustomRoleResponse(Optional<User> userOptional) {
             if (assignNode == null) return;
 
             membersGridPane.getChildren().remove(assignNode);
-            Button button = addCustomRoleButton(userOptional.get(), editedUserRowIndex);
+            Button button = addCustomRoleButton(updatedUser, editedUserRowIndex);
             applyStandardMargin(button);
             membersGridPane.add(button, ASSIGN_ROLE_COLUMN_INDEX, editedUserRowIndex);
             GridPane.setHalignment(button, HPos.CENTER);
 
             editedUserRowIndex = -1;
         });
-        return userOptional;
+        return result;
     }
 
-    private Optional<User> handleCustomRoleException(Throwable throwable) {
+    private Result<User> handleCustomRoleException(Throwable throwable) {
         fallbackManager.setErrorMessage("Failed to assign custom role.");
-        return Optional.empty();
+        return new Result<>();
     }
 
     private void cancelAssignCustomRole() {
@@ -442,13 +443,13 @@ private void removeOrganizationMember(Pair<String, Integer> userIdOrganizationId
                 .exceptionally(this::handleRemoveMemberException);
     }
 
-    private Optional<User> handleRemoveMemberResponse(Optional<User> userOptional) {
+    private Result<User> handleRemoveMemberResponse(Result<User> result) {
         Platform.runLater(() -> {
-            if (userOptional.isEmpty()) {
+            if (result.getError() != null) {
                 fallbackManager.setErrorMessage("Failed to remove member.");
                 return;
             }
-            User user = userOptional.get();
+            User user = result.getData();
 
             toggleRemoveDialog(false);
             fallbackManager.setLoading(false);
@@ -460,12 +461,12 @@ private Optional<User> handleRemoveMemberResponse(Optional<User> userOptional) {
             // Re-render members grid - necessary here, at least from that user down
             renderMembersGrid();
         });
-        return userOptional;
+        return result;
     }
 
-    private Optional<User> handleRemoveMemberException(Throwable throwable) {
+    private Result<User> handleRemoveMemberException(Throwable throwable) {
         fallbackManager.setErrorMessage("Failed to remove member.");
-        return Optional.empty();
+        return new Result<>();
     }
 
     private void cancelRemoveMember() {
diff --git a/src/main/java/org/chainoptim/desktop/core/organization/service/CustomRoleService.java b/src/main/java/org/chainoptim/desktop/core/organization/service/CustomRoleService.java
index 5dfda068..9b28c319 100644
--- a/src/main/java/org/chainoptim/desktop/core/organization/service/CustomRoleService.java
+++ b/src/main/java/org/chainoptim/desktop/core/organization/service/CustomRoleService.java
@@ -3,6 +3,7 @@
 import org.chainoptim.desktop.core.organization.dto.CreateCustomRoleDTO;
 import org.chainoptim.desktop.core.organization.dto.UpdateCustomRoleDTO;
 import org.chainoptim.desktop.core.organization.model.CustomRole;
+import org.chainoptim.desktop.shared.httphandling.Result;
 
 import java.util.List;
 import java.util.Optional;
@@ -10,8 +11,8 @@
 
 public interface CustomRoleService {
 
-    CompletableFuture<Optional<List<CustomRole>>> getCustomRolesByOrganizationId(Integer organizationId);
-    CompletableFuture<Optional<CustomRole>> createCustomRole(CreateCustomRoleDTO roleDTO);
-    CompletableFuture<Optional<CustomRole>> updateCustomRole(UpdateCustomRoleDTO roleDTO);
-    CompletableFuture<Optional<Integer>> deleteCustomRole(Integer roleId);
+    CompletableFuture<Result<List<CustomRole>>> getCustomRolesByOrganizationId(Integer organizationId);
+    CompletableFuture<Result<CustomRole>> createCustomRole(CreateCustomRoleDTO roleDTO);
+    CompletableFuture<Result<CustomRole>> updateCustomRole(UpdateCustomRoleDTO roleDTO);
+    CompletableFuture<Result<Integer>> deleteCustomRole(Integer roleId);
 }
diff --git a/src/main/java/org/chainoptim/desktop/core/organization/service/CustomRoleServiceImpl.java b/src/main/java/org/chainoptim/desktop/core/organization/service/CustomRoleServiceImpl.java
index bc726e50..f3d17574 100644
--- a/src/main/java/org/chainoptim/desktop/core/organization/service/CustomRoleServiceImpl.java
+++ b/src/main/java/org/chainoptim/desktop/core/organization/service/CustomRoleServiceImpl.java
@@ -3,142 +3,66 @@
 import org.chainoptim.desktop.core.organization.dto.CreateCustomRoleDTO;
 import org.chainoptim.desktop.core.organization.dto.UpdateCustomRoleDTO;
 import org.chainoptim.desktop.core.organization.model.CustomRole;
-import org.chainoptim.desktop.core.user.util.TokenManager;
-import org.chainoptim.desktop.shared.util.JsonUtil;
+import org.chainoptim.desktop.core.user.service.TokenManager;
+import org.chainoptim.desktop.shared.httphandling.HttpMethod;
+import org.chainoptim.desktop.shared.httphandling.RequestBuilder;
+import org.chainoptim.desktop.shared.httphandling.RequestHandler;
+import org.chainoptim.desktop.shared.httphandling.Result;
 
 import com.fasterxml.jackson.core.type.TypeReference;
-import java.net.HttpURLConnection;
-import java.net.URI;
-import java.net.http.HttpClient;
+import com.google.inject.Inject;
+
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.nio.charset.StandardCharsets;
 import java.util.List;
-import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 
 public class CustomRoleServiceImpl implements CustomRoleService {
 
-    private final HttpClient client = HttpClient.newHttpClient();
-
-    private static final String HEADER_KEY = "Authorization";
-    private static final String HEADER_VALUE_PREFIX = "Bearer ";
+    private final RequestHandler requestHandler;
+    private final RequestBuilder requestBuilder;
+    private final TokenManager tokenManager;
+
+    @Inject
+    public CustomRoleServiceImpl(RequestHandler requestHandler,
+                                 RequestBuilder requestBuilder,
+                                 TokenManager tokenManager) {
+        this.requestHandler = requestHandler;
+        this.requestBuilder = requestBuilder;
+        this.tokenManager = tokenManager;
+    }
 
-    public CompletableFuture<Optional<List<CustomRole>>> getCustomRolesByOrganizationId(Integer organizationId) {
+    public CompletableFuture<Result<List<CustomRole>>> getCustomRolesByOrganizationId(Integer organizationId) {
         String routeAddress = "http://localhost:8080/api/v1/custom-roles/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))
-                .GET()
-                .headers(HEADER_KEY, headerValue)
-                .build();
-
-        return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
-                .thenApply(response -> {
-                    if (response.statusCode() != HttpURLConnection.HTTP_OK) return Optional.<List<CustomRole>>empty();
-                    try {
-                        List<CustomRole> customRoles = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference<List<CustomRole>>() {});
-                        return Optional.of(customRoles);
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                        return Optional.<List<CustomRole>>empty();
-                    }
-                });
+        HttpRequest request = requestBuilder.buildReadRequest(routeAddress, tokenManager.getToken());
+
+        return requestHandler.sendRequest(request, new TypeReference<List<CustomRole>>() {});
     }
 
-    public CompletableFuture<Optional<CustomRole>> createCustomRole(CreateCustomRoleDTO roleDTO) {
+    public CompletableFuture<Result<CustomRole>> createCustomRole(CreateCustomRoleDTO roleDTO) {
         String routeAddress = "http://localhost:8080/api/v1/custom-roles/create";
 
-        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(roleDTO);
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-        assert requestBody != null;
-
-        HttpRequest request = HttpRequest.newBuilder()
-                .uri(URI.create(routeAddress))
-                .POST(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 {
-                        CustomRole customRole = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference<CustomRole>() {});
-                        return Optional.of(customRole);
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                        return Optional.<CustomRole>empty();
-                    }
-                });
+        HttpRequest request = requestBuilder.buildWriteRequest(
+                HttpMethod.POST, routeAddress, tokenManager.getToken(), roleDTO);
+
+        return requestHandler.sendRequest(request, new TypeReference<CustomRole>() {});
     }
 
-    public CompletableFuture<Optional<CustomRole>> updateCustomRole(UpdateCustomRoleDTO roleDTO) {
+    public CompletableFuture<Result<CustomRole>> updateCustomRole(UpdateCustomRoleDTO roleDTO) {
         String routeAddress = "http://localhost:8080/api/v1/custom-roles/update";
 
-        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(roleDTO);
-        } 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 {
-                        CustomRole customRole = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference<CustomRole>() {});
-                        return Optional.of(customRole);
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                        return Optional.<CustomRole>empty();
-                    }
-                });
+        HttpRequest request = requestBuilder.buildWriteRequest(
+                HttpMethod.PUT, routeAddress, tokenManager.getToken(), roleDTO);
+
+        return requestHandler.sendRequest(request, new TypeReference<CustomRole>() {});
     }
 
-    public CompletableFuture<Optional<Integer>> deleteCustomRole(Integer roleId) {
+    public CompletableFuture<Result<Integer>> deleteCustomRole(Integer roleId) {
         String routeAddress = "http://localhost:8080/api/v1/custom-roles/delete/" + roleId;
 
-        String jwtToken = TokenManager.getToken();
-        if (jwtToken == null) return new CompletableFuture<>();
-        String headerValue = HEADER_VALUE_PREFIX + jwtToken;
-
-        HttpRequest request = HttpRequest.newBuilder()
-                .uri(URI.create(routeAddress))
-                .DELETE()
-                .headers(HEADER_KEY, headerValue)
-                .build();
-
-        return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
-                .thenApply(response -> {
-                    if (response.statusCode() != HttpURLConnection.HTTP_OK) return Optional.empty();
-                    return Optional.of(roleId);
-                });
+        HttpRequest request = requestBuilder.buildWriteRequest(
+                HttpMethod.DELETE, routeAddress, tokenManager.getToken(), null);
+
+        return requestHandler.sendRequest(request, new TypeReference<Integer>() {});
     }
 }
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 40408cd9..59cb929a 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
@@ -2,12 +2,13 @@
 
 import org.chainoptim.desktop.core.organization.model.Organization;
 import org.chainoptim.desktop.core.user.model.User;
+import org.chainoptim.desktop.shared.httphandling.Result;
 
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 
 public interface OrganizationService {
 
-    CompletableFuture<Optional<Organization>> getOrganizationById(Integer organizationId, boolean includeUsers);
+    CompletableFuture<Result<Organization>> 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 9a2c49d2..4ede8f4b 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,49 +1,37 @@
 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;
+import org.chainoptim.desktop.core.user.service.TokenManager;
+import org.chainoptim.desktop.shared.httphandling.RequestBuilder;
+import org.chainoptim.desktop.shared.httphandling.RequestHandler;
+import org.chainoptim.desktop.shared.httphandling.Result;
 
 import com.fasterxml.jackson.core.type.TypeReference;
-import java.net.HttpURLConnection;
-import java.net.URI;
-import java.net.http.HttpClient;
+import com.google.inject.Inject;
+
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 
 public class OrganizationServiceImpl implements OrganizationService {
 
-    private final HttpClient client = HttpClient.newHttpClient();
-
-    private static final String HEADER_KEY = "Authorization";
-    private static final String HEADER_VALUE_PREFIX = "Bearer ";
+    private final RequestHandler requestHandler;
+    private final RequestBuilder requestBuilder;
+    private final TokenManager tokenManager;
+
+    @Inject
+    public OrganizationServiceImpl(RequestHandler requestHandler,
+                                   RequestBuilder requestBuilder,
+                                   TokenManager tokenManager) {
+        this.requestHandler = requestHandler;
+        this.requestBuilder = requestBuilder;
+        this.tokenManager = tokenManager;
+    }
 
-    public CompletableFuture<Optional<Organization>> getOrganizationById(Integer organizationId, boolean includeUsers) {
+    public CompletableFuture<Result<Organization>> getOrganizationById(Integer organizationId, boolean includeUsers) {
         String routeAddress = "http://localhost:8080/api/v1/organizations/" + organizationId.toString() + "?includeUsers=" + includeUsers;
 
-        String jwtToken = TokenManager.getToken();
-        if (jwtToken == null) return new CompletableFuture<>();
-        String headerValue = HEADER_VALUE_PREFIX + jwtToken;
-
-        HttpRequest request = HttpRequest.newBuilder()
-                .uri(URI.create(routeAddress))
-                .GET()
-                .headers(HEADER_KEY, headerValue)
-                .build();
-
-        return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
-                .thenApply(response -> {
-                    if (response.statusCode() != HttpURLConnection.HTTP_OK) return Optional.<Organization>empty();
-                    try {
-                        Organization organization = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference<Organization>() {});
-                        return Optional.of(organization);
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                        return Optional.<Organization>empty();
-                    }
-                });
+        HttpRequest request = requestBuilder.buildReadRequest(routeAddress, tokenManager.getToken());
+
+        return requestHandler.sendRequest(request, new TypeReference<Organization>() {});
     }
 }
diff --git a/src/main/java/org/chainoptim/desktop/core/overview/controller/OverviewController.java b/src/main/java/org/chainoptim/desktop/core/overview/controller/OverviewController.java
index 8a0cbed3..718d9513 100644
--- a/src/main/java/org/chainoptim/desktop/core/overview/controller/OverviewController.java
+++ b/src/main/java/org/chainoptim/desktop/core/overview/controller/OverviewController.java
@@ -14,6 +14,7 @@
 import org.chainoptim.desktop.shared.common.uielements.badge.BadgeData;
 import org.chainoptim.desktop.shared.common.uielements.badge.FeatureCountBadge;
 import org.chainoptim.desktop.shared.fallback.FallbackManager;
+import org.chainoptim.desktop.shared.httphandling.Result;
 import org.chainoptim.desktop.shared.search.model.PaginatedResults;
 import org.chainoptim.desktop.shared.search.model.SearchParams;
 import org.chainoptim.desktop.shared.util.resourceloader.CommonViewsLoader;
@@ -127,8 +128,6 @@ private void initializeUI() {
         headerImageView.setFitHeight(16);
         headerImageView.setFitWidth(16);
         titleLabel.setGraphic(headerImageView);
-
-//        alertImage = new Image(Objects.requireNonNull(getClass().getResourceAsStream("/img/alert-circle-solid.png")));
     }
 
     private void loadData(User currentUser) {
@@ -144,46 +143,46 @@ private void loadData(User currentUser) {
                 .thenApply(this::handleSupplyChainSnapshotResponse)
                 .exceptionally(ex -> {
                     Platform.runLater(() -> fallbackManager.setErrorMessage("Failed to load supply chain snapshot."));
-                    return Optional.empty();
+                    return new Result<>();
                 });
 
         notificationPersistenceService.getNotificationsByUserIdAdvanced(currentUser.getId(), searchParams)
                 .thenApply(this::handleNotificationsResponse)
                 .exceptionally(ex -> {
                     Platform.runLater(() -> fallbackManager.setErrorMessage("Failed to load notifications."));
-                    return Optional.empty();
+                    return new Result<>();
                 });
     }
 
     // Fetches
-    private Optional<SupplyChainSnapshot> handleSupplyChainSnapshotResponse(Optional<SupplyChainSnapshot> snapshotOptional) {
+    private Result<SupplyChainSnapshot> handleSupplyChainSnapshotResponse(Result<SupplyChainSnapshot> result) {
         Platform.runLater(() -> {
-            if (snapshotOptional.isEmpty()) {
+            if (result.getError() != null) {
                 return;
             }
-            snapshot = snapshotOptional.get().getSnapshot();
+            snapshot = result.getData().getSnapshot();
             snapshotContext.setSnapshot(snapshot);
             fallbackManager.setLoading(false);
 
             renderEntityCountsVBox(snapshot);
         });
 
-        return snapshotOptional;
+        return result;
     }
 
-    private Optional<PaginatedResults<NotificationUser>> handleNotificationsResponse(Optional<PaginatedResults<NotificationUser>> notificationsOptional) {
+    private Result<PaginatedResults<NotificationUser>> handleNotificationsResponse(Result<PaginatedResults<NotificationUser>> result) {
         Platform.runLater(() -> {
-            if (notificationsOptional.isEmpty()) {
+            if (result.getError() != null) {
                 return;
             }
-            PaginatedResults<NotificationUser> notifications = notificationsOptional.get();
+            PaginatedResults<NotificationUser> notifications = result.getData();
             fallbackManager.setLoading(false);
 
             totalCount = notifications.getTotalCount();
             renderNotificationsVBox(notifications.getResults());
         });
 
-        return notificationsOptional;
+        return result;
     }
 
     // UI Rendering
@@ -271,7 +270,7 @@ private void addLoadMoreButton() {
                     .thenApply(this::handleNotificationsResponse)
                     .exceptionally(ex -> {
                         Platform.runLater(() -> fallbackManager.setErrorMessage("Failed to load more notifications."));
-                        return Optional.empty();
+                        return new Result<>();
                     });
         });
         notificationsVBox.getChildren().add(currentLoadMoreButton);
diff --git a/src/main/java/org/chainoptim/desktop/core/overview/service/SupplyChainSnapshotService.java b/src/main/java/org/chainoptim/desktop/core/overview/service/SupplyChainSnapshotService.java
index 8d334c8e..8c9e7e46 100644
--- a/src/main/java/org/chainoptim/desktop/core/overview/service/SupplyChainSnapshotService.java
+++ b/src/main/java/org/chainoptim/desktop/core/overview/service/SupplyChainSnapshotService.java
@@ -1,11 +1,12 @@
 package org.chainoptim.desktop.core.overview.service;
 
 import org.chainoptim.desktop.core.overview.model.SupplyChainSnapshot;
+import org.chainoptim.desktop.shared.httphandling.Result;
 
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 
 public interface SupplyChainSnapshotService {
 
-    CompletableFuture<Optional<SupplyChainSnapshot>> getSupplyChainSnapshot(Integer organizationId);
+    CompletableFuture<Result<SupplyChainSnapshot>> getSupplyChainSnapshot(Integer organizationId);
 }
diff --git a/src/main/java/org/chainoptim/desktop/core/overview/service/SupplyChainSnapshotServiceImpl.java b/src/main/java/org/chainoptim/desktop/core/overview/service/SupplyChainSnapshotServiceImpl.java
index d65724e2..e8ed6626 100644
--- a/src/main/java/org/chainoptim/desktop/core/overview/service/SupplyChainSnapshotServiceImpl.java
+++ b/src/main/java/org/chainoptim/desktop/core/overview/service/SupplyChainSnapshotServiceImpl.java
@@ -1,49 +1,38 @@
 package org.chainoptim.desktop.core.overview.service;
 
 import org.chainoptim.desktop.core.overview.model.SupplyChainSnapshot;
-import org.chainoptim.desktop.core.user.util.TokenManager;
-import org.chainoptim.desktop.shared.util.JsonUtil;
+import org.chainoptim.desktop.core.user.service.TokenManager;
+import org.chainoptim.desktop.shared.httphandling.RequestBuilder;
+import org.chainoptim.desktop.shared.httphandling.RequestHandler;
+import org.chainoptim.desktop.shared.httphandling.Result;
 
 import com.fasterxml.jackson.core.type.TypeReference;
+import com.google.inject.Inject;
 
-import java.net.HttpURLConnection;
-import java.net.URI;
-import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 
 public class SupplyChainSnapshotServiceImpl implements SupplyChainSnapshotService {
 
-    private final HttpClient client = HttpClient.newHttpClient();
-
-    private static final String HEADER_KEY = "Authorization";
-    private static final String HEADER_VALUE_PREFIX = "Bearer ";
+    private final RequestHandler requestHandler;
+    private final RequestBuilder requestBuilder;
+    private final org.chainoptim.desktop.core.user.service.TokenManager tokenManager;
+
+    @Inject
+    public SupplyChainSnapshotServiceImpl(
+            RequestHandler requestHandler,
+            RequestBuilder requestBuilder,
+            TokenManager tokenManager) {
+        this.requestHandler = requestHandler;
+        this.requestBuilder = requestBuilder;
+        this.tokenManager = tokenManager;
+    }
 
-    public CompletableFuture<Optional<SupplyChainSnapshot>> getSupplyChainSnapshot(Integer organizationId) {
+    public CompletableFuture<Result<SupplyChainSnapshot>> getSupplyChainSnapshot(Integer organizationId) {
         String routeAddress = "http://localhost:8080/api/v1/supply-chain-snapshots/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))
-                .GET()
-                .headers(HEADER_KEY, headerValue)
-                .build();
-
-        return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
-                .thenApply(response -> {
-                    if (response.statusCode() != HttpURLConnection.HTTP_OK) return Optional.<SupplyChainSnapshot>empty();
-                    try {
-                        SupplyChainSnapshot snapshot = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference<SupplyChainSnapshot>() {});
-                        return Optional.of(snapshot);
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                        return Optional.<SupplyChainSnapshot>empty();
-                    }
-                });
+        HttpRequest request = requestBuilder.buildReadRequest(routeAddress, tokenManager.getToken());
+
+        return requestHandler.sendRequest(request, new TypeReference<SupplyChainSnapshot>() {});
     }
 }
diff --git a/src/main/java/org/chainoptim/desktop/core/settings/controller/SettingsController.java b/src/main/java/org/chainoptim/desktop/core/settings/controller/SettingsController.java
index 42042d05..98ea8089 100644
--- a/src/main/java/org/chainoptim/desktop/core/settings/controller/SettingsController.java
+++ b/src/main/java/org/chainoptim/desktop/core/settings/controller/SettingsController.java
@@ -8,6 +8,7 @@
 import org.chainoptim.desktop.core.settings.service.UserSettingsService;
 import org.chainoptim.desktop.core.user.model.User;
 import org.chainoptim.desktop.shared.fallback.FallbackManager;
+import org.chainoptim.desktop.shared.httphandling.Result;
 import org.chainoptim.desktop.shared.util.DataReceiver;
 import org.chainoptim.desktop.shared.util.resourceloader.CommonViewsLoader;
 import org.chainoptim.desktop.shared.util.resourceloader.FXMLLoaderService;
@@ -155,25 +156,25 @@ private void loadUserSettings(String userId) {
                 .exceptionally(this::handleUserSettingsException);
     }
 
-    private Optional<UserSettings> handleUserSettingsResponse(Optional<UserSettings> userSettingsOptional) {
+    private Result<UserSettings> handleUserSettingsResponse(Result<UserSettings> result) {
         Platform.runLater(() -> {
-            if (userSettingsOptional.isEmpty()) {
+            if (result.getError() != null) {
                 fallbackManager.setErrorMessage("Failed to load user settings.");
                 return;
             }
 
-            userSettings = userSettingsOptional.get();
+            userSettings = result.getData();
             fallbackManager.setLoading(false);
 
             loadTabContent(generalTab, "/org/chainoptim/desktop/core/settings/GeneralSettingsView.fxml", userSettings);
         });
 
-        return userSettingsOptional;
+        return result;
     }
 
-    private Optional<UserSettings> handleUserSettingsException(Throwable ex) {
+    private Result<UserSettings> handleUserSettingsException(Throwable ex) {
         fallbackManager.setErrorMessage("Failed to load user settings.");
-        return Optional.empty();
+        return new Result<>();
     }
 
     @Override
@@ -197,13 +198,13 @@ public void handleSave() {
                 .exceptionally(this::handleSaveException);
     }
 
-    private Optional<UserSettings> handleSaveResponse(Optional<UserSettings> userSettingsOptional) {
+    private Result<UserSettings> handleSaveResponse(Result<UserSettings> result) {
         Platform.runLater(() -> {
-            if (userSettingsOptional.isEmpty()) {
+            if (result.getError() != null) {
                 fallbackManager.setErrorMessage("Failed to save user settings.");
                 return;
             }
-            userSettings = userSettingsOptional.get();
+            userSettings = result.getData();
             TenantSettingsContext.setCurrentUserSettings(userSettings.deepCopy());
 
             if (generalSettingsController != null) {
@@ -217,12 +218,12 @@ private Optional<UserSettings> handleSaveResponse(Optional<UserSettings> userSet
             handleSettingsChanged(false);
         });
 
-        return userSettingsOptional;
+        return result;
     }
 
-    private Optional<UserSettings> handleSaveException(Throwable ex) {
+    private Result<UserSettings> handleSaveException(Throwable ex) {
         fallbackManager.setErrorMessage("Failed to save user settings.");
-        return Optional.empty();
+        return new Result<>();
     }
 
     @FXML
diff --git a/src/main/java/org/chainoptim/desktop/core/settings/service/UserSettingsService.java b/src/main/java/org/chainoptim/desktop/core/settings/service/UserSettingsService.java
index a8443e93..3f4a7cdc 100644
--- a/src/main/java/org/chainoptim/desktop/core/settings/service/UserSettingsService.java
+++ b/src/main/java/org/chainoptim/desktop/core/settings/service/UserSettingsService.java
@@ -2,13 +2,14 @@
 
 import org.chainoptim.desktop.core.settings.dto.UpdateUserSettingsDTO;
 import org.chainoptim.desktop.core.settings.model.UserSettings;
+import org.chainoptim.desktop.shared.httphandling.Result;
 
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 
 public interface UserSettingsService {
 
-    CompletableFuture<Optional<UserSettings>> getUserSettings(String userId);
+    CompletableFuture<Result<UserSettings>> getUserSettings(String userId);
 
-    CompletableFuture<Optional<UserSettings>> saveUserSettings(UpdateUserSettingsDTO userSettings);
+    CompletableFuture<Result<UserSettings>> saveUserSettings(UpdateUserSettingsDTO userSettings);
 }
diff --git a/src/main/java/org/chainoptim/desktop/core/settings/service/UserSettingsServiceImpl.java b/src/main/java/org/chainoptim/desktop/core/settings/service/UserSettingsServiceImpl.java
index d729525d..ac35c00d 100644
--- a/src/main/java/org/chainoptim/desktop/core/settings/service/UserSettingsServiceImpl.java
+++ b/src/main/java/org/chainoptim/desktop/core/settings/service/UserSettingsServiceImpl.java
@@ -2,84 +2,47 @@
 
 import org.chainoptim.desktop.core.settings.dto.UpdateUserSettingsDTO;
 import org.chainoptim.desktop.core.settings.model.UserSettings;
-import org.chainoptim.desktop.core.user.util.TokenManager;
-import org.chainoptim.desktop.shared.util.JsonUtil;
+import org.chainoptim.desktop.core.user.service.TokenManager;
+import org.chainoptim.desktop.shared.httphandling.HttpMethod;
+import org.chainoptim.desktop.shared.httphandling.RequestBuilder;
+import org.chainoptim.desktop.shared.httphandling.RequestHandler;
+import org.chainoptim.desktop.shared.httphandling.Result;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.google.inject.Inject;
 
-import java.net.HttpURLConnection;
-import java.net.URI;
-import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.nio.charset.StandardCharsets;
-import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 
 public class UserSettingsServiceImpl implements UserSettingsService {
 
-    private final HttpClient client = HttpClient.newHttpClient();
-
-    private static final String HEADER_KEY = "Authorization";
-    private static final String HEADER_VALUE_PREFIX = "Bearer ";
+    private final RequestHandler requestHandler;
+    private final RequestBuilder requestBuilder;
+    private final TokenManager tokenManager;
+
+    @Inject
+    public UserSettingsServiceImpl(RequestHandler requestHandler,
+                                   RequestBuilder requestBuilder,
+                                   TokenManager tokenManager) {
+        this.requestHandler = requestHandler;
+        this.requestBuilder = requestBuilder;
+        this.tokenManager = tokenManager;
+    }
 
-    public CompletableFuture<Optional<UserSettings>> getUserSettings(String userId) {
+    public CompletableFuture<Result<UserSettings>> getUserSettings(String userId) {
         String routeAddress = "http://localhost:8080/api/v1/user-settings/user/" + userId;
 
-        String jwtToken = TokenManager.getToken();
-        if (jwtToken == null) return new CompletableFuture<>();
-        String headerValue = HEADER_VALUE_PREFIX + jwtToken;
+        HttpRequest request = requestBuilder.buildReadRequest(routeAddress, tokenManager.getToken());
 
-        HttpRequest request = HttpRequest.newBuilder()
-                .uri(URI.create(routeAddress))
-                .GET()
-                .headers(HEADER_KEY, headerValue)
-                .build();
-
-        return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
-                .thenApply(response -> {
-                    if (response.statusCode() != HttpURLConnection.HTTP_OK) return Optional.<UserSettings>empty();
-                    try {
-                        UserSettings userSettings = JsonUtil.getObjectMapper().readValue(response.body(), UserSettings.class);
-                        return Optional.of(userSettings);
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                        return Optional.<UserSettings>empty();
-                    }
-                });
+        return requestHandler.sendRequest(request, new TypeReference<UserSettings>() {});
     }
 
-    public CompletableFuture<Optional<UserSettings>> saveUserSettings(UpdateUserSettingsDTO userSettingsDTO) {
+    public CompletableFuture<Result<UserSettings>> saveUserSettings(UpdateUserSettingsDTO userSettingsDTO) {
         String routeAddress = "http://localhost:8080/api/v1/user-settings/update";
 
-        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(userSettingsDTO);
-        } 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();
+        HttpRequest request = requestBuilder.buildWriteRequest(
+                HttpMethod.PUT, routeAddress, tokenManager.getToken(), userSettingsDTO);
 
-        return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
-                .thenApply(response -> {
-                    if (response.statusCode() != HttpURLConnection.HTTP_OK) return Optional.<UserSettings>empty();
-                    try {
-                        UserSettings userSettings = JsonUtil.getObjectMapper().readValue(response.body(), UserSettings.class);
-                        return Optional.of(userSettings);
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                        return Optional.<UserSettings>empty();
-                    }
-                });
+        return requestHandler.sendRequest(request, new TypeReference<UserSettings>() {});
     }
 }
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
index b3e01fc2..c575dea3 100644
--- a/src/main/java/org/chainoptim/desktop/core/user/controller/PublicUsersSearchAndSelectionController.java
+++ b/src/main/java/org/chainoptim/desktop/core/user/controller/PublicUsersSearchAndSelectionController.java
@@ -2,6 +2,7 @@
 
 import org.chainoptim.desktop.core.user.dto.UserSearchResultDTO;
 import org.chainoptim.desktop.core.user.service.UserService;
+import org.chainoptim.desktop.shared.httphandling.Result;
 import org.chainoptim.desktop.shared.search.model.PaginatedResults;
 import com.google.inject.Inject;
 import javafx.application.Platform;
@@ -91,12 +92,12 @@ private void searchForUsers() {
                 .exceptionally(this::handleSearchException);
     }
 
-    private Optional<PaginatedResults<UserSearchResultDTO>> handleSearchResponse(Optional<PaginatedResults<UserSearchResultDTO>> optionalPaginatedResults) {
+    private Result<PaginatedResults<UserSearchResultDTO>> handleSearchResponse(Result<PaginatedResults<UserSearchResultDTO>> optionalPaginatedResults) {
         Platform.runLater(() -> {
-            if (optionalPaginatedResults.isEmpty()) {
+            if (optionalPaginatedResults.getError() != null) {
                 return;
             }
-            PaginatedResults<UserSearchResultDTO> paginatedResults = optionalPaginatedResults.get();
+            PaginatedResults<UserSearchResultDTO> paginatedResults = optionalPaginatedResults.getData();
             totalCount = (int) paginatedResults.getTotalCount();
 
             // Render Users List + Next Page Button
@@ -129,9 +130,9 @@ private Optional<PaginatedResults<UserSearchResultDTO>> handleSearchResponse(Opt
         return optionalPaginatedResults;
     }
 
-    private Optional<PaginatedResults<UserSearchResultDTO>> handleSearchException(Throwable throwable) {
+    private Result<PaginatedResults<UserSearchResultDTO>> handleSearchException(Throwable throwable) {
         System.out.println("Error searching for users: " + throwable.getMessage());
-        return Optional.empty();
+        return new Result<>();
     }
 
     private void selectUser(UserSearchResultDTO user) {
diff --git a/src/main/java/org/chainoptim/desktop/core/user/controller/UsersListByCustomRoleController.java b/src/main/java/org/chainoptim/desktop/core/user/controller/UsersListByCustomRoleController.java
index 2ac76b98..bbb7e54c 100644
--- a/src/main/java/org/chainoptim/desktop/core/user/controller/UsersListByCustomRoleController.java
+++ b/src/main/java/org/chainoptim/desktop/core/user/controller/UsersListByCustomRoleController.java
@@ -3,6 +3,7 @@
 import org.chainoptim.desktop.core.organization.model.CustomRole;
 import org.chainoptim.desktop.core.user.model.User;
 import org.chainoptim.desktop.core.user.service.UserService;
+import org.chainoptim.desktop.shared.httphandling.Result;
 import org.chainoptim.desktop.shared.util.DataReceiver;
 import com.google.inject.Inject;
 import javafx.application.Platform;
@@ -36,12 +37,12 @@ public void setData(CustomRole data) {
                 .exceptionally(this::handleUsersException);
     }
 
-    private Optional<List<User>> handleUsersResponse(Optional<List<User>> users) {
-        if (users.isEmpty()) {
-            return Optional.empty();
+    private Result<List<User>> handleUsersResponse(Result<List<User>> users) {
+        if (users.getError() != null) {
+            return new Result<>();
         }
         Platform.runLater(() -> {
-            List<User> userList = users.get();
+            List<User> userList = users.getData();
             usersVBox.getChildren().clear();
 
             for (User user : userList) {
@@ -53,9 +54,9 @@ private Optional<List<User>> handleUsersResponse(Optional<List<User>> users) {
         return users;
     }
 
-    private Optional<List<User>> handleUsersException(Throwable ex) {
+    private Result<List<User>> handleUsersException(Throwable ex) {
         ex.printStackTrace();
-        return Optional.empty();
+        return new Result<>();
     }
 
 }
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 7d99a282..07ecdd51 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
@@ -2,6 +2,7 @@
 
 import org.chainoptim.desktop.core.user.dto.UserSearchResultDTO;
 import org.chainoptim.desktop.core.user.model.User;
+import org.chainoptim.desktop.shared.httphandling.Result;
 import org.chainoptim.desktop.shared.search.model.PaginatedResults;
 
 import java.util.List;
@@ -10,11 +11,11 @@
 
 public interface UserService {
     // Read
-    CompletableFuture<Optional<User>> getUserByUsername(String username);
-    CompletableFuture<Optional<List<User>>> getUsersByCustomRoleId(Integer customRoleId);
-    CompletableFuture<Optional<PaginatedResults<UserSearchResultDTO>>> searchPublicUsers(String searchQuery, int page, int itemsPerPage);
+    CompletableFuture<Result<User>> getUserByUsername(String username);
+    CompletableFuture<Result<List<User>>> getUsersByCustomRoleId(Integer customRoleId);
+    CompletableFuture<Result<PaginatedResults<UserSearchResultDTO>>> searchPublicUsers(String searchQuery, int page, int itemsPerPage);
     // Write
-    CompletableFuture<Optional<User>> assignBasicRoleToUser(String userId, User.Role role);
-    CompletableFuture<Optional<User>> assignCustomRoleToUser(String userId, Integer roleId);
-    CompletableFuture<Optional<User>> removeUserFromOrganization(String userId, Integer organizationId);
+    CompletableFuture<Result<User>> assignBasicRoleToUser(String userId, User.Role role);
+    CompletableFuture<Result<User>> assignCustomRoleToUser(String userId, Integer roleId);
+    CompletableFuture<Result<User>> 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 76d02c1e..2e5fcfb3 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
@@ -4,212 +4,88 @@
 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.httphandling.HttpMethod;
+import org.chainoptim.desktop.shared.httphandling.RequestBuilder;
+import org.chainoptim.desktop.shared.httphandling.RequestHandler;
+import org.chainoptim.desktop.shared.httphandling.Result;
 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 com.google.inject.Inject;
 
-import java.net.HttpURLConnection;
-import java.net.URI;
 import java.net.URLEncoder;
-import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
-import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 
 public class UserServiceImpl implements UserService {
 
-    private final HttpClient client = HttpClient.newHttpClient();
-
-    private static final String HEADER_KEY = "Authorization";
-    private static final String HEADER_VALUE_PREFIX = "Bearer ";
+    private final RequestHandler requestHandler;
+    private final RequestBuilder requestBuilder;
+    private final TokenManager tokenManager;
     private static final String BASE_PATH = "http://localhost:8080/api/v1/users";
 
-    public CompletableFuture<Optional<User>> getUserByUsername(String username) {
+    @Inject
+    public UserServiceImpl(RequestHandler requestHandler, RequestBuilder requestBuilder, TokenManager tokenManager) {
+        this.requestHandler = requestHandler;
+        this.requestBuilder = requestBuilder;
+        this.tokenManager = tokenManager;
+    }
+
+    public CompletableFuture<Result<User>> getUserByUsername(String username) {
         String encodedUsername = URLEncoder.encode(username, StandardCharsets.UTF_8);
         String routeAddress = BASE_PATH + "/username/" + encodedUsername;
 
-        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) {
-                        try {
-                            User user = JsonUtil.getObjectMapper().readValue(response.body(), User.class);
-                            return Optional.of(user);
-                        } catch (Exception e) {
-                            e.printStackTrace();
-                        }
-                    }
-                    return Optional.empty();
-                });
+        HttpRequest request = requestBuilder.buildReadRequest(routeAddress, tokenManager.getToken());
+
+        return requestHandler.sendRequest(request, new TypeReference<User>() {});
     }
 
-    public CompletableFuture<Optional<List<User>>> getUsersByCustomRoleId(Integer customRoleId) {
+    public CompletableFuture<Result<List<User>>> getUsersByCustomRoleId(Integer customRoleId) {
         String routeAddress = BASE_PATH + "/search/custom-role/" + customRoleId;
 
-        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.<List<User>>empty();
-
-                    try {
-                        List<User> users = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference<List<User>>() {});
-                        return Optional.of(users);
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                    }
-
-                    return Optional.<List<User>>empty();
-                });
+        HttpRequest request = requestBuilder.buildReadRequest(routeAddress, tokenManager.getToken());
+
+        return requestHandler.sendRequest(request, new TypeReference<List<User>>() {});
     }
 
-    public CompletableFuture<Optional<PaginatedResults<UserSearchResultDTO>>> searchPublicUsers(String searchQuery, int page, int itemsPerPage) {
+    public CompletableFuture<Result<PaginatedResults<UserSearchResultDTO>>> 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.<PaginatedResults<UserSearchResultDTO>>empty();
-                    try {
-                        PaginatedResults<UserSearchResultDTO> userSearchResultDTO = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference<PaginatedResults<UserSearchResultDTO>>() {});
-                        return Optional.of(userSearchResultDTO);
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                    }
-                    return Optional.<PaginatedResults<UserSearchResultDTO>>empty();
-                });
+        HttpRequest request = requestBuilder.buildReadRequest(routeAddress, tokenManager.getToken());
+
+        return requestHandler.sendRequest(request, new TypeReference<PaginatedResults<UserSearchResultDTO>>() {});
     }
 
     // Write
-    public CompletableFuture<Optional<User>> assignBasicRoleToUser(String userId, User.Role role) {
+    public CompletableFuture<Result<User>> 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();
-                });
+        HttpRequest request = requestBuilder.buildWriteRequest(
+                HttpMethod.PUT, routeAddress, tokenManager.getToken(), new AssignBasicRoleDTO(role));
+        if (request == null) return requestHandler.getParsingErrorResult();
+
+        return requestHandler.sendRequest(request, new TypeReference<User>() {});
     }
 
-    public CompletableFuture<Optional<User>> assignCustomRoleToUser(String userId, Integer roleId) {
+    public CompletableFuture<Result<User>> assignCustomRoleToUser(String userId, Integer 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)
-                .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();
-                });
+        HttpRequest request = requestBuilder.buildWriteRequest(
+                HttpMethod.PUT, routeAddress, tokenManager.getToken(), new AssignCustomRoleDTO(roleId));
+
+        return requestHandler.sendRequest(request, new TypeReference<User>() {});
     }
 
-    public CompletableFuture<Optional<User>> removeUserFromOrganization(String userId, Integer organizationId) {
+    public CompletableFuture<Result<User>> 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.<User>empty();
-                    try {
-                        User user = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference<User>() {});
-                        return Optional.of(user);
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                        return Optional.<User>empty();
-                    }
-                });
+        HttpRequest request = requestBuilder.buildWriteRequest(
+                HttpMethod.PUT, routeAddress, tokenManager.getToken(), null);
+
+        return requestHandler.sendRequest(request, new TypeReference<User>() {});
     }
 }
 
diff --git a/src/main/java/org/chainoptim/desktop/shared/common/uielements/select/SelectOrCreateLocationController.java b/src/main/java/org/chainoptim/desktop/shared/common/uielements/select/SelectOrCreateLocationController.java
index aaf31ee1..0797eb96 100644
--- a/src/main/java/org/chainoptim/desktop/shared/common/uielements/select/SelectOrCreateLocationController.java
+++ b/src/main/java/org/chainoptim/desktop/shared/common/uielements/select/SelectOrCreateLocationController.java
@@ -7,6 +7,7 @@
 import org.chainoptim.desktop.shared.features.location.dto.CreateLocationDTO;
 import org.chainoptim.desktop.shared.features.location.model.Location;
 import org.chainoptim.desktop.shared.features.location.service.LocationService;
+import org.chainoptim.desktop.shared.httphandling.Result;
 
 import com.google.inject.Inject;
 import javafx.application.Platform;
@@ -96,19 +97,19 @@ protected void updateItem(Location item, boolean empty) {
                 .exceptionally(this::handleLocationsException);
     }
 
-    private Optional<List<Location>> handleLocationsResponse(Optional<List<Location>> locationsOptional) {
+    private Result<List<Location>> handleLocationsResponse(Result<List<Location>> result) {
         Platform.runLater(() -> {
-            if (locationsOptional.isEmpty()) {
+            if (result.getError() != null) {
                 return;
             }
-            locationComboBox.getItems().setAll(locationsOptional.get());
+            locationComboBox.getItems().setAll(result.getData());
 
         });
-        return locationsOptional;
+        return result;
     }
 
-    private Optional<List<Location>> handleLocationsException(Throwable ex) {
-        return Optional.empty();
+    private Result<List<Location>> handleLocationsException(Throwable ex) {
+        return new Result<>();
     }
 
     private void toggleVisibilityBasedOnSelection() {
diff --git a/src/main/java/org/chainoptim/desktop/shared/features/location/service/LocationService.java b/src/main/java/org/chainoptim/desktop/shared/features/location/service/LocationService.java
index a599e37f..0859de51 100644
--- a/src/main/java/org/chainoptim/desktop/shared/features/location/service/LocationService.java
+++ b/src/main/java/org/chainoptim/desktop/shared/features/location/service/LocationService.java
@@ -3,6 +3,7 @@
 import org.chainoptim.desktop.shared.features.location.dto.CreateLocationDTO;
 import org.chainoptim.desktop.shared.features.location.dto.UpdateLocationDTO;
 import org.chainoptim.desktop.shared.features.location.model.Location;
+import org.chainoptim.desktop.shared.httphandling.Result;
 
 import java.util.List;
 import java.util.Optional;
@@ -10,8 +11,8 @@
 
 public interface LocationService {
 
-    CompletableFuture<Optional<List<Location>>> getLocationsByOrganizationId(Integer organizationId);
-    CompletableFuture<Optional<Location>> createLocation(CreateLocationDTO locationDTO);
-    CompletableFuture<Optional<Location>> updateLocation(UpdateLocationDTO locationDTO);
-    CompletableFuture<Optional<Integer>> deleteLocation(Integer id);
+    CompletableFuture<Result<List<Location>>> getLocationsByOrganizationId(Integer organizationId);
+    CompletableFuture<Result<Location>> createLocation(CreateLocationDTO locationDTO);
+    CompletableFuture<Result<Location>> updateLocation(UpdateLocationDTO locationDTO);
+    CompletableFuture<Result<Integer>> deleteLocation(Integer id);
 }
diff --git a/src/main/java/org/chainoptim/desktop/shared/features/location/service/LocationServiceImpl.java b/src/main/java/org/chainoptim/desktop/shared/features/location/service/LocationServiceImpl.java
index 9d5f7154..7ad0e37c 100644
--- a/src/main/java/org/chainoptim/desktop/shared/features/location/service/LocationServiceImpl.java
+++ b/src/main/java/org/chainoptim/desktop/shared/features/location/service/LocationServiceImpl.java
@@ -1,151 +1,73 @@
 package org.chainoptim.desktop.shared.features.location.service;
 
-import com.fasterxml.jackson.core.type.TypeReference;
-import org.chainoptim.desktop.core.user.util.TokenManager;
-import org.chainoptim.desktop.features.product.dto.CreateUnitOfMeasurementDTO;
-import org.chainoptim.desktop.features.product.dto.UpdateUnitOfMeasurementDTO;
-import org.chainoptim.desktop.features.product.model.UnitOfMeasurement;
+import org.chainoptim.desktop.core.user.service.TokenManager;
 import org.chainoptim.desktop.shared.features.location.dto.CreateLocationDTO;
 import org.chainoptim.desktop.shared.features.location.dto.UpdateLocationDTO;
 import org.chainoptim.desktop.shared.features.location.model.Location;
-import org.chainoptim.desktop.shared.util.JsonUtil;
+import org.chainoptim.desktop.shared.httphandling.HttpMethod;
+import org.chainoptim.desktop.shared.httphandling.RequestBuilder;
+import org.chainoptim.desktop.shared.httphandling.RequestHandler;
+import org.chainoptim.desktop.shared.httphandling.Result;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.google.inject.Inject;
 
-import java.net.HttpURLConnection;
-import java.net.URI;
-import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
 import java.util.List;
-import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 
 public class LocationServiceImpl implements LocationService {
 
-    private final HttpClient client = HttpClient.newHttpClient();
-
-    private static final String HEADER_KEY = "Authorization";
-    private static final String HEADER_VALUE_PREFIX = "Bearer ";
+    private final RequestHandler requestHandler;
+    private final RequestBuilder requestBuilder;
+    private final TokenManager tokenManager;
+
+    @Inject
+    public LocationServiceImpl(RequestHandler requestHandler,
+                               RequestBuilder requestBuilder,
+                               TokenManager tokenManager) {
+        this.requestHandler = requestHandler;
+        this.requestBuilder = requestBuilder;
+        this.tokenManager = tokenManager;
+    }
 
     // Fetch
-    public CompletableFuture<Optional<List<Location>>> getLocationsByOrganizationId(Integer organizationId) {
+    public CompletableFuture<Result<List<Location>>> getLocationsByOrganizationId(Integer organizationId) {
         String routeAddress = "http://localhost:8080/api/v1/locations/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))
-                .GET()
-                .headers(HEADER_KEY, headerValue)
-                .build();
-
-        return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
-                .thenApply(response -> {
-                    if (response.statusCode() != HttpURLConnection.HTTP_OK) return Optional.<List<Location>>empty();
-                    try {
-                        List<Location> locations = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference<List<Location>>() {});
-                        return Optional.of(locations);
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                        return Optional.<List<Location>>empty();
-                    }
-                });
+        HttpRequest request = requestBuilder.buildReadRequest(routeAddress, tokenManager.getToken());
+
+        return requestHandler.sendRequest(request, new TypeReference<List<Location>>() {});
     }
 
     // Create
-    public CompletableFuture<Optional<Location>> createLocation(CreateLocationDTO locationDTO) {
+    public CompletableFuture<Result<Location>> createLocation(CreateLocationDTO locationDTO) {
         String routeAddress = "http://localhost:8080/api/v1/locations/create";
 
-        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(locationDTO);
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-        assert requestBody != null;
-
-        HttpRequest request = HttpRequest.newBuilder()
-                .uri(URI.create(routeAddress))
-                .POST(HttpRequest.BodyPublishers.ofString(requestBody))
-                .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 {
-                        Location location = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference<Location>() {});
-                        return Optional.of(location);
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                        return Optional.<Location>empty();
-                    }
-                });
+        HttpRequest request = requestBuilder.buildWriteRequest(
+                HttpMethod.POST, routeAddress, tokenManager.getToken(), locationDTO);
+
+        return requestHandler.sendRequest(request, new TypeReference<Location>() {});
     }
 
     // Update
-    public CompletableFuture<Optional<Location>> updateLocation(UpdateLocationDTO locationDTO) {
+    public CompletableFuture<Result<Location>> updateLocation(UpdateLocationDTO locationDTO) {
         String routeAddress = "http://localhost:8080/api/v1/locations/update";
 
-        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(locationDTO);
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-        assert requestBody != null;
-
-        HttpRequest request = HttpRequest.newBuilder()
-                .uri(URI.create(routeAddress))
-                .POST(HttpRequest.BodyPublishers.ofString(requestBody))
-                .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 {
-                        Location location = JsonUtil.getObjectMapper().readValue(response.body(), new TypeReference<Location>() {});
-                        return Optional.of(location);
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                        return Optional.<Location>empty();
-                    }
-                });
+        HttpRequest request = requestBuilder.buildWriteRequest(
+                HttpMethod.PUT, routeAddress, tokenManager.getToken(), locationDTO);
+
+        return requestHandler.sendRequest(request, new TypeReference<Location>() {});
     }
 
     // Delete
-    public CompletableFuture<Optional<Integer>> deleteLocation(Integer id) {
+    public CompletableFuture<Result<Integer>> deleteLocation(Integer id) {
         String routeAddress = "http://localhost:8080/api/v1/locations/delete/" + id.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))
-                .DELETE()
-                .headers(HEADER_KEY, headerValue)
-                .build();
-
-        return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
-                .thenApply(response -> {
-                    if (response.statusCode() != HttpURLConnection.HTTP_OK) return Optional.empty();
-                    return Optional.of(id);
-                });
+        HttpRequest request = requestBuilder.buildWriteRequest(
+                HttpMethod.DELETE, routeAddress, tokenManager.getToken(), null);
+
+        return requestHandler.sendRequest(request, new TypeReference<Integer>() {});
     }
 
 }

From 61e358390ee30b625d7b5ee5d40d83a1e1f29ad0 Mon Sep 17 00:00:00 2001
From: TudorOrban <130213626+TudorOrban@users.noreply.github.com>
Date: Thu, 25 Apr 2024 12:12:21 +0300
Subject: [PATCH 02/10] Make small UI adjustments

---
 .../NotificationSettingsController.java       |  4 ++
 .../controller/ProductsController.java        | 30 ---------------
 .../common/uielements/settings/Switch.java    | 38 +++++++++++++++++++
 src/main/resources/css/buttons.css            |  5 +++
 src/main/resources/css/entity-card.css        |  5 ++-
 src/main/resources/css/entity-page.css        |  5 +--
 src/main/resources/css/factory-production.css |  3 ++
 src/main/resources/css/list-header.css        |  1 +
 src/main/resources/css/settings.css           | 28 +++++++++++++-
 src/main/resources/css/sidebar.css            |  2 +-
 .../settings/NotificationSettingsView.fxml    |  2 +
 .../features/product/ProductsView.fxml        |  7 +---
 12 files changed, 87 insertions(+), 43 deletions(-)
 create mode 100644 src/main/java/org/chainoptim/desktop/shared/common/uielements/settings/Switch.java

diff --git a/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java b/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java
index ed39511a..e10f6932 100644
--- a/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java
+++ b/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java
@@ -1,6 +1,7 @@
 package org.chainoptim.desktop.core.settings.controller;
 
 import org.chainoptim.desktop.core.settings.model.UserSettings;
+import org.chainoptim.desktop.shared.common.uielements.settings.Switch;
 import org.chainoptim.desktop.shared.util.DataReceiver;
 
 import javafx.beans.property.BooleanProperty;
@@ -38,12 +39,15 @@ public class NotificationSettingsController implements DataReceiver<UserSettings
     // FXML
     @FXML
     private VBox contentVBox;
+    @FXML
+    private Switch overallSwitch;
     private final ToggleButton toggleOverallButton = new ToggleButton();
     private final Map<String, ToggleButton> featureToggleButtons = new HashMap<>();
 
     @Override
     public void setData(UserSettings userSettings) {
         this.userSettings = userSettings;
+        overallSwitch.initializeSwitch(aggregateNotificationSettings());
         initializeUI();
     }
 
diff --git a/src/main/java/org/chainoptim/desktop/features/product/controller/ProductsController.java b/src/main/java/org/chainoptim/desktop/features/product/controller/ProductsController.java
index f5691d48..b3fb7f4a 100644
--- a/src/main/java/org/chainoptim/desktop/features/product/controller/ProductsController.java
+++ b/src/main/java/org/chainoptim/desktop/features/product/controller/ProductsController.java
@@ -8,14 +8,11 @@
 import org.chainoptim.desktop.features.product.model.Product;
 import org.chainoptim.desktop.features.product.service.ProductService;
 import org.chainoptim.desktop.shared.enums.Feature;
-import org.chainoptim.desktop.shared.enums.OperationOutcome;
 import org.chainoptim.desktop.shared.fallback.FallbackManager;
 import org.chainoptim.desktop.shared.httphandling.Result;
 import org.chainoptim.desktop.shared.search.controller.PageSelectorController;
 import org.chainoptim.desktop.shared.search.model.PaginatedResults;
 import org.chainoptim.desktop.shared.search.model.SearchParams;
-import org.chainoptim.desktop.shared.toast.controller.ToastManager;
-import org.chainoptim.desktop.shared.toast.model.ToastInfo;
 import org.chainoptim.desktop.shared.util.resourceloader.CommonViewsLoader;
 
 import com.google.inject.Inject;
@@ -36,7 +33,6 @@ public class ProductsController implements Initializable {
     private final NavigationService navigationService;
     private final CurrentSelectionService currentSelectionService;
     private final CommonViewsLoader commonViewsLoader;
-    private final ToastManager toastManager;
 
     // State
     private final FallbackManager fallbackManager;
@@ -68,7 +64,6 @@ public ProductsController(ProductService productService,
                               NavigationService navigationService,
                               CurrentSelectionService currentSelectionService,
                               CommonViewsLoader commonViewsLoader,
-                              ToastManager toastManager,
                               FallbackManager fallbackManager,
                               SearchParams searchParams
     ) {
@@ -76,35 +71,10 @@ public ProductsController(ProductService productService,
         this.navigationService = navigationService;
         this.currentSelectionService = currentSelectionService;
         this.commonViewsLoader = commonViewsLoader;
-        this.toastManager = toastManager;
         this.fallbackManager = fallbackManager;
         this.searchParams = searchParams;
     }
 
-    @FXML
-    private void addToast() {
-        ToastInfo toastInfo = new ToastInfo("Success", "The product has been created successfully", OperationOutcome.SUCCESS);
-        toastManager.addToast(toastInfo);
-    }
-
-    @FXML
-    private void addToastError() {
-        ToastInfo toastInfo = new ToastInfo("Error", "There was an error creating the product", OperationOutcome.ERROR);
-        toastManager.addToast(toastInfo);
-    }
-
-    @FXML
-    private void addToastInfo() {
-        ToastInfo toastInfo = new ToastInfo("Info", "A Product is any good manufactured by your organization", OperationOutcome.INFO);
-        toastManager.addToast(toastInfo);
-    }
-
-    @FXML
-    private void addToastWarning() {
-        ToastInfo toastInfo = new ToastInfo("Warning", "You are about to delete a product", OperationOutcome.WARNING);
-        toastManager.addToast(toastInfo);
-    }
-
     @Override
     public void initialize(URL location, ResourceBundle resources) {
         headerController = commonViewsLoader.loadListHeader(headerContainer);
diff --git a/src/main/java/org/chainoptim/desktop/shared/common/uielements/settings/Switch.java b/src/main/java/org/chainoptim/desktop/shared/common/uielements/settings/Switch.java
new file mode 100644
index 00000000..c688797d
--- /dev/null
+++ b/src/main/java/org/chainoptim/desktop/shared/common/uielements/settings/Switch.java
@@ -0,0 +1,38 @@
+package org.chainoptim.desktop.shared.common.uielements.settings;
+
+import javafx.scene.control.Button;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
+import lombok.Getter;
+
+@Getter
+public class Switch extends HBox {
+
+    private boolean isOn;
+
+    private Button leftButton;
+    private Button rightButton;
+
+    public void initializeSwitch(boolean isOn) {
+        this.getStyleClass().setAll("custom-switch");
+        this.setMaxWidth(Region.USE_PREF_SIZE);
+
+        leftButton = new Button("On");
+        leftButton.setStyle("-fx-border-radius: 5 0 0 5; -fx-background-radius: 5 0 0 5; -fx-border-width: 1 0 1 1;");
+        leftButton.setOnAction(event -> toggle(true));
+
+        rightButton = new Button("Off");
+        rightButton.setStyle("-fx-border-radius: 0 5 5 0; -fx-background-radius: 0 5 5 0; -fx-border-width: 1 1 1 0;");
+        rightButton.setOnAction(event -> toggle(false));
+
+        toggle(isOn);
+
+        this.getChildren().addAll(leftButton, rightButton);
+    }
+
+    private void toggle(boolean isOn) {
+        leftButton.getStyleClass().setAll(isOn ? "switch-select-item-selected" : "switch-select-item");
+        rightButton.getStyleClass().setAll(isOn ? "switch-select-item" : "switch-select-item-selected");
+        this.isOn = isOn;
+    }
+}
diff --git a/src/main/resources/css/buttons.css b/src/main/resources/css/buttons.css
index cc1a126e..77b7580d 100644
--- a/src/main/resources/css/buttons.css
+++ b/src/main/resources/css/buttons.css
@@ -10,6 +10,7 @@
     -fx-font-size: 14px;
     -fx-graphic-text-gap: 8px;
     -fx-cursor: hand;
+    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.15), 5, 0, 0, 2);
 }
 
 .standard-write-button:hover {
@@ -39,6 +40,7 @@
     -fx-font-size: 13px;
     -fx-graphic-text-gap: 8px;
     -fx-cursor: hand;
+    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.1), 5, 0, 0, 2);
 }
 
 .standard-action-button:hover {
@@ -57,6 +59,7 @@
     -fx-font-size: 14px;
     -fx-graphic-text-gap: 8px;
     -fx-cursor: hand;
+    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.15), 5, 0, 0, 2);
 }
 
 .standard-delete-button:hover {
@@ -75,6 +78,7 @@
     -fx-font-size: 14px;
     -fx-graphic-text-gap: 8px;
     -fx-cursor: hand;
+    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.15), 5, 0, 0, 2);
 }
 
 .standard-cancel-button:hover {
@@ -116,6 +120,7 @@
     -fx-border-radius: 6px;
     -fx-graphic-text-gap: 8px;
     -fx-cursor: hand;
+    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.15), 5, 0, 0, 2);
 }
 
 .cancel-edit-button:hover {
diff --git a/src/main/resources/css/entity-card.css b/src/main/resources/css/entity-card.css
index 02d45246..8fc1cbe9 100644
--- a/src/main/resources/css/entity-card.css
+++ b/src/main/resources/css/entity-card.css
@@ -1,9 +1,10 @@
 .entity-card {
-    -fx-background-color: #f1f2f2;
-    -fx-border-color: #bdbfad;
+    -fx-background-color: #f2f2f2;
+    -fx-border-color: #cdcdcd;
     -fx-border-width: 1px;
     -fx-border-radius: 5px;
     -fx-padding: 8px 12px;
+    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.05), 5, 0, 0, 2);
 }
 
 .entity-card:hover {
diff --git a/src/main/resources/css/entity-page.css b/src/main/resources/css/entity-page.css
index 6d478118..b42b3bf8 100644
--- a/src/main/resources/css/entity-page.css
+++ b/src/main/resources/css/entity-page.css
@@ -1,9 +1,8 @@
-
-
 .entity-header-container {
     -fx-padding: 20;
-    -fx-background-color: #eeeeee;
+    -fx-background-color: #f2f2f2;
     -fx-border-color: #c0c0c0 #ffffff #c0c0c0 #ffffff;
+    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.1), 4, 0, 0, 2);
 }
 
 .entity-page-title {
diff --git a/src/main/resources/css/factory-production.css b/src/main/resources/css/factory-production.css
index 7e2d47ca..af89eaf0 100644
--- a/src/main/resources/css/factory-production.css
+++ b/src/main/resources/css/factory-production.css
@@ -44,6 +44,7 @@
     -fx-pref-width: 24px;
     -fx-background-color: #0077C7;
     -fx-cursor: hand;
+    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.15), 5, 0, 0, 2);
 }
 
 .add-button:hover {
@@ -55,6 +56,7 @@
     -fx-pref-width: 24px;
     -fx-background-color: #00A062;
     -fx-cursor: hand;
+    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.15), 5, 0, 0, 2);
 }
 
 .update-button:hover {
@@ -66,6 +68,7 @@
     -fx-pref-width: 24px;
     -fx-background-color: #B71011;
     -fx-cursor: hand;
+    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.15), 5, 0, 0, 2);
 }
 
 .delete-button:hover {
diff --git a/src/main/resources/css/list-header.css b/src/main/resources/css/list-header.css
index 3181a175..621a6a87 100644
--- a/src/main/resources/css/list-header.css
+++ b/src/main/resources/css/list-header.css
@@ -2,6 +2,7 @@
     -fx-background-color: #ffffff;
     -fx-spacing: 20px;
     -fx-border-color: #c0c0c0;
+    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.15), 5, 0, 0, 2);
 }
 
 .toolbar {
diff --git a/src/main/resources/css/settings.css b/src/main/resources/css/settings.css
index d44d4e3e..807c08bd 100644
--- a/src/main/resources/css/settings.css
+++ b/src/main/resources/css/settings.css
@@ -26,4 +26,30 @@
 .settings-label {
     -fx-font-size: 14px;
     -fx-font-weight: bold;
-}
\ No newline at end of file
+}
+
+.custom-switch {
+    -fx-background-radius: 5;
+    -fx-border-radius: 5;
+    -fx-border-color: #c0c0c0;
+    -fx-border-width: 1;
+}
+
+.switch-select-item {
+    -fx-padding: 4px 8px;
+    -fx-background-color: #efeeef;
+    -fx-font-size: 14px;
+    -fx-font-weight: bold;
+    -fx-alignment: center;
+    -fx-cursor: hand;
+}
+
+.switch-select-item-selected {
+    -fx-padding: 4px 8px;
+    -fx-background-color: #006AEE;
+    -fx-text-fill: white;
+    -fx-font-size: 14px;
+    -fx-font-weight: bold;
+    -fx-alignment: center;
+    -fx-cursor: hand;
+}
diff --git a/src/main/resources/css/sidebar.css b/src/main/resources/css/sidebar.css
index ecc8c292..8b019b40 100644
--- a/src/main/resources/css/sidebar.css
+++ b/src/main/resources/css/sidebar.css
@@ -3,7 +3,7 @@
     -fx-background-radius: 0px 6px 6px 0px;
     -fx-border-color: #333;
     -fx-border-radius: 0px 6px 6px 0px;
-    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.16), 5, 0, 0, 2);
+    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.25), 5, 0, 0, 2);
 }
 
 .sidebar-inner-container {
diff --git a/src/main/resources/org/chainoptim/desktop/core/settings/NotificationSettingsView.fxml b/src/main/resources/org/chainoptim/desktop/core/settings/NotificationSettingsView.fxml
index d8066a6e..63486d26 100644
--- a/src/main/resources/org/chainoptim/desktop/core/settings/NotificationSettingsView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/core/settings/NotificationSettingsView.fxml
@@ -1,11 +1,13 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?import javafx.scene.layout.*?>
 
+<?import org.chainoptim.desktop.shared.common.uielements.settings.Switch?>
 <VBox xmlns="http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
       fx:controller="org.chainoptim.desktop.core.settings.controller.NotificationSettingsController"
       prefHeight="400.0" prefWidth="600.0">
 
+    <Switch fx:id="overallSwitch" />
     <VBox fx:id="contentVBox" styleClass="settings-tab-container">
         <!-- Overall Settings -->
 
diff --git a/src/main/resources/org/chainoptim/desktop/features/product/ProductsView.fxml b/src/main/resources/org/chainoptim/desktop/features/product/ProductsView.fxml
index 5909d2c2..76b16b4a 100644
--- a/src/main/resources/org/chainoptim/desktop/features/product/ProductsView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/product/ProductsView.fxml
@@ -1,18 +1,13 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?import javafx.scene.layout.*?>
-
 <?import javafx.scene.control.ScrollPane?>
-<?import javafx.scene.control.Button?>
+
 <VBox xmlns="http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
       styleClass="main-container"
       fx:controller="org.chainoptim.desktop.features.product.controller.ProductsController">
 
     <StackPane fx:id="headerContainer"/>
-    <Button onAction="#addToast"/>
-    <Button onAction="#addToastError"/>
-    <Button onAction="#addToastInfo"/>
-    <Button onAction="#addToastWarning"/>
 
     <StackPane VBox.vgrow="ALWAYS">
         <ScrollPane fx:id="productsScrollPane" VBox.vgrow="ALWAYS" fitToWidth="true" fitToHeight="true">

From a8c4188c9a463255dbbf3ad69a31420c9a12c673 Mon Sep 17 00:00:00 2001
From: TudorOrban <130213626+TudorOrban@users.noreply.github.com>
Date: Thu, 25 Apr 2024 16:18:18 +0300
Subject: [PATCH 03/10] Switch to a third-party ToggleButton

---
 pom.xml                                       |  5 ++
 .../controller/GeneralSettingsController.java |  2 +-
 .../NotificationSettingsController.java       | 66 ++++++++++---------
 .../uielements/settings/EnumSelector.java     | 41 ++++++++++--
 .../common/uielements/settings/Switch.java    | 28 +++++---
 src/main/resources/css/buttons.css            |  3 +-
 src/main/resources/css/common-elements.css    | 34 ++++++++--
 src/main/resources/css/settings.css           | 49 ++++++++------
 .../settings/NotificationSettingsView.fxml    |  2 -
 9 files changed, 151 insertions(+), 79 deletions(-)

diff --git a/pom.xml b/pom.xml
index 73071dc0..3f7175a6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -99,6 +99,11 @@
                 </exclusion>
             </exclusions>
         </dependency>
+        <dependency>
+            <groupId>com.jfoenix</groupId>
+            <artifactId>jfoenix</artifactId>
+            <version>9.0.10</version>
+        </dependency>
         <dependency>
             <groupId>com.google.inject</groupId>
             <artifactId>guice</artifactId>
diff --git a/src/main/java/org/chainoptim/desktop/core/settings/controller/GeneralSettingsController.java b/src/main/java/org/chainoptim/desktop/core/settings/controller/GeneralSettingsController.java
index 2b2ce004..4042ac0b 100644
--- a/src/main/java/org/chainoptim/desktop/core/settings/controller/GeneralSettingsController.java
+++ b/src/main/java/org/chainoptim/desktop/core/settings/controller/GeneralSettingsController.java
@@ -47,6 +47,6 @@ private void setUpListeners() {
     }
 
     public void cancelChanges(UserSettings originalUserSettings) {
-        infoLevelSelector.selectValue(originalUserSettings.getGeneralSettings().getInfoLevel());
+        infoLevelSelector.selectValue(originalUserSettings.getGeneralSettings().getInfoLevel(), InfoLevel.class);
     }
 }
diff --git a/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java b/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java
index e10f6932..9ed7898a 100644
--- a/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java
+++ b/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java
@@ -1,20 +1,21 @@
 package org.chainoptim.desktop.core.settings.controller;
 
 import org.chainoptim.desktop.core.settings.model.UserSettings;
-import org.chainoptim.desktop.shared.common.uielements.settings.Switch;
 import org.chainoptim.desktop.shared.util.DataReceiver;
 
+import com.jfoenix.controls.JFXToggleButton;
 import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.SimpleBooleanProperty;
 import javafx.beans.value.ChangeListener;
 import javafx.beans.value.ObservableValue;
 import javafx.fxml.FXML;
+import javafx.geometry.Pos;
 import javafx.scene.control.Label;
-import javafx.scene.control.ToggleButton;
 import javafx.scene.layout.HBox;
 import javafx.scene.layout.Priority;
 import javafx.scene.layout.Region;
 import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
 import lombok.Setter;
 
 import java.util.HashMap;
@@ -39,21 +40,18 @@ public class NotificationSettingsController implements DataReceiver<UserSettings
     // FXML
     @FXML
     private VBox contentVBox;
-    @FXML
-    private Switch overallSwitch;
-    private final ToggleButton toggleOverallButton = new ToggleButton();
-    private final Map<String, ToggleButton> featureToggleButtons = new HashMap<>();
+    private JFXToggleButton overallToggleButton = new JFXToggleButton();
+    private final Map<String, JFXToggleButton> featureToggleButtons = new HashMap<>();
 
     @Override
     public void setData(UserSettings userSettings) {
         this.userSettings = userSettings;
-        overallSwitch.initializeSwitch(aggregateNotificationSettings());
         initializeUI();
     }
 
     private void initializeUI() {
         contentVBox.getChildren().clear();
-        contentVBox.setSpacing(10);
+        contentVBox.setSpacing(12);
 
         cleanUpGlobalListeners();
 
@@ -72,7 +70,7 @@ private void cleanUpGlobalListeners() {
             haveSettingsChanged.removeListener(hasChangedListener);
         }
         if (overallChangeListener != null) {
-            toggleOverallButton.selectedProperty().removeListener(overallChangeListener);
+            overallToggleButton.selectedProperty().removeListener(overallChangeListener);
         }
     }
 
@@ -87,9 +85,9 @@ private void setUpGlobalListeners() {
 
         // Overall change listener
         overallChangeListener = (ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
-            handleToggleOverallButton(userSettings);
+            handleToggleOverallSwitch();
         };
-        toggleOverallButton.selectedProperty().addListener(overallChangeListener);
+        overallToggleButton.selectedProperty().addListener(overallChangeListener);
     }
 
     private void renderOverallHBox() {
@@ -102,17 +100,18 @@ private void renderOverallHBox() {
         overallHBox.getChildren().add(region);
         HBox.setHgrow(region, Priority.ALWAYS);
 
-        toggleOverallButton.getStyleClass().setAll("toggle-button");
+        overallToggleButton = new JFXToggleButton();
         boolean overallSetting = aggregateNotificationSettings();
-        toggleOverallButton.setText(overallSetting ? "On" : "Off");
-        toggleOverallButton.setSelected(overallSetting);
-        overallHBox.getChildren().add(toggleOverallButton);
+        overallToggleButton.setSelected(overallSetting);
+        styleToggleButton(overallToggleButton);
+        overallHBox.getChildren().add(overallToggleButton);
 
         contentVBox.getChildren().add(overallHBox);
     }
 
     private void renderFeatureHBox(String feature) {
         HBox featureHBox = new HBox();
+        featureHBox.setAlignment(Pos.CENTER_LEFT);
         Label featureLabel = new Label(feature);
         featureLabel.getStyleClass().add("settings-label");
         featureHBox.getChildren().add(featureLabel);
@@ -121,40 +120,35 @@ private void renderFeatureHBox(String feature) {
         featureHBox.getChildren().add(region);
         HBox.setHgrow(region, Priority.ALWAYS);
 
-        ToggleButton toggleButton = new ToggleButton();
+        JFXToggleButton toggleButton = new JFXToggleButton();
         boolean featureSetting = getFeatureSetting(feature);
-        toggleButton.setText(featureSetting ? "On" : "Off");
         toggleButton.setSelected(featureSetting);
-        toggleButton.getStyleClass().setAll("toggle-button");
+        styleToggleButton(toggleButton);
         toggleButton.selectedProperty().addListener((observable, oldValue, newValue) ->
-                handleToggleFeatureButton(userSettings, toggleButton, feature, toggleButton.isSelected()));
+                handleToggleFeatureSwitch(featureToggleButtons.get(feature), feature, newValue));
         featureToggleButtons.put(feature, toggleButton);
         featureHBox.getChildren().add(toggleButton);
 
         contentVBox.getChildren().add(featureHBox);
     }
 
-    private void handleToggleOverallButton(UserSettings userSettings) {
-        if (toggleOverallButton.isSelected()) {
-            toggleOverallButton.setText("On");
-        } else {
-            toggleOverallButton.setText("Off");
-        }
+    private void handleToggleOverallSwitch() {
+        boolean newState = overallToggleButton.isSelected();
         for (String feature : notificationFeatures) {
-            ToggleButton toggleButton = featureToggleButtons.get(feature);
-            handleToggleFeatureButton(userSettings, toggleButton, feature, toggleOverallButton.isSelected());
+            JFXToggleButton featureToggleButton = featureToggleButtons.get(feature);
+            handleToggleFeatureSwitch(featureToggleButton, feature, newState);
         }
     }
 
-    private void handleToggleFeatureButton(UserSettings userSettings, ToggleButton toggleButton, String feature, Boolean isOn) {
-        toggleButton.setSelected(isOn);
-        toggleButton.setText(Boolean.TRUE.equals(isOn) ? "On" : "Off");
+    private void handleToggleFeatureSwitch(JFXToggleButton featureToggleButton, String feature, Boolean isOn) {
+        featureToggleButton.setSelected(isOn);
         haveSettingsChanged.setValue(true);
+        System.out.println("Settings changed: " + haveSettingsChanged);
         setFeatureSetting(feature, isOn);
     }
 
     public void cancelChanges(UserSettings originalUserSettings) {
-        toggleOverallButton.selectedProperty().removeListener(overallChangeListener);
+        overallToggleButton.selectedProperty().removeListener(overallChangeListener);
         setData(originalUserSettings);
         haveSettingsChanged.setValue(false);
     }
@@ -186,4 +180,14 @@ private void setFeatureSetting(String feature, boolean isOn) {
             default -> {}
         }
     }
+
+    private void styleToggleButton(JFXToggleButton toggleButton) {
+        toggleButton.setStyle("-fx-background-color: transparent; -fx-border-width: 0; -fx-border-color: transparent;");
+        toggleButton.setUnToggleColor(Color.valueOf("#cccccc"));
+        toggleButton.setToggleColor(Color.valueOf("#006AEE"));
+        toggleButton.setToggleLineColor(Color.valueOf("#337BEF"));
+        toggleButton.setBorder(null);
+        toggleButton.setDisableVisualFocus(true);
+        toggleButton.setMinSize(50, 30);
+    }
 }
diff --git a/src/main/java/org/chainoptim/desktop/shared/common/uielements/settings/EnumSelector.java b/src/main/java/org/chainoptim/desktop/shared/common/uielements/settings/EnumSelector.java
index 2d00fe37..671b0f7d 100644
--- a/src/main/java/org/chainoptim/desktop/shared/common/uielements/settings/EnumSelector.java
+++ b/src/main/java/org/chainoptim/desktop/shared/common/uielements/settings/EnumSelector.java
@@ -5,8 +5,11 @@
 import javafx.geometry.Pos;
 import javafx.scene.control.Button;
 import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 public class EnumSelector<T extends Enum<T>> extends HBox {
@@ -18,28 +21,52 @@ public EnumSelector() {
         super();
 
         this.setAlignment(Pos.CENTER_LEFT);
+        this.setMaxWidth(Region.USE_PREF_SIZE);
+        this.getStyleClass().setAll("enum-selector");
     }
 
     public void initializeSelector(Class<T> enumType, T selectedValue) {
         if (selectedValue == null) return;
         this.selectedValueProperty.setValue(selectedValue);
 
+        int index = 0;
+
         for (T value : enumType.getEnumConstants()) {
             // Create a button for each enum value
             Button button = new Button(value.toString());
-            button.getStyleClass().setAll(
-                    selectedValue.equals(value) ? "select-item-selected" : "select-item"
-            );
-            button.setOnAction(event -> selectValue(value));
+
+            styleButton(selectedValue, value, button, index, enumType);
+
+            button.setOnAction(event -> selectValue(value, enumType));
+
             this.getChildren().add(button);
             buttons.put(value, button);
+
+            index++;
         }
     }
 
-    public void selectValue(T value) {
+    private void styleButton(T selectedValue, T value, Button button, int index, Class<T> enumType) {
+        List<String> styles = new ArrayList<>();
+        styles.add(selectedValue.equals(value) ? "enum-select-item-selected" : "enum-select-item");
+        if (index == 0) {
+            styles.add("enum-left-item");
+        }
+        if (index == enumType.getEnumConstants().length - 1) {
+            styles.add("enum-right-item");
+        }
+        button.getStyleClass().setAll(styles);
+    }
+
+    public void selectValue(T value, Class<T> enumType) {
         selectedValueProperty.setValue(value);
-        buttons.forEach((k, v) -> v.getStyleClass().setAll(
-                k.equals(selectedValueProperty.getValue()) ? "select-item-selected" : "select-item"
+        int index = 0;
+        for (Map.Entry<T, Button> entry : buttons.entrySet()) {
+            styleButton(value, entry.getKey(), entry.getValue(), index, enumType);
+            index++;
+        }
+        buttons.forEach((k, v) -> v.getStyleClass().add(
+                k.equals(selectedValueProperty.getValue()) ? "enum-select-item-selected" : "enum-select-item"
         ));
     }
 
diff --git a/src/main/java/org/chainoptim/desktop/shared/common/uielements/settings/Switch.java b/src/main/java/org/chainoptim/desktop/shared/common/uielements/settings/Switch.java
index c688797d..5452578f 100644
--- a/src/main/java/org/chainoptim/desktop/shared/common/uielements/settings/Switch.java
+++ b/src/main/java/org/chainoptim/desktop/shared/common/uielements/settings/Switch.java
@@ -1,28 +1,32 @@
 package org.chainoptim.desktop.shared.common.uielements.settings;
 
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
 import javafx.scene.control.Button;
 import javafx.scene.layout.HBox;
 import javafx.scene.layout.Region;
 import lombok.Getter;
 
+import java.util.ArrayList;
+import java.util.List;
+
 @Getter
 public class Switch extends HBox {
 
-    private boolean isOn;
+    private BooleanProperty isOn = new SimpleBooleanProperty();
 
     private Button leftButton;
     private Button rightButton;
 
     public void initializeSwitch(boolean isOn) {
-        this.getStyleClass().setAll("custom-switch");
+        this.getStyleClass().setAll("enum-selector");
         this.setMaxWidth(Region.USE_PREF_SIZE);
+        this.setMaxHeight(Region.USE_PREF_SIZE);
 
         leftButton = new Button("On");
-        leftButton.setStyle("-fx-border-radius: 5 0 0 5; -fx-background-radius: 5 0 0 5; -fx-border-width: 1 0 1 1;");
         leftButton.setOnAction(event -> toggle(true));
 
         rightButton = new Button("Off");
-        rightButton.setStyle("-fx-border-radius: 0 5 5 0; -fx-background-radius: 0 5 5 0; -fx-border-width: 1 1 1 0;");
         rightButton.setOnAction(event -> toggle(false));
 
         toggle(isOn);
@@ -30,9 +34,17 @@ public void initializeSwitch(boolean isOn) {
         this.getChildren().addAll(leftButton, rightButton);
     }
 
-    private void toggle(boolean isOn) {
-        leftButton.getStyleClass().setAll(isOn ? "switch-select-item-selected" : "switch-select-item");
-        rightButton.getStyleClass().setAll(isOn ? "switch-select-item" : "switch-select-item-selected");
-        this.isOn = isOn;
+    public void toggle(boolean isOn) {
+        leftButton.getStyleClass().clear();
+        leftButton.getStyleClass().add(isOn ? "enum-select-item-selected" : "enum-select-item");
+        leftButton.getStyleClass().add("enum-left-item");
+        rightButton.getStyleClass().clear();
+        rightButton.getStyleClass().add(isOn ? "enum-select-item" : "enum-select-item-selected");
+        rightButton.getStyleClass().add("enum-right-item");
+        this.isOn.setValue(isOn);
+    }
+
+    public BooleanProperty getIsOnProperty() {
+        return isOn;
     }
 }
diff --git a/src/main/resources/css/buttons.css b/src/main/resources/css/buttons.css
index 77b7580d..1f693949 100644
--- a/src/main/resources/css/buttons.css
+++ b/src/main/resources/css/buttons.css
@@ -33,7 +33,7 @@
 .standard-action-button {
     -fx-padding: 4px 8px;
     -fx-background-color: #eeeeee;
-    -fx-border-color: #d0d0d0;
+    -fx-border-color: #dcdcdc;
     -fx-background-radius: 4;
     -fx-border-radius: 4;
     -fx-font-weight: 600;
@@ -71,7 +71,6 @@
     -fx-max-height: 32px;
     -fx-padding: 4px 12px;
     -fx-background-color: #eeeeee;
-    -fx-border-color: #d0d0d0;
     -fx-background-radius: 4;
     -fx-border-radius: 4;
     -fx-font-weight: 500;
diff --git a/src/main/resources/css/common-elements.css b/src/main/resources/css/common-elements.css
index 4a90d563..4d9d59ed 100644
--- a/src/main/resources/css/common-elements.css
+++ b/src/main/resources/css/common-elements.css
@@ -105,21 +105,41 @@
     -fx-fill: #EAEAEA;
 }
 
-.select-item {
-    -fx-padding: 6px 12px;
-    -fx-background-color: #ffffff;
-    -fx-font-size: 16px;
+.enum-selector {
+    -fx-background-radius: 5;
+    -fx-border-radius: 5;
+    -fx-border-color: #cccccc;
+    -fx-border-width: 1;
+    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.1), 5, 0, 0, 2);
+}
+
+.enum-select-item {
+    -fx-padding: 6px 9px;
+    -fx-background-color: #efeeef;
+    -fx-font-size: 14px;
     -fx-font-weight: bold;
     -fx-alignment: center;
     -fx-cursor: hand;
 }
 
-.select-item-selected {
-    -fx-padding: 6px 12px;
+.enum-select-item-selected {
+    -fx-padding: 6px 9px;
     -fx-background-color: #006AEE;
     -fx-text-fill: white;
-    -fx-font-size: 16px;
+    -fx-font-size: 14px;
     -fx-font-weight: bold;
     -fx-alignment: center;
     -fx-cursor: hand;
+}
+
+.enum-left-item {
+    -fx-padding: 6px 9px 6px 12px;
+    -fx-border-radius: 5 0 0 5;
+    -fx-background-radius: 5 0 0 5;
+}
+
+.enum-right-item {
+    -fx-padding: 6px 12px 6px 9px;
+    -fx-border-radius: 0 5 5 0;
+    -fx-background-radius: 0 5 5 0;
 }
\ No newline at end of file
diff --git a/src/main/resources/css/settings.css b/src/main/resources/css/settings.css
index 807c08bd..d0b95746 100644
--- a/src/main/resources/css/settings.css
+++ b/src/main/resources/css/settings.css
@@ -18,7 +18,7 @@
 }
 
 .settings-section-label {
-    -fx-font-size: 18px;
+    -fx-font-size: 20px;
     -fx-font-weight: bold;
     -fx-padding: 10 0 10 0;
 }
@@ -28,28 +28,35 @@
     -fx-font-weight: bold;
 }
 
-.custom-switch {
-    -fx-background-radius: 5;
-    -fx-border-radius: 5;
-    -fx-border-color: #c0c0c0;
-    -fx-border-width: 1;
+/* Switch */
+/* Style for the toggle button */
+/* General styling for the toggle button */
+.jfx-toggle-button {
+    -fx-padding: 0;
 }
 
-.switch-select-item {
-    -fx-padding: 4px 8px;
-    -fx-background-color: #efeeef;
-    -fx-font-size: 14px;
-    -fx-font-weight: bold;
-    -fx-alignment: center;
-    -fx-cursor: hand;
+/* Specific styling for the sliding thumb and track in both states */
+.jfx-toggle-button .jfx-toggle-node {
+    -fx-background-radius: 15; /* Optional: Adjust as per your design needs */
+    -fx-background-color: transparent; /* Makes background transparent */
 }
 
-.switch-select-item-selected {
-    -fx-padding: 4px 8px;
-    -fx-background-color: #006AEE;
-    -fx-text-fill: white;
-    -fx-font-size: 14px;
-    -fx-font-weight: bold;
-    -fx-alignment: center;
-    -fx-cursor: hand;
+.jfx-toggle-button:selected .jfx-toggle-node {
+    -fx-background-color: #006AEE; /* Your specified blue color for "On" state */
+}
+
+.jfx-toggle-button .jfx-rippler {
+    -fx-rippler-fill: transparent; /* Remove the ripple effect on click */
+}
+
+.jfx-toggle-button:selected .jfx-rippler {
+    -fx-rippler-fill: #006AEE; /* Consistent with the toggle's "On" state color */
+}
+
+.jfx-toggle-button .jfx-toggle-color {
+    -fx-background-color: #cce4ff; /* Lighter blue for the track */
+}
+
+.jfx-toggle-button:selected .jfx-toggle-color {
+    -fx-background-color: #006AEE; /* ON state color for the track */
 }
diff --git a/src/main/resources/org/chainoptim/desktop/core/settings/NotificationSettingsView.fxml b/src/main/resources/org/chainoptim/desktop/core/settings/NotificationSettingsView.fxml
index 63486d26..d8066a6e 100644
--- a/src/main/resources/org/chainoptim/desktop/core/settings/NotificationSettingsView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/core/settings/NotificationSettingsView.fxml
@@ -1,13 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?import javafx.scene.layout.*?>
 
-<?import org.chainoptim.desktop.shared.common.uielements.settings.Switch?>
 <VBox xmlns="http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
       fx:controller="org.chainoptim.desktop.core.settings.controller.NotificationSettingsController"
       prefHeight="400.0" prefWidth="600.0">
 
-    <Switch fx:id="overallSwitch" />
     <VBox fx:id="contentVBox" styleClass="settings-tab-container">
         <!-- Overall Settings -->
 

From 88c8dae2d097807fe2720e0191da03e0e7f2f725 Mon Sep 17 00:00:00 2001
From: TudorOrban <130213626+TudorOrban@users.noreply.github.com>
Date: Thu, 25 Apr 2024 17:13:44 +0300
Subject: [PATCH 04/10] Fix User Settings commit and cancel issues

---
 .../NotificationSettingsController.java       | 121 +++++++++++-------
 .../controller/SettingsController.java        |   7 +-
 2 files changed, 77 insertions(+), 51 deletions(-)

diff --git a/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java b/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java
index 9ed7898a..5247d2be 100644
--- a/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java
+++ b/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java
@@ -32,7 +32,7 @@ public class NotificationSettingsController implements DataReceiver<UserSettings
     @Setter
     private SettingsListener settingsListener;
     private ChangeListener<Boolean> hasChangedListener;
-    private ChangeListener<Boolean> overallChangeListener;
+    private ChangeListener<Boolean> notificationOverallChangeListener;
 
     // Constants
     private static final List<String> notificationFeatures = List.of("Supplier Orders", "Client Orders", "Factory Inventory", "Warehouse Inventory");
@@ -40,8 +40,8 @@ public class NotificationSettingsController implements DataReceiver<UserSettings
     // FXML
     @FXML
     private VBox contentVBox;
-    private JFXToggleButton overallToggleButton = new JFXToggleButton();
-    private final Map<String, JFXToggleButton> featureToggleButtons = new HashMap<>();
+    private JFXToggleButton notificationOverallToggleButton = new JFXToggleButton();
+    private final Map<String, JFXToggleButton> notificationFeatureToggleButtons = new HashMap<>();
 
     @Override
     public void setData(UserSettings userSettings) {
@@ -53,8 +53,6 @@ private void initializeUI() {
         contentVBox.getChildren().clear();
         contentVBox.setSpacing(12);
 
-        cleanUpGlobalListeners();
-
         renderOverallHBox();
 
         for (String feature : notificationFeatures) {
@@ -64,32 +62,6 @@ private void initializeUI() {
         setUpGlobalListeners();
     }
 
-    private void cleanUpGlobalListeners() {
-        haveSettingsChanged.setValue(false);
-        if (hasChangedListener != null) {
-            haveSettingsChanged.removeListener(hasChangedListener);
-        }
-        if (overallChangeListener != null) {
-            overallToggleButton.selectedProperty().removeListener(overallChangeListener);
-        }
-    }
-
-    private void setUpGlobalListeners() {
-        // Any change listener
-        hasChangedListener = (ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
-            if (settingsListener != null) {
-                settingsListener.handleSettingsChanged(newValue);
-            }
-        };
-        haveSettingsChanged.addListener(hasChangedListener);
-
-        // Overall change listener
-        overallChangeListener = (ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
-            handleToggleOverallSwitch();
-        };
-        overallToggleButton.selectedProperty().addListener(overallChangeListener);
-    }
-
     private void renderOverallHBox() {
         HBox overallHBox = new HBox();
         Label overallLabel = new Label("Overall");
@@ -100,11 +72,11 @@ private void renderOverallHBox() {
         overallHBox.getChildren().add(region);
         HBox.setHgrow(region, Priority.ALWAYS);
 
-        overallToggleButton = new JFXToggleButton();
+        notificationOverallToggleButton = new JFXToggleButton();
         boolean overallSetting = aggregateNotificationSettings();
-        overallToggleButton.setSelected(overallSetting);
-        styleToggleButton(overallToggleButton);
-        overallHBox.getChildren().add(overallToggleButton);
+        notificationOverallToggleButton.setSelected(overallSetting);
+        styleToggleButton(notificationOverallToggleButton);
+        overallHBox.getChildren().add(notificationOverallToggleButton);
 
         contentVBox.getChildren().add(overallHBox);
     }
@@ -121,21 +93,21 @@ private void renderFeatureHBox(String feature) {
         HBox.setHgrow(region, Priority.ALWAYS);
 
         JFXToggleButton toggleButton = new JFXToggleButton();
-        boolean featureSetting = getFeatureSetting(feature);
+        boolean featureSetting = getNotificationFeatureSetting(feature);
         toggleButton.setSelected(featureSetting);
         styleToggleButton(toggleButton);
         toggleButton.selectedProperty().addListener((observable, oldValue, newValue) ->
-                handleToggleFeatureSwitch(featureToggleButtons.get(feature), feature, newValue));
-        featureToggleButtons.put(feature, toggleButton);
+                handleToggleFeatureSwitch(notificationFeatureToggleButtons.get(feature), feature, newValue));
+        notificationFeatureToggleButtons.put(feature, toggleButton);
         featureHBox.getChildren().add(toggleButton);
 
         contentVBox.getChildren().add(featureHBox);
     }
 
-    private void handleToggleOverallSwitch() {
-        boolean newState = overallToggleButton.isSelected();
-        for (String feature : notificationFeatures) {
-            JFXToggleButton featureToggleButton = featureToggleButtons.get(feature);
+    private void handleToggleOverallSwitch(List<String> features) {
+        boolean newState = notificationOverallToggleButton.isSelected();
+        for (String feature : features) {
+            JFXToggleButton featureToggleButton = notificationFeatureToggleButtons.get(feature);
             handleToggleFeatureSwitch(featureToggleButton, feature, newState);
         }
     }
@@ -143,16 +115,69 @@ private void handleToggleOverallSwitch() {
     private void handleToggleFeatureSwitch(JFXToggleButton featureToggleButton, String feature, Boolean isOn) {
         featureToggleButton.setSelected(isOn);
         haveSettingsChanged.setValue(true);
-        System.out.println("Settings changed: " + haveSettingsChanged);
-        setFeatureSetting(feature, isOn);
+        setNotificationFeatureSetting(feature, isOn);
+    }
+
+    private void setUpGlobalListeners() {
+        // Any change listener
+        hasChangedListener = (ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
+            if (settingsListener != null) {
+                settingsListener.handleSettingsChanged(newValue);
+            }
+        };
+        haveSettingsChanged.addListener(hasChangedListener);
+
+        // Overall change listener
+        notificationOverallChangeListener = (ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
+            handleToggleOverallSwitch(notificationFeatures);
+        };
+        notificationOverallToggleButton.selectedProperty().addListener(notificationOverallChangeListener);
+    }
+
+    public void commitChanges(UserSettings newSettings) {
+        // Temporarily remove listeners to prevent unpredictable behavior
+        removeListeners();
+
+        this.userSettings = newSettings;
+        updateUI();
+
+        reinstallListeners();
+        haveSettingsChanged.setValue(false);
     }
 
     public void cancelChanges(UserSettings originalUserSettings) {
-        overallToggleButton.selectedProperty().removeListener(overallChangeListener);
-        setData(originalUserSettings);
+        // Temporarily remove listeners to prevent unpredictable behavior
+        removeListeners();
+
+        this.userSettings = originalUserSettings;
+        updateUI();
+
+        reinstallListeners();
         haveSettingsChanged.setValue(false);
     }
 
+    private void reinstallListeners() {
+        notificationOverallToggleButton.selectedProperty().addListener(notificationOverallChangeListener);
+        haveSettingsChanged.addListener(hasChangedListener);
+    }
+
+    private void removeListeners() {
+        notificationOverallToggleButton.selectedProperty().removeListener(notificationOverallChangeListener);
+        haveSettingsChanged.removeListener(hasChangedListener);
+    }
+
+    private void updateUI() {
+        boolean overallSetting = aggregateNotificationSettings();
+        notificationOverallToggleButton.setSelected(overallSetting);
+
+        for (Map.Entry<String, JFXToggleButton> entry : notificationFeatureToggleButtons.entrySet()) {
+            String feature = entry.getKey();
+            JFXToggleButton toggleButton = entry.getValue();
+            boolean featureSetting = getNotificationFeatureSetting(feature);
+            toggleButton.setSelected(featureSetting);
+        }
+    }
+
     // Utils
     private boolean aggregateNotificationSettings() {
         return userSettings.getNotificationSettings().isClientOrdersOn() &&
@@ -161,7 +186,7 @@ private boolean aggregateNotificationSettings() {
                 userSettings.getNotificationSettings().isWarehouseInventoryOn();
     }
 
-    private boolean getFeatureSetting(String feature) {
+    private boolean getNotificationFeatureSetting(String feature) {
         return switch (feature) {
             case "Supplier Orders" -> userSettings.getNotificationSettings().isSupplierOrdersOn();
             case "Client Orders" -> userSettings.getNotificationSettings().isClientOrdersOn();
@@ -171,7 +196,7 @@ private boolean getFeatureSetting(String feature) {
         };
     }
 
-    private void setFeatureSetting(String feature, boolean isOn) {
+    private void setNotificationFeatureSetting(String feature, boolean isOn) {
         switch (feature) {
             case "Supplier Orders" -> userSettings.getNotificationSettings().setSupplierOrdersOn(isOn);
             case "Client Orders" -> userSettings.getNotificationSettings().setClientOrdersOn(isOn);
diff --git a/src/main/java/org/chainoptim/desktop/core/settings/controller/SettingsController.java b/src/main/java/org/chainoptim/desktop/core/settings/controller/SettingsController.java
index 98ea8089..7965c396 100644
--- a/src/main/java/org/chainoptim/desktop/core/settings/controller/SettingsController.java
+++ b/src/main/java/org/chainoptim/desktop/core/settings/controller/SettingsController.java
@@ -211,7 +211,7 @@ private Result<UserSettings> handleSaveResponse(Result<UserSettings> result) {
                 generalSettingsController.setData(userSettings);
             }
             if (notificationSettingsController != null) {
-                notificationSettingsController.setData(userSettings);
+                notificationSettingsController.commitChanges(userSettings);
             }
 
             fallbackManager.setLoading(false);
@@ -229,12 +229,13 @@ private Result<UserSettings> handleSaveException(Throwable ex) {
     @FXML
     private void handleCancel() {
         // Reselect based on original settings
+        userSettings = TenantSettingsContext.getCurrentUserSettings().deepCopy();
         if (generalSettingsController != null) {
-            generalSettingsController.cancelChanges(TenantSettingsContext.getCurrentUserSettings().deepCopy());
+            generalSettingsController.cancelChanges(userSettings);
         }
 
         if (notificationSettingsController != null) {
-            notificationSettingsController.cancelChanges(TenantSettingsContext.getCurrentUserSettings().deepCopy());
+            notificationSettingsController.cancelChanges(userSettings);
         }
         handleSettingsChanged(false);
     }

From a9333ade84e995c659c83ca82232deb1d855b58d Mon Sep 17 00:00:00 2001
From: TudorOrban <130213626+TudorOrban@users.noreply.github.com>
Date: Thu, 25 Apr 2024 17:42:38 +0300
Subject: [PATCH 05/10] Add Email Notification Settings

---
 .../NotificationSettingsController.java       | 168 +++++++++++++-----
 .../settings/model/NotificationSettings.java  |   8 +-
 .../desktop/shared/enums/Feature.java         |   7 +-
 src/main/resources/css/entity-page.css        |   2 +-
 .../chainoptim/desktop/core/main/AppView.fxml |   2 +-
 .../settings/NotificationSettingsView.fxml    |  14 +-
 6 files changed, 150 insertions(+), 51 deletions(-)

diff --git a/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java b/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java
index 5247d2be..fa1cd027 100644
--- a/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java
+++ b/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java
@@ -1,6 +1,7 @@
 package org.chainoptim.desktop.core.settings.controller;
 
 import org.chainoptim.desktop.core.settings.model.UserSettings;
+import org.chainoptim.desktop.shared.enums.Feature;
 import org.chainoptim.desktop.shared.util.DataReceiver;
 
 import com.jfoenix.controls.JFXToggleButton;
@@ -21,6 +22,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 public class NotificationSettingsController implements DataReceiver<UserSettings> {
 
@@ -33,15 +35,19 @@ public class NotificationSettingsController implements DataReceiver<UserSettings
     private SettingsListener settingsListener;
     private ChangeListener<Boolean> hasChangedListener;
     private ChangeListener<Boolean> notificationOverallChangeListener;
+    private ChangeListener<Boolean> emailOverallChangeListener;
 
     // Constants
-    private static final List<String> notificationFeatures = List.of("Supplier Orders", "Client Orders", "Factory Inventory", "Warehouse Inventory");
+    private static final List<Feature> notificationFeatures = List.of(Feature.SUPPLIER_ORDER, Feature.CLIENT_ORDER, Feature.FACTORY_INVENTORY, Feature.WAREHOUSE_INVENTORY);
+    private static final List<Feature> emailFeatures = List.of(Feature.SUPPLIER_ORDER, Feature.CLIENT_ORDER, Feature.FACTORY_INVENTORY, Feature.WAREHOUSE_INVENTORY);
 
     // FXML
     @FXML
     private VBox contentVBox;
     private JFXToggleButton notificationOverallToggleButton = new JFXToggleButton();
-    private final Map<String, JFXToggleButton> notificationFeatureToggleButtons = new HashMap<>();
+    private final Map<Feature, JFXToggleButton> notificationFeatureToggleButtons = new HashMap<>();
+    private JFXToggleButton emailOverallToggleButton = new JFXToggleButton();
+    private final Map<Feature, JFXToggleButton> emailFeatureToggleButtons = new HashMap<>();
 
     @Override
     public void setData(UserSettings userSettings) {
@@ -53,18 +59,28 @@ private void initializeUI() {
         contentVBox.getChildren().clear();
         contentVBox.setSpacing(12);
 
-        renderOverallHBox();
+        renderOverallHBox("Notifications");
 
-        for (String feature : notificationFeatures) {
-            renderFeatureHBox(feature);
+        for (Feature feature : notificationFeatures) {
+            renderFeatureHBox(feature, "Notifications");
+        }
+
+        Region region = new Region();
+        region.setMinHeight(8);
+        contentVBox.getChildren().add(region);
+
+        renderOverallHBox("Emails");
+
+        for (Feature feature : emailFeatures) {
+            renderFeatureHBox(feature, "Emails");
         }
 
         setUpGlobalListeners();
     }
 
-    private void renderOverallHBox() {
+    private void renderOverallHBox(String type) {
         HBox overallHBox = new HBox();
-        Label overallLabel = new Label("Overall");
+        Label overallLabel = new Label(type);
         overallLabel.getStyleClass().add("settings-section-label");
         overallHBox.getChildren().add(overallLabel);
 
@@ -72,19 +88,25 @@ private void renderOverallHBox() {
         overallHBox.getChildren().add(region);
         HBox.setHgrow(region, Priority.ALWAYS);
 
-        notificationOverallToggleButton = new JFXToggleButton();
-        boolean overallSetting = aggregateNotificationSettings();
-        notificationOverallToggleButton.setSelected(overallSetting);
-        styleToggleButton(notificationOverallToggleButton);
-        overallHBox.getChildren().add(notificationOverallToggleButton);
+        if (Objects.equals(type, "Notifications")) {
+            boolean overallSetting = aggregateNotificationSettings();
+            notificationOverallToggleButton.setSelected(overallSetting);
+            styleToggleButton(notificationOverallToggleButton);
+            overallHBox.getChildren().add(notificationOverallToggleButton);
+        } else if ("Emails".equals(type)) {
+            boolean overallSetting = aggregateEmailSettings();
+            emailOverallToggleButton.setSelected(overallSetting);
+            styleToggleButton(emailOverallToggleButton);
+            overallHBox.getChildren().add(emailOverallToggleButton);
+        }
 
         contentVBox.getChildren().add(overallHBox);
     }
 
-    private void renderFeatureHBox(String feature) {
+    private void renderFeatureHBox(Feature feature, String type) {
         HBox featureHBox = new HBox();
         featureHBox.setAlignment(Pos.CENTER_LEFT);
-        Label featureLabel = new Label(feature);
+        Label featureLabel = new Label(feature.toString());
         featureLabel.getStyleClass().add("settings-label");
         featureHBox.getChildren().add(featureLabel);
 
@@ -93,29 +115,49 @@ private void renderFeatureHBox(String feature) {
         HBox.setHgrow(region, Priority.ALWAYS);
 
         JFXToggleButton toggleButton = new JFXToggleButton();
-        boolean featureSetting = getNotificationFeatureSetting(feature);
-        toggleButton.setSelected(featureSetting);
         styleToggleButton(toggleButton);
-        toggleButton.selectedProperty().addListener((observable, oldValue, newValue) ->
-                handleToggleFeatureSwitch(notificationFeatureToggleButtons.get(feature), feature, newValue));
-        notificationFeatureToggleButtons.put(feature, toggleButton);
+        if (Objects.equals(type, "Notifications")) {
+            boolean featureSetting = getNotificationFeatureSetting(feature);
+            toggleButton.setSelected(featureSetting);
+            toggleButton.selectedProperty().addListener((observable, oldValue, newValue) ->
+                    handleToggleFeatureSwitch(notificationFeatureToggleButtons.get(feature), feature, newValue, type));
+            notificationFeatureToggleButtons.put(feature, toggleButton);
+        } else if ("Emails".equals(type)) {
+            boolean featureSetting = getEmailFeatureSetting(feature);
+            toggleButton.setSelected(featureSetting);
+            toggleButton.selectedProperty().addListener((observable, oldValue, newValue) ->
+                    handleToggleFeatureSwitch(emailFeatureToggleButtons.get(feature), feature, newValue, type));
+            emailFeatureToggleButtons.put(feature, toggleButton);
+        }
         featureHBox.getChildren().add(toggleButton);
 
         contentVBox.getChildren().add(featureHBox);
     }
 
-    private void handleToggleOverallSwitch(List<String> features) {
-        boolean newState = notificationOverallToggleButton.isSelected();
-        for (String feature : features) {
-            JFXToggleButton featureToggleButton = notificationFeatureToggleButtons.get(feature);
-            handleToggleFeatureSwitch(featureToggleButton, feature, newState);
+    private void handleToggleOverallSwitch(String type) {
+        if (Objects.equals(type, "Notifications")) {
+            boolean newState = notificationOverallToggleButton.isSelected();
+            for (Feature feature : notificationFeatures) {
+                JFXToggleButton featureToggleButton = notificationFeatureToggleButtons.get(feature);
+                handleToggleFeatureSwitch(featureToggleButton, feature, newState, type);
+            }
+        } else if ("Emails".equals(type)) {
+            boolean newState = emailOverallToggleButton.isSelected();
+            for (Feature feature : emailFeatures) {
+                JFXToggleButton featureToggleButton = emailFeatureToggleButtons.get(feature);
+                handleToggleFeatureSwitch(featureToggleButton, feature, newState, type);
+            }
         }
     }
 
-    private void handleToggleFeatureSwitch(JFXToggleButton featureToggleButton, String feature, Boolean isOn) {
+    private void handleToggleFeatureSwitch(JFXToggleButton featureToggleButton, Feature feature, Boolean isOn, String type) {
         featureToggleButton.setSelected(isOn);
         haveSettingsChanged.setValue(true);
-        setNotificationFeatureSetting(feature, isOn);
+        if (Objects.equals(type, "Notifications")) {
+            setNotificationFeatureSetting(feature, isOn);
+        } else if ("Emails".equals(type)) {
+            setEmailFeatureSetting(feature, isOn);
+        }
     }
 
     private void setUpGlobalListeners() {
@@ -129,9 +171,14 @@ private void setUpGlobalListeners() {
 
         // Overall change listener
         notificationOverallChangeListener = (ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
-            handleToggleOverallSwitch(notificationFeatures);
+            handleToggleOverallSwitch("Notifications");
         };
         notificationOverallToggleButton.selectedProperty().addListener(notificationOverallChangeListener);
+
+        emailOverallChangeListener = (ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
+            handleToggleOverallSwitch("Emails");
+        };
+        emailOverallToggleButton.selectedProperty().addListener(emailOverallChangeListener);
     }
 
     public void commitChanges(UserSettings newSettings) {
@@ -158,11 +205,13 @@ public void cancelChanges(UserSettings originalUserSettings) {
 
     private void reinstallListeners() {
         notificationOverallToggleButton.selectedProperty().addListener(notificationOverallChangeListener);
+        emailOverallToggleButton.selectedProperty().addListener(emailOverallChangeListener);
         haveSettingsChanged.addListener(hasChangedListener);
     }
 
     private void removeListeners() {
         notificationOverallToggleButton.selectedProperty().removeListener(notificationOverallChangeListener);
+        emailOverallToggleButton.selectedProperty().removeListener(emailOverallChangeListener);
         haveSettingsChanged.removeListener(hasChangedListener);
     }
 
@@ -170,38 +219,75 @@ private void updateUI() {
         boolean overallSetting = aggregateNotificationSettings();
         notificationOverallToggleButton.setSelected(overallSetting);
 
-        for (Map.Entry<String, JFXToggleButton> entry : notificationFeatureToggleButtons.entrySet()) {
-            String feature = entry.getKey();
+        for (Map.Entry<Feature, JFXToggleButton> entry : notificationFeatureToggleButtons.entrySet()) {
+            Feature feature = entry.getKey();
             JFXToggleButton toggleButton = entry.getValue();
             boolean featureSetting = getNotificationFeatureSetting(feature);
             toggleButton.setSelected(featureSetting);
         }
+
+        boolean emailOverallSetting = aggregateEmailSettings();
+        emailOverallToggleButton.setSelected(emailOverallSetting);
+
+        for (Map.Entry<Feature, JFXToggleButton> entry : emailFeatureToggleButtons.entrySet()) {
+            Feature feature = entry.getKey();
+            JFXToggleButton toggleButton = entry.getValue();
+            boolean featureSetting = getEmailFeatureSetting(feature);
+            toggleButton.setSelected(featureSetting);
+        }
     }
 
     // Utils
     private boolean aggregateNotificationSettings() {
-        return userSettings.getNotificationSettings().isClientOrdersOn() &&
-                userSettings.getNotificationSettings().isSupplierOrdersOn() &&
+        return userSettings.getNotificationSettings().isSupplierOrdersOn() &&
+                userSettings.getNotificationSettings().isClientOrdersOn() &&
                 userSettings.getNotificationSettings().isFactoryInventoryOn() &&
                 userSettings.getNotificationSettings().isWarehouseInventoryOn();
     }
 
-    private boolean getNotificationFeatureSetting(String feature) {
+    private boolean aggregateEmailSettings() {
+        return userSettings.getNotificationSettings().isEmailSupplierOrdersOn() &&
+                userSettings.getNotificationSettings().isEmailClientOrdersOn() &&
+                userSettings.getNotificationSettings().isEmailFactoryInventoryOn() &&
+                userSettings.getNotificationSettings().isEmailWarehouseInventoryOn();
+    }
+
+    private boolean getNotificationFeatureSetting(Feature feature) {
         return switch (feature) {
-            case "Supplier Orders" -> userSettings.getNotificationSettings().isSupplierOrdersOn();
-            case "Client Orders" -> userSettings.getNotificationSettings().isClientOrdersOn();
-            case "Factory Inventory" -> userSettings.getNotificationSettings().isFactoryInventoryOn();
-            case "Warehouse Inventory" -> userSettings.getNotificationSettings().isWarehouseInventoryOn();
+            case SUPPLIER_ORDER -> userSettings.getNotificationSettings().isSupplierOrdersOn();
+            case CLIENT_ORDER -> userSettings.getNotificationSettings().isClientOrdersOn();
+            case FACTORY_INVENTORY -> userSettings.getNotificationSettings().isFactoryInventoryOn();
+            case WAREHOUSE_INVENTORY -> userSettings.getNotificationSettings().isWarehouseInventoryOn();
             default -> false;
         };
     }
 
-    private void setNotificationFeatureSetting(String feature, boolean isOn) {
+    private boolean getEmailFeatureSetting(Feature feature) {
+        return switch (feature) {
+            case SUPPLIER_ORDER -> userSettings.getNotificationSettings().isEmailSupplierOrdersOn();
+            case CLIENT_ORDER -> userSettings.getNotificationSettings().isEmailClientOrdersOn();
+            case FACTORY_INVENTORY -> userSettings.getNotificationSettings().isEmailFactoryInventoryOn();
+            case WAREHOUSE_INVENTORY -> userSettings.getNotificationSettings().isEmailWarehouseInventoryOn();
+            default -> false;
+        };
+    }
+
+    private void setNotificationFeatureSetting(Feature feature, boolean isOn) {
+        switch (feature) {
+            case SUPPLIER_ORDER -> userSettings.getNotificationSettings().setSupplierOrdersOn(isOn);
+            case CLIENT_ORDER -> userSettings.getNotificationSettings().setClientOrdersOn(isOn);
+            case FACTORY_INVENTORY -> userSettings.getNotificationSettings().setFactoryInventoryOn(isOn);
+            case WAREHOUSE_INVENTORY -> userSettings.getNotificationSettings().setWarehouseInventoryOn(isOn);
+            default -> {}
+        }
+    }
+
+    private void setEmailFeatureSetting(Feature feature, boolean isOn) {
         switch (feature) {
-            case "Supplier Orders" -> userSettings.getNotificationSettings().setSupplierOrdersOn(isOn);
-            case "Client Orders" -> userSettings.getNotificationSettings().setClientOrdersOn(isOn);
-            case "Factory Inventory" -> userSettings.getNotificationSettings().setFactoryInventoryOn(isOn);
-            case "Warehouse Inventory" -> userSettings.getNotificationSettings().setWarehouseInventoryOn(isOn);
+            case SUPPLIER_ORDER -> userSettings.getNotificationSettings().setEmailSupplierOrdersOn(isOn);
+            case CLIENT_ORDER -> userSettings.getNotificationSettings().setEmailClientOrdersOn(isOn);
+            case FACTORY_INVENTORY -> userSettings.getNotificationSettings().setEmailFactoryInventoryOn(isOn);
+            case WAREHOUSE_INVENTORY -> userSettings.getNotificationSettings().setEmailWarehouseInventoryOn(isOn);
             default -> {}
         }
     }
diff --git a/src/main/java/org/chainoptim/desktop/core/settings/model/NotificationSettings.java b/src/main/java/org/chainoptim/desktop/core/settings/model/NotificationSettings.java
index bc1c0428..2c025838 100644
--- a/src/main/java/org/chainoptim/desktop/core/settings/model/NotificationSettings.java
+++ b/src/main/java/org/chainoptim/desktop/core/settings/model/NotificationSettings.java
@@ -13,8 +13,14 @@ public class NotificationSettings {
     private boolean clientOrdersOn;
     private boolean factoryInventoryOn;
     private boolean warehouseInventoryOn;
+    private boolean emailSupplierOrdersOn;
+    private boolean emailClientOrdersOn;
+    private boolean emailFactoryInventoryOn;
+    private boolean emailWarehouseInventoryOn;
 
     public NotificationSettings deepCopy() {
-        return new NotificationSettings(supplierOrdersOn, clientOrdersOn, factoryInventoryOn, warehouseInventoryOn);
+        return new NotificationSettings(
+                supplierOrdersOn, clientOrdersOn, factoryInventoryOn, warehouseInventoryOn,
+                emailSupplierOrdersOn, emailClientOrdersOn, emailFactoryInventoryOn, emailWarehouseInventoryOn);
     }
 }
diff --git a/src/main/java/org/chainoptim/desktop/shared/enums/Feature.java b/src/main/java/org/chainoptim/desktop/shared/enums/Feature.java
index 7e7762e7..18bba397 100644
--- a/src/main/java/org/chainoptim/desktop/shared/enums/Feature.java
+++ b/src/main/java/org/chainoptim/desktop/shared/enums/Feature.java
@@ -31,5 +31,10 @@ public enum Feature {
     CLIENT,
     CLIENT_ORDER,
     CLIENT_SHIPMENT,
-    CLIENT_EVALUATION
+    CLIENT_EVALUATION;
+
+    @Override
+    public String toString() {
+        return (name().charAt(0) + name().substring(1).toLowerCase()).replace("_", " ");
+    }
 }
diff --git a/src/main/resources/css/entity-page.css b/src/main/resources/css/entity-page.css
index b42b3bf8..e7bab219 100644
--- a/src/main/resources/css/entity-page.css
+++ b/src/main/resources/css/entity-page.css
@@ -1,7 +1,7 @@
 .entity-header-container {
     -fx-padding: 20;
     -fx-background-color: #f2f2f2;
-    -fx-border-color: #c0c0c0 #ffffff #c0c0c0 #ffffff;
+    -fx-border-color: #ffffff #ffffff #c0c0c0 #ffffff;
     -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.1), 4, 0, 0, 2);
 }
 
diff --git a/src/main/resources/org/chainoptim/desktop/core/main/AppView.fxml b/src/main/resources/org/chainoptim/desktop/core/main/AppView.fxml
index bf159ffa..ec0e3852 100644
--- a/src/main/resources/org/chainoptim/desktop/core/main/AppView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/core/main/AppView.fxml
@@ -4,7 +4,7 @@
 <BorderPane xmlns = "http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
       fx:controller="org.chainoptim.desktop.core.main.controller.AppController"
-        style="-fx-background-color: #ffffff;">
+      style="-fx-background-color: #ffffff; -fx-border-width: 1px 0px; -fx-border-color: #c0c0c0;">
 
     <!-- Sidebar -->
     <left>
diff --git a/src/main/resources/org/chainoptim/desktop/core/settings/NotificationSettingsView.fxml b/src/main/resources/org/chainoptim/desktop/core/settings/NotificationSettingsView.fxml
index d8066a6e..be0910ca 100644
--- a/src/main/resources/org/chainoptim/desktop/core/settings/NotificationSettingsView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/core/settings/NotificationSettingsView.fxml
@@ -1,14 +1,16 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?import javafx.scene.layout.*?>
 
+<?import javafx.scene.control.ScrollPane?>
 <VBox xmlns="http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
-      fx:controller="org.chainoptim.desktop.core.settings.controller.NotificationSettingsController"
-      prefHeight="400.0" prefWidth="600.0">
+      fx:controller="org.chainoptim.desktop.core.settings.controller.NotificationSettingsController">
 
-    <VBox fx:id="contentVBox" styleClass="settings-tab-container">
-        <!-- Overall Settings -->
+    <ScrollPane fitToHeight="true" fitToWidth="true">
+        <VBox fx:id="contentVBox" styleClass="settings-tab-container">
+            <!-- Overall Settings -->
 
-        <!-- Feature-specific Settings -->
-    </VBox>
+            <!-- Feature-specific Settings -->
+        </VBox>
+    </ScrollPane>
 </VBox>

From 5272ab3932cd8f7809519b34b7ea01cf373ec385 Mon Sep 17 00:00:00 2001
From: TudorOrban <130213626+TudorOrban@users.noreply.github.com>
Date: Thu, 25 Apr 2024 18:15:48 +0300
Subject: [PATCH 06/10] Make minor adjustments to User Settings

---
 .../NotificationSettingsController.java       | 59 +++++++++++--------
 .../controller/SettingsController.java        |  9 ++-
 .../client/service/ClientServiceImpl.java     |  1 -
 .../uielements/settings/EnumSelector.java     | 13 ++--
 src/main/resources/css/list-header.css        | 20 +------
 .../desktop/core/main/ListHeaderView.fxml     |  2 +-
 6 files changed, 48 insertions(+), 56 deletions(-)

diff --git a/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java b/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java
index fa1cd027..0f51ed6f 100644
--- a/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java
+++ b/src/main/java/org/chainoptim/desktop/core/settings/controller/NotificationSettingsController.java
@@ -1,5 +1,6 @@
 package org.chainoptim.desktop.core.settings.controller;
 
+import org.chainoptim.desktop.core.context.TenantContext;
 import org.chainoptim.desktop.core.settings.model.UserSettings;
 import org.chainoptim.desktop.shared.enums.Feature;
 import org.chainoptim.desktop.shared.util.DataReceiver;
@@ -19,15 +20,14 @@
 import javafx.scene.paint.Color;
 import lombok.Setter;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
+import java.util.*;
 
 public class NotificationSettingsController implements DataReceiver<UserSettings> {
 
     // State
     private UserSettings userSettings;
+    private boolean areNotificationsWithinPlan;
+    private boolean areEmailsWithinPlan;
     private final BooleanProperty haveSettingsChanged = new SimpleBooleanProperty(false);
 
     // Listeners
@@ -40,18 +40,23 @@ public class NotificationSettingsController implements DataReceiver<UserSettings
     // Constants
     private static final List<Feature> notificationFeatures = List.of(Feature.SUPPLIER_ORDER, Feature.CLIENT_ORDER, Feature.FACTORY_INVENTORY, Feature.WAREHOUSE_INVENTORY);
     private static final List<Feature> emailFeatures = List.of(Feature.SUPPLIER_ORDER, Feature.CLIENT_ORDER, Feature.FACTORY_INVENTORY, Feature.WAREHOUSE_INVENTORY);
-
+    private static final String NOTIFICATIONS = "Notifications";
+    private static final String EMAILS = "Emails";
+    
     // FXML
     @FXML
     private VBox contentVBox;
-    private JFXToggleButton notificationOverallToggleButton = new JFXToggleButton();
-    private final Map<Feature, JFXToggleButton> notificationFeatureToggleButtons = new HashMap<>();
-    private JFXToggleButton emailOverallToggleButton = new JFXToggleButton();
-    private final Map<Feature, JFXToggleButton> emailFeatureToggleButtons = new HashMap<>();
+    private final JFXToggleButton notificationOverallToggleButton = new JFXToggleButton();
+    private final Map<Feature, JFXToggleButton> notificationFeatureToggleButtons = new EnumMap<>(Feature.class);
+    private final JFXToggleButton emailOverallToggleButton = new JFXToggleButton();
+    private final Map<Feature, JFXToggleButton> emailFeatureToggleButtons = new EnumMap<>(Feature.class);
 
     @Override
     public void setData(UserSettings userSettings) {
         this.userSettings = userSettings;
+        this.areNotificationsWithinPlan = TenantContext.getCurrentUser().getOrganization().getSubscriptionPlan().isCustomNotificationsOn();
+        this.areEmailsWithinPlan = TenantContext.getCurrentUser().getOrganization().getSubscriptionPlan().isEmailNotificationsOn();
+
         initializeUI();
     }
 
@@ -59,20 +64,20 @@ private void initializeUI() {
         contentVBox.getChildren().clear();
         contentVBox.setSpacing(12);
 
-        renderOverallHBox("Notifications");
+        renderOverallHBox(NOTIFICATIONS);
 
         for (Feature feature : notificationFeatures) {
-            renderFeatureHBox(feature, "Notifications");
+            renderFeatureHBox(feature, NOTIFICATIONS);
         }
 
         Region region = new Region();
         region.setMinHeight(8);
         contentVBox.getChildren().add(region);
 
-        renderOverallHBox("Emails");
+        renderOverallHBox(EMAILS);
 
         for (Feature feature : emailFeatures) {
-            renderFeatureHBox(feature, "Emails");
+            renderFeatureHBox(feature, EMAILS);
         }
 
         setUpGlobalListeners();
@@ -88,14 +93,16 @@ private void renderOverallHBox(String type) {
         overallHBox.getChildren().add(region);
         HBox.setHgrow(region, Priority.ALWAYS);
 
-        if (Objects.equals(type, "Notifications")) {
+        if (Objects.equals(type, NOTIFICATIONS)) {
             boolean overallSetting = aggregateNotificationSettings();
             notificationOverallToggleButton.setSelected(overallSetting);
+            notificationOverallToggleButton.setDisable(!areNotificationsWithinPlan);
             styleToggleButton(notificationOverallToggleButton);
             overallHBox.getChildren().add(notificationOverallToggleButton);
-        } else if ("Emails".equals(type)) {
+        } else if (EMAILS.equals(type)) {
             boolean overallSetting = aggregateEmailSettings();
             emailOverallToggleButton.setSelected(overallSetting);
+            emailOverallToggleButton.setDisable(!areEmailsWithinPlan);
             styleToggleButton(emailOverallToggleButton);
             overallHBox.getChildren().add(emailOverallToggleButton);
         }
@@ -116,14 +123,16 @@ private void renderFeatureHBox(Feature feature, String type) {
 
         JFXToggleButton toggleButton = new JFXToggleButton();
         styleToggleButton(toggleButton);
-        if (Objects.equals(type, "Notifications")) {
+        if (Objects.equals(type, NOTIFICATIONS)) {
             boolean featureSetting = getNotificationFeatureSetting(feature);
             toggleButton.setSelected(featureSetting);
+            toggleButton.setDisable(!areNotificationsWithinPlan);
             toggleButton.selectedProperty().addListener((observable, oldValue, newValue) ->
                     handleToggleFeatureSwitch(notificationFeatureToggleButtons.get(feature), feature, newValue, type));
             notificationFeatureToggleButtons.put(feature, toggleButton);
-        } else if ("Emails".equals(type)) {
+        } else if (EMAILS.equals(type)) {
             boolean featureSetting = getEmailFeatureSetting(feature);
+            toggleButton.setDisable(!areEmailsWithinPlan);
             toggleButton.setSelected(featureSetting);
             toggleButton.selectedProperty().addListener((observable, oldValue, newValue) ->
                     handleToggleFeatureSwitch(emailFeatureToggleButtons.get(feature), feature, newValue, type));
@@ -135,13 +144,13 @@ private void renderFeatureHBox(Feature feature, String type) {
     }
 
     private void handleToggleOverallSwitch(String type) {
-        if (Objects.equals(type, "Notifications")) {
+        if (Objects.equals(type, NOTIFICATIONS)) {
             boolean newState = notificationOverallToggleButton.isSelected();
             for (Feature feature : notificationFeatures) {
                 JFXToggleButton featureToggleButton = notificationFeatureToggleButtons.get(feature);
                 handleToggleFeatureSwitch(featureToggleButton, feature, newState, type);
             }
-        } else if ("Emails".equals(type)) {
+        } else if (EMAILS.equals(type)) {
             boolean newState = emailOverallToggleButton.isSelected();
             for (Feature feature : emailFeatures) {
                 JFXToggleButton featureToggleButton = emailFeatureToggleButtons.get(feature);
@@ -153,9 +162,9 @@ private void handleToggleOverallSwitch(String type) {
     private void handleToggleFeatureSwitch(JFXToggleButton featureToggleButton, Feature feature, Boolean isOn, String type) {
         featureToggleButton.setSelected(isOn);
         haveSettingsChanged.setValue(true);
-        if (Objects.equals(type, "Notifications")) {
+        if (Objects.equals(type, NOTIFICATIONS)) {
             setNotificationFeatureSetting(feature, isOn);
-        } else if ("Emails".equals(type)) {
+        } else if (EMAILS.equals(type)) {
             setEmailFeatureSetting(feature, isOn);
         }
     }
@@ -171,12 +180,12 @@ private void setUpGlobalListeners() {
 
         // Overall change listener
         notificationOverallChangeListener = (ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
-            handleToggleOverallSwitch("Notifications");
+            handleToggleOverallSwitch(NOTIFICATIONS);
         };
         notificationOverallToggleButton.selectedProperty().addListener(notificationOverallChangeListener);
 
         emailOverallChangeListener = (ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
-            handleToggleOverallSwitch("Emails");
+            handleToggleOverallSwitch(EMAILS);
         };
         emailOverallToggleButton.selectedProperty().addListener(emailOverallChangeListener);
     }
@@ -278,7 +287,7 @@ private void setNotificationFeatureSetting(Feature feature, boolean isOn) {
             case CLIENT_ORDER -> userSettings.getNotificationSettings().setClientOrdersOn(isOn);
             case FACTORY_INVENTORY -> userSettings.getNotificationSettings().setFactoryInventoryOn(isOn);
             case WAREHOUSE_INVENTORY -> userSettings.getNotificationSettings().setWarehouseInventoryOn(isOn);
-            default -> {}
+            default -> throw new IllegalStateException("Unexpected value: " + feature);
         }
     }
 
@@ -288,7 +297,7 @@ private void setEmailFeatureSetting(Feature feature, boolean isOn) {
             case CLIENT_ORDER -> userSettings.getNotificationSettings().setEmailClientOrdersOn(isOn);
             case FACTORY_INVENTORY -> userSettings.getNotificationSettings().setEmailFactoryInventoryOn(isOn);
             case WAREHOUSE_INVENTORY -> userSettings.getNotificationSettings().setEmailWarehouseInventoryOn(isOn);
-            default -> {}
+            default -> throw new IllegalStateException("Unexpected value: " + feature);
         }
     }
 
diff --git a/src/main/java/org/chainoptim/desktop/core/settings/controller/SettingsController.java b/src/main/java/org/chainoptim/desktop/core/settings/controller/SettingsController.java
index 7965c396..7cd7a73d 100644
--- a/src/main/java/org/chainoptim/desktop/core/settings/controller/SettingsController.java
+++ b/src/main/java/org/chainoptim/desktop/core/settings/controller/SettingsController.java
@@ -7,8 +7,11 @@
 import org.chainoptim.desktop.core.settings.model.UserSettings;
 import org.chainoptim.desktop.core.settings.service.UserSettingsService;
 import org.chainoptim.desktop.core.user.model.User;
+import org.chainoptim.desktop.shared.enums.OperationOutcome;
 import org.chainoptim.desktop.shared.fallback.FallbackManager;
 import org.chainoptim.desktop.shared.httphandling.Result;
+import org.chainoptim.desktop.shared.toast.controller.ToastManager;
+import org.chainoptim.desktop.shared.toast.model.ToastInfo;
 import org.chainoptim.desktop.shared.util.DataReceiver;
 import org.chainoptim.desktop.shared.util.resourceloader.CommonViewsLoader;
 import org.chainoptim.desktop.shared.util.resourceloader.FXMLLoaderService;
@@ -36,6 +39,7 @@ public class SettingsController implements Initializable, SettingsListener {
     private final UserSettingsService userSettingsService;
     private final CommonViewsLoader commonViewsLoader;
     private final ControllerFactory controllerFactory;
+    private final ToastManager toastManager;
 
     // Controllers
     private GeneralSettingsController generalSettingsController;
@@ -69,10 +73,12 @@ public class SettingsController implements Initializable, SettingsListener {
     public SettingsController(UserSettingsService userSettingsService,
                               CommonViewsLoader commonViewsLoader,
                               ControllerFactory controllerFactory,
+                              ToastManager toastManager,
                               FallbackManager fallbackManager) {
         this.userSettingsService = userSettingsService;
         this.commonViewsLoader = commonViewsLoader;
         this.controllerFactory = controllerFactory;
+        this.toastManager = toastManager;
         this.fallbackManager = fallbackManager;
     }
 
@@ -201,7 +207,7 @@ public void handleSave() {
     private Result<UserSettings> handleSaveResponse(Result<UserSettings> result) {
         Platform.runLater(() -> {
             if (result.getError() != null) {
-                fallbackManager.setErrorMessage("Failed to save user settings.");
+                toastManager.addToast(new ToastInfo("An error occurred", "Failed to save user settings.", OperationOutcome.ERROR));
                 return;
             }
             userSettings = result.getData();
@@ -214,6 +220,7 @@ private Result<UserSettings> handleSaveResponse(Result<UserSettings> result) {
                 notificationSettingsController.commitChanges(userSettings);
             }
 
+            toastManager.addToast(new ToastInfo("Success", "User settings saved successfully.", OperationOutcome.SUCCESS));
             fallbackManager.setLoading(false);
             handleSettingsChanged(false);
         });
diff --git a/src/main/java/org/chainoptim/desktop/features/client/service/ClientServiceImpl.java b/src/main/java/org/chainoptim/desktop/features/client/service/ClientServiceImpl.java
index 8b8c4862..66c13eac 100644
--- a/src/main/java/org/chainoptim/desktop/features/client/service/ClientServiceImpl.java
+++ b/src/main/java/org/chainoptim/desktop/features/client/service/ClientServiceImpl.java
@@ -53,7 +53,6 @@ public CompletableFuture<Result<PaginatedResults<Client>>> getClientsByOrganizat
         String cacheKey = CacheKeyBuilder.buildAdvancedSearchKey("clients", "organization", organizationId.toString(), searchParams);
         String routeAddress = rootAddress + cacheKey;
 
-        System.out.println("Token: " + tokenManager.getToken());
         HttpRequest request = requestBuilder.buildReadRequest(routeAddress, tokenManager.getToken());
 
         if (cachingService.isCached(cacheKey) && !cachingService.isStale(cacheKey)) {
diff --git a/src/main/java/org/chainoptim/desktop/shared/common/uielements/settings/EnumSelector.java b/src/main/java/org/chainoptim/desktop/shared/common/uielements/settings/EnumSelector.java
index 671b0f7d..b7b10e80 100644
--- a/src/main/java/org/chainoptim/desktop/shared/common/uielements/settings/EnumSelector.java
+++ b/src/main/java/org/chainoptim/desktop/shared/common/uielements/settings/EnumSelector.java
@@ -7,15 +7,13 @@
 import javafx.scene.layout.HBox;
 import javafx.scene.layout.Region;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
+import java.util.stream.Collectors;
 
 public class EnumSelector<T extends Enum<T>> extends HBox {
 
     private final ObjectProperty<T> selectedValueProperty = new SimpleObjectProperty<>();;
-    private final Map<T, Button> buttons = new HashMap<>();
+    private final Map<T, Button> buttons = new LinkedHashMap<>();
 
     public EnumSelector() {
         super();
@@ -30,7 +28,6 @@ public void initializeSelector(Class<T> enumType, T selectedValue) {
         this.selectedValueProperty.setValue(selectedValue);
 
         int index = 0;
-
         for (T value : enumType.getEnumConstants()) {
             // Create a button for each enum value
             Button button = new Button(value.toString());
@@ -60,14 +57,12 @@ private void styleButton(T selectedValue, T value, Button button, int index, Cla
 
     public void selectValue(T value, Class<T> enumType) {
         selectedValueProperty.setValue(value);
+
         int index = 0;
         for (Map.Entry<T, Button> entry : buttons.entrySet()) {
             styleButton(value, entry.getKey(), entry.getValue(), index, enumType);
             index++;
         }
-        buttons.forEach((k, v) -> v.getStyleClass().add(
-                k.equals(selectedValueProperty.getValue()) ? "enum-select-item-selected" : "enum-select-item"
-        ));
     }
 
     public ObjectProperty<T> getValueProperty() {
diff --git a/src/main/resources/css/list-header.css b/src/main/resources/css/list-header.css
index 621a6a87..331c669f 100644
--- a/src/main/resources/css/list-header.css
+++ b/src/main/resources/css/list-header.css
@@ -2,6 +2,7 @@
     -fx-background-color: #ffffff;
     -fx-spacing: 20px;
     -fx-border-color: #c0c0c0;
+    -fx-border-width: 0px 0px 1px 0px;
     -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.15), 5, 0, 0, 2);
 }
 
@@ -40,25 +41,6 @@
     -fx-background-color: #000000;
 }
 
-.header .combo-box {
-    -fx-pref-height: 32px;
-    -fx-background-color: #ffffff;
-    -fx-font-weight: bold;
-    -fx-border-color: #c0c0c0;
-    -fx-border-width: 1px;
-    -fx-border-radius: 4px;
-    -fx-cursor: hand;
-}
-
-.header .combo-box .arrow-button {
-    -fx-background-color: transparent;
-    -fx-background-insets: 0;
-}
-
-.header .combo-box .arrow {
-    -fx-background-color: #c0c0c0;
-}
-
 .ordering-button {
     -fx-pref-height: 32px;
     -fx-background-color: transparent;
diff --git a/src/main/resources/org/chainoptim/desktop/core/main/ListHeaderView.fxml b/src/main/resources/org/chainoptim/desktop/core/main/ListHeaderView.fxml
index fd24340d..929791ef 100644
--- a/src/main/resources/org/chainoptim/desktop/core/main/ListHeaderView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/core/main/ListHeaderView.fxml
@@ -20,7 +20,7 @@
                 <Button fx:id="searchButton" onAction="#handleSearch" styleClass="search-button"/>
             </StackPane>
             <Region HBox.hgrow="ALWAYS"/>
-            <ComboBox fx:id="sortOptions" promptText="Sort By..." onAction="#handleSortOption"/>
+            <ComboBox fx:id="sortOptions" promptText="Sort By..." onAction="#handleSortOption" styleClass="custom-combo-box"/>
             <Button fx:id="orderingButton" onAction="#handleOrdering" styleClass="ordering-button"/>
             <Region HBox.hgrow="ALWAYS"/>
             <Button fx:id="refreshButton" styleClass="refresh-button"/>

From 00fca6b466b4c6e704d31cebc5c52dbca56c1107 Mon Sep 17 00:00:00 2001
From: TudorOrban <130213626+TudorOrban@users.noreply.github.com>
Date: Thu, 25 Apr 2024 21:34:40 +0300
Subject: [PATCH 07/10] Start refactoring forms

---
 .../controller/CreateProductController.java   |  86 ++++++++-----
 .../common/uielements/forms/FormField.java    | 117 ++++++++++++++++++
 .../uielements/forms/ValidationException.java |   8 ++
 src/main/resources/css/common-elements.css    |   2 +-
 src/main/resources/css/forms.css              |   6 +
 .../features/product/CreateProductView.fxml   |   9 +-
 6 files changed, 192 insertions(+), 36 deletions(-)
 create mode 100644 src/main/java/org/chainoptim/desktop/shared/common/uielements/forms/FormField.java
 create mode 100644 src/main/java/org/chainoptim/desktop/shared/common/uielements/forms/ValidationException.java

diff --git a/src/main/java/org/chainoptim/desktop/features/product/controller/CreateProductController.java b/src/main/java/org/chainoptim/desktop/features/product/controller/CreateProductController.java
index 2ba86a8b..902a3267 100644
--- a/src/main/java/org/chainoptim/desktop/features/product/controller/CreateProductController.java
+++ b/src/main/java/org/chainoptim/desktop/features/product/controller/CreateProductController.java
@@ -7,27 +7,35 @@
 import org.chainoptim.desktop.features.product.dto.CreateProductDTO;
 import org.chainoptim.desktop.features.product.model.Product;
 import org.chainoptim.desktop.features.product.service.ProductWriteService;
+import org.chainoptim.desktop.shared.common.uielements.forms.FormField;
+import org.chainoptim.desktop.shared.common.uielements.forms.ValidationException;
 import org.chainoptim.desktop.shared.common.uielements.select.SelectOrCreateUnitOfMeasurementController;
+import org.chainoptim.desktop.shared.enums.OperationOutcome;
 import org.chainoptim.desktop.shared.fallback.FallbackManager;
+import org.chainoptim.desktop.shared.httphandling.Result;
+import org.chainoptim.desktop.shared.toast.controller.ToastManager;
+import org.chainoptim.desktop.shared.toast.model.ToastInfo;
 import org.chainoptim.desktop.shared.util.resourceloader.CommonViewsLoader;
 
 import com.google.inject.Inject;
 import javafx.application.Platform;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
+import javafx.scene.control.Button;
 import javafx.scene.control.TextField;
 import javafx.scene.layout.StackPane;
 
 import java.net.URL;
 import java.util.ResourceBundle;
 
-public class CreateProductController implements Initializable {
+public class CreateProductController {
 
     private final ProductWriteService productWriteService;
     private final NavigationService navigationService;
     private final CurrentSelectionService currentSelectionService;
     private final CommonViewsLoader commonViewsLoader;
     private final FallbackManager fallbackManager;
+    private final ToastManager toastManager;
 
     private SelectOrCreateUnitOfMeasurementController unitOfMeasurementController;
 
@@ -36,37 +44,42 @@ public class CreateProductController implements Initializable {
     @FXML
     private StackPane unitOfMeasurementContainer;
     @FXML
-    private TextField nameField;
+    private FormField<String> nameFormField;
     @FXML
-    private TextField descriptionField;
+    private FormField<String> descriptionFormField;
 
     @Inject
     public CreateProductController(
             ProductWriteService productWriteService,
             NavigationService navigationService,
             CurrentSelectionService currentSelectionService,
-            FallbackManager fallbackManager,
-            CommonViewsLoader commonViewsLoader
+            CommonViewsLoader commonViewsLoader,
+            ToastManager toastManager,
+            FallbackManager fallbackManager
     ) {
         this.productWriteService = productWriteService;
         this.navigationService = navigationService;
         this.currentSelectionService = currentSelectionService;
         this.commonViewsLoader = commonViewsLoader;
+        this.toastManager = toastManager;
         this.fallbackManager = fallbackManager;
     }
 
-    @Override
-    public void initialize(URL location, ResourceBundle resources) {
+    public void initialize() {
         commonViewsLoader.loadFallbackManager(fallbackContainer);
         unitOfMeasurementController = commonViewsLoader.loadSelectOrCreateUnitOfMeasurement(unitOfMeasurementContainer);
         unitOfMeasurementController.initialize();
+
+        initializeFormFields();
+    }
+
+    private void initializeFormFields() {
+        nameFormField.initialize(String::new, "Name", true, null, "Your input is not valid.");
+        descriptionFormField.initialize(String::new, "Description", false, null, "Your input is not valid.");
     }
 
     @FXML
     private void handleSubmit() {
-        fallbackManager.reset();
-        fallbackManager.setLoading(true);
-
         User currentUser = TenantContext.getCurrentUser();
         if (currentUser == null) {
             return;
@@ -74,33 +87,26 @@ private void handleSubmit() {
         Integer organizationId = currentUser.getOrganization().getId();
 
         CreateProductDTO productDTO = getCreateProductDTO(organizationId);
-        System.out.println("CreateProduct: " + productDTO.getUnitDTO());
+        System.out.println("CreateProduct: " + productDTO);
+        if (productDTO == null) return;
+
+        fallbackManager.reset();
+        fallbackManager.setLoading(true);
 
         productWriteService.createProduct(productDTO)
-                .thenAccept(result ->
-                    // Navigate to product page
-                    Platform.runLater(() -> {
-                        if (result.getError() != null) {
-                            fallbackManager.setErrorMessage("Failed to create product.");
-                            return;
-                        }
-                        Product product = result.getData();
-                        fallbackManager.setLoading(false);
-                        currentSelectionService.setSelectedId(product.getId());
-                        navigationService.switchView("Product?id=" + product.getId(), true);
-                    })
-                )
-                .exceptionally(ex -> {
-                    ex.printStackTrace();
-                    return null;
-                });
+                .thenApply(this::handleCreateProductResponse)
+                .exceptionally(this::handleCreateProductException);
     }
 
     private CreateProductDTO getCreateProductDTO(Integer organizationId) {
         CreateProductDTO productDTO = new CreateProductDTO();
         productDTO.setOrganizationId(organizationId);
-        productDTO.setName(nameField.getText());
-        productDTO.setDescription(descriptionField.getText());
+        try {
+            productDTO.setName(nameFormField.handleSubmit());
+            productDTO.setDescription(descriptionFormField.handleSubmit());
+        } catch (ValidationException e) {
+            return null;
+        }
         if (unitOfMeasurementController.isCreatingNewUnit()) {
             productDTO.setCreateUnit(true);
             productDTO.setUnitDTO(unitOfMeasurementController.getNewUnitDTO());
@@ -111,4 +117,24 @@ private CreateProductDTO getCreateProductDTO(Integer organizationId) {
 
         return productDTO;
     }
+
+    private Result<Product> handleCreateProductResponse(Result<Product> result) {
+        Platform.runLater(() -> {
+            if (result.getError() != null) {
+                fallbackManager.setErrorMessage("Failed to create product.");
+                return;
+            }
+            Product product = result.getData();
+            fallbackManager.setLoading(false);
+            currentSelectionService.setSelectedId(product.getId());
+            navigationService.switchView("Product?id=" + product.getId(), true);
+        });
+        return result;
+    }
+
+    private Result<Product> handleCreateProductException(Throwable ex) {
+        Platform.runLater(() -> toastManager.addToast(
+                new ToastInfo("An error occurred.", "Failed to create product.", OperationOutcome.ERROR)));
+        return new Result<>();
+    }
 }
diff --git a/src/main/java/org/chainoptim/desktop/shared/common/uielements/forms/FormField.java b/src/main/java/org/chainoptim/desktop/shared/common/uielements/forms/FormField.java
new file mode 100644
index 00000000..d5437e81
--- /dev/null
+++ b/src/main/java/org/chainoptim/desktop/shared/common/uielements/forms/FormField.java
@@ -0,0 +1,117 @@
+package org.chainoptim.desktop.shared.common.uielements.forms;
+
+import org.chainoptim.desktop.shared.table.util.StringConverter;
+
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Text;
+import javafx.scene.text.TextFlow;
+
+public class FormField<T> extends VBox {
+
+    // Initial state
+    private StringConverter<T> stringConverter;
+    private String formLabel;
+    private boolean isMandatory;
+    private String errorMessage;
+
+    // Current state
+    private String currentValue;
+    private boolean hasTriedSubmitting = false;
+
+    // UI
+    private TextField textField;
+    private Label errorLabel;
+    private Label mandatoryErrorLabel;
+
+    public void initialize(StringConverter<T> stringConverter, String formLabel, boolean isMandatory, T initialValue, String errorMessage) {
+        this.stringConverter = stringConverter;
+        this.formLabel = formLabel;
+        this.isMandatory = isMandatory;
+        this.errorMessage = errorMessage;
+
+        this.currentValue = initialValue != null ? initialValue.toString() : "";
+
+
+        initializeUI();
+    }
+
+    private void initializeUI() {
+        // Label
+        TextFlow label = new TextFlow();
+        Text labelText = new Text(formLabel);
+        label.getChildren().add(labelText);
+        if (isMandatory) {
+            Text mandatoryText = new Text(" *");
+            mandatoryText.setStyle("-fx-fill: #B22222;");
+            label.getChildren().add(mandatoryText);
+        }
+        label.getStyleClass().add("form-label");
+        this.getChildren().add(label);
+
+        textField = new TextField(currentValue);
+        textField.getStyleClass().setAll("custom-text-field");
+        textField.textProperty().addListener((observable, oldValue, newValue) -> {
+            currentValue = newValue;
+            if (hasTriedSubmitting) {
+                checkValidity();
+            }
+        });
+        this.getChildren().add(textField);
+
+        VBox errorVBox = new VBox(4);
+        errorVBox.setStyle("-fx-padding: 4px 0px 0px 0px;");
+        errorLabel = new Label(errorMessage);
+        errorLabel.getStyleClass().add("form-error-message");
+        toggleNodeVisibility(errorLabel, false);
+        errorVBox.getChildren().add(errorLabel);
+
+        mandatoryErrorLabel = new Label("This field is mandatory.");
+        mandatoryErrorLabel.getStyleClass().add("form-error-message");
+        toggleNodeVisibility(mandatoryErrorLabel, false);
+        errorVBox.getChildren().add(mandatoryErrorLabel);
+
+        this.getChildren().add(errorVBox);
+    }
+
+    private void checkValidity() {
+        String input = textField.getText();
+        if (isMandatory && input.isEmpty()) {
+            toggleNodeVisibility(mandatoryErrorLabel, true);
+            return;
+        }
+        try {
+            stringConverter.convert(input);
+            toggleNodeVisibility(errorLabel, false);
+            toggleNodeVisibility(mandatoryErrorLabel, false);
+        } catch (Exception e) {
+            toggleNodeVisibility(errorLabel, true);
+        }
+    }
+
+    public T handleSubmit() throws ValidationException {
+        hasTriedSubmitting = true;
+
+        String input = textField.getText();
+        if (isMandatory && input.isEmpty()) {
+            toggleNodeVisibility(mandatoryErrorLabel, true);
+            throw new ValidationException("Mandatory field is empty.");
+        }
+        try {
+            T value = stringConverter.convert(input);
+            toggleNodeVisibility(errorLabel, false);
+            toggleNodeVisibility(mandatoryErrorLabel, false);
+            return value;
+        } catch (Exception e) {
+            toggleNodeVisibility(errorLabel, true);
+            throw new ValidationException("Mandatory field is empty.");
+        }
+    }
+
+    private void toggleNodeVisibility(Node node, boolean isVisible) {
+        node.setVisible(isVisible);
+        node.setManaged(isVisible);
+    }
+}
diff --git a/src/main/java/org/chainoptim/desktop/shared/common/uielements/forms/ValidationException.java b/src/main/java/org/chainoptim/desktop/shared/common/uielements/forms/ValidationException.java
new file mode 100644
index 00000000..fa98906d
--- /dev/null
+++ b/src/main/java/org/chainoptim/desktop/shared/common/uielements/forms/ValidationException.java
@@ -0,0 +1,8 @@
+package org.chainoptim.desktop.shared.common.uielements.forms;
+
+public class ValidationException extends Exception {
+
+    public ValidationException(String message) {
+        super(message);
+    }
+}
diff --git a/src/main/resources/css/common-elements.css b/src/main/resources/css/common-elements.css
index 4d9d59ed..19bb715b 100644
--- a/src/main/resources/css/common-elements.css
+++ b/src/main/resources/css/common-elements.css
@@ -45,7 +45,7 @@
 .custom-text-field {
     -fx-min-height: 32px;
     -fx-max-height: 32px;
-    -fx-padding: 0px 4px;
+    -fx-padding: 0px 6px;
     -fx-background-color: #ffffff;
     -fx-font-weight: bold;
     -fx-border-color: #c0c0c0;
diff --git a/src/main/resources/css/forms.css b/src/main/resources/css/forms.css
index 51a4180c..0a2c51da 100644
--- a/src/main/resources/css/forms.css
+++ b/src/main/resources/css/forms.css
@@ -28,4 +28,10 @@
 .radio-button {
     -fx-font-size: 14px;
     -fx-font-weight: bold;
+}
+
+.form-error-message {
+    -fx-text-fill: #B22222;
+    -fx-font-weight: 600;
+    -fx-font-size: 13px;
 }
\ No newline at end of file
diff --git a/src/main/resources/org/chainoptim/desktop/features/product/CreateProductView.fxml b/src/main/resources/org/chainoptim/desktop/features/product/CreateProductView.fxml
index 0943f16b..c970307a 100644
--- a/src/main/resources/org/chainoptim/desktop/features/product/CreateProductView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/product/CreateProductView.fxml
@@ -1,10 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.control.Label?>
-<?import javafx.scene.control.TextField?>
 <?import javafx.scene.control.Button?>
 <?import javafx.scene.text.Text?>
 <?import javafx.scene.control.ScrollPane?>
+<?import org.chainoptim.desktop.shared.common.uielements.forms.FormField?>
+
 <VBox xmlns="http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
       fx:controller="org.chainoptim.desktop.features.product.controller.CreateProductController">
@@ -15,11 +16,9 @@
                 <Text text="Create Product" styleClass="form-title"/>
             </HBox>
 
-            <Label text="Name *:" styleClass="form-label"/>
-            <TextField fx:id="nameField" styleClass="custom-text-field"/>
+            <FormField fx:id="nameFormField"/>
 
-            <Label text="Description:" styleClass="form-label"/>
-            <TextField fx:id="descriptionField" styleClass="custom-text-field"/>
+            <FormField fx:id="descriptionFormField"/>
 
             <Label text="Unit of Measurement:" styleClass="form-label"/>
             <StackPane fx:id="unitOfMeasurementContainer"/>

From 87516c8b4a2d128d3d6605f3f06f902b1b0200a2 Mon Sep 17 00:00:00 2001
From: TudorOrban <130213626+TudorOrban@users.noreply.github.com>
Date: Thu, 25 Apr 2024 22:30:50 +0300
Subject: [PATCH 08/10] Continue refactoring forms

---
 .../controller/CreateClientController.java    | 74 +++++++++++++------
 .../controller/UpdateClientController.java    | 74 ++++++++++++-------
 .../controller/CreateProductController.java   | 28 ++++---
 .../controller/CreateComponentController.java | 69 +++++++----------
 .../common/uielements/forms/FormField.java    | 24 ++++--
 .../SelectOrCreateLocationController.java     | 61 ++++++++-------
 ...ctOrCreateUnitOfMeasurementController.java | 21 ++++--
 .../SelectOrCreateLocationView.fxml           | 23 +++---
 .../SelectOrCreateUnitOfMeasurementView.fxml  |  7 +-
 9 files changed, 214 insertions(+), 167 deletions(-)

diff --git a/src/main/java/org/chainoptim/desktop/features/client/controller/CreateClientController.java b/src/main/java/org/chainoptim/desktop/features/client/controller/CreateClientController.java
index 4c7e1d8f..56717f3f 100644
--- a/src/main/java/org/chainoptim/desktop/features/client/controller/CreateClientController.java
+++ b/src/main/java/org/chainoptim/desktop/features/client/controller/CreateClientController.java
@@ -7,9 +7,14 @@
 import org.chainoptim.desktop.features.client.dto.CreateClientDTO;
 import org.chainoptim.desktop.features.client.model.Client;
 import org.chainoptim.desktop.features.client.service.ClientWriteService;
+import org.chainoptim.desktop.shared.common.uielements.forms.FormField;
+import org.chainoptim.desktop.shared.common.uielements.forms.ValidationException;
 import org.chainoptim.desktop.shared.common.uielements.select.SelectOrCreateLocationController;
+import org.chainoptim.desktop.shared.enums.OperationOutcome;
 import org.chainoptim.desktop.shared.fallback.FallbackManager;
 import org.chainoptim.desktop.shared.httphandling.Result;
+import org.chainoptim.desktop.shared.toast.controller.ToastManager;
+import org.chainoptim.desktop.shared.toast.model.ToastInfo;
 import org.chainoptim.desktop.shared.util.resourceloader.CommonViewsLoader;
 
 import javafx.scene.layout.StackPane;
@@ -17,7 +22,6 @@
 import javafx.application.Platform;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
-import javafx.scene.control.TextField;
 
 import java.net.URL;
 import java.util.ResourceBundle;
@@ -28,6 +32,7 @@ public class CreateClientController implements Initializable {
     private final NavigationService navigationService;
     private final CurrentSelectionService currentSelectionService;
     private final CommonViewsLoader commonViewsLoader;
+    private final ToastManager toastManager;
     private final FallbackManager fallbackManager;
 
     private SelectOrCreateLocationController selectOrCreateLocationController;
@@ -37,19 +42,21 @@ public class CreateClientController implements Initializable {
     @FXML
     private StackPane selectOrCreateLocationContainer;
     @FXML
-    private TextField nameField;
+    private FormField<String> nameFormField;
 
 
     @Inject
     public CreateClientController(ClientWriteService clientWriteService,
                                   NavigationService navigationService,
                                   CurrentSelectionService currentSelectionService,
+                                  ToastManager toastManager,
                                   FallbackManager fallbackManager,
                                   CommonViewsLoader commonViewsLoader) {
         this.clientWriteService = clientWriteService;
         this.navigationService = navigationService;
         this.currentSelectionService = currentSelectionService;
         this.commonViewsLoader = commonViewsLoader;
+        this.toastManager = toastManager;
         this.fallbackManager = fallbackManager;
     }
 
@@ -57,14 +64,16 @@ public CreateClientController(ClientWriteService clientWriteService,
     public void initialize(URL location, ResourceBundle resources) {
         commonViewsLoader.loadFallbackManager(fallbackContainer);
         selectOrCreateLocationController = commonViewsLoader.loadSelectOrCreateLocation(selectOrCreateLocationContainer);
-        selectOrCreateLocationController.initialize();
+
+        initializeFormFields();
+    }
+
+    private void initializeFormFields() {
+        nameFormField.initialize(String::new, "Name", true, null, "Your input is not valid.");
     }
 
     @FXML
     private void handleSubmit() {
-        fallbackManager.reset();
-        fallbackManager.setLoading(true);
-
         User currentUser = TenantContext.getCurrentUser();
         if (currentUser == null) {
             return;
@@ -72,42 +81,59 @@ private void handleSubmit() {
         Integer organizationId = currentUser.getOrganization().getId();
 
         CreateClientDTO clientDTO = getCreateClientDTO(organizationId);
+        if (clientDTO == null) return;
+
+        fallbackManager.reset();
+        fallbackManager.setLoading(true);
 
         clientWriteService.createClient(clientDTO)
                 .thenApply(this::handleCreateClientResponse)
-                .exceptionally(ex -> {
-                    ex.printStackTrace();
-                    return new Result<>();
-                });
+                .exceptionally(this::handleCreateClientException);
+    }
+
+    private CreateClientDTO getCreateClientDTO(Integer organizationId) {
+        CreateClientDTO clientDTO = new CreateClientDTO();
+        try {
+            clientDTO.setName(nameFormField.handleSubmit());
+            clientDTO.setOrganizationId(organizationId);
+            if (selectOrCreateLocationController.isCreatingNewLocation()) {
+                clientDTO.setCreateLocation(true);
+                clientDTO.setLocation(selectOrCreateLocationController.getNewLocationDTO());
+            } else {
+                clientDTO.setCreateLocation(false);
+                clientDTO.setLocationId(selectOrCreateLocationController.getSelectedLocation().getId());
+            }
+        } catch (ValidationException e) {
+            return null;
+        }
+
+        return clientDTO;
     }
 
     private Result<Client> handleCreateClientResponse(Result<Client> result) {
         Platform.runLater(() -> {
             if (result.getError() != null) {
-                fallbackManager.setErrorMessage("Failed to create client.");
+                toastManager.addToast(new ToastInfo(
+                        "Error", "Failed to create client.", OperationOutcome.ERROR));
                 return;
             }
             Client client = result.getData();
             fallbackManager.setLoading(false);
+            toastManager.addToast(new ToastInfo(
+                    "Success", "Client created successfully.", OperationOutcome.SUCCESS));
+
             currentSelectionService.setSelectedId(client.getId());
             navigationService.switchView("Client?id=" + client.getId(), true);
         });
         return result;
     }
 
-    private CreateClientDTO getCreateClientDTO(Integer organizationId) {
-        CreateClientDTO clientDTO = new CreateClientDTO();
-        clientDTO.setName(nameField.getText());
-        clientDTO.setOrganizationId(organizationId);
-        if (selectOrCreateLocationController.isCreatingNewLocation()) {
-            clientDTO.setCreateLocation(true);
-            clientDTO.setLocation(selectOrCreateLocationController.getNewLocationDTO());
-        } else {
-            clientDTO.setCreateLocation(false);
-            clientDTO.setLocationId(selectOrCreateLocationController.getSelectedLocation().getId());
-        }
-
-        return clientDTO;
+    private Result<Client> handleCreateClientException(Throwable ex) {
+        Platform.runLater(() ->
+            toastManager.addToast(new ToastInfo(
+                    "Error", "Failed to create client.", OperationOutcome.ERROR)));
+        return new Result<>();
     }
+
 }
 
diff --git a/src/main/java/org/chainoptim/desktop/features/client/controller/UpdateClientController.java b/src/main/java/org/chainoptim/desktop/features/client/controller/UpdateClientController.java
index f7d0e298..681f24e7 100644
--- a/src/main/java/org/chainoptim/desktop/features/client/controller/UpdateClientController.java
+++ b/src/main/java/org/chainoptim/desktop/features/client/controller/UpdateClientController.java
@@ -7,9 +7,15 @@
 import org.chainoptim.desktop.features.client.model.Client;
 import org.chainoptim.desktop.features.client.service.ClientService;
 import org.chainoptim.desktop.features.client.service.ClientWriteService;
+import org.chainoptim.desktop.features.product.model.Product;
+import org.chainoptim.desktop.shared.common.uielements.forms.FormField;
+import org.chainoptim.desktop.shared.common.uielements.forms.ValidationException;
 import org.chainoptim.desktop.shared.common.uielements.select.SelectOrCreateLocationController;
+import org.chainoptim.desktop.shared.enums.OperationOutcome;
 import org.chainoptim.desktop.shared.fallback.FallbackManager;
 import org.chainoptim.desktop.shared.httphandling.Result;
+import org.chainoptim.desktop.shared.toast.controller.ToastManager;
+import org.chainoptim.desktop.shared.toast.model.ToastInfo;
 import org.chainoptim.desktop.shared.util.resourceloader.CommonViewsLoader;
 
 import com.google.inject.Inject;
@@ -30,6 +36,7 @@ public class UpdateClientController implements Initializable {
     private final NavigationService navigationService;
     private final CurrentSelectionService currentSelectionService;
     private final CommonViewsLoader commonViewsLoader;
+    private final ToastManager toastManager;
     private final FallbackManager fallbackManager;
 
     private Client client;
@@ -41,7 +48,7 @@ public class UpdateClientController implements Initializable {
     @FXML
     private StackPane selectOrCreateLocationContainer;
     @FXML
-    private TextField nameField;
+    private FormField<String> nameFormField;
 
     @Inject
     public UpdateClientController(ClientService clientService,
@@ -49,12 +56,14 @@ public UpdateClientController(ClientService clientService,
                                   NavigationService navigationService,
                                   CurrentSelectionService currentSelectionService,
                                   CommonViewsLoader commonViewsLoader,
+                                  ToastManager toastManager,
                                   FallbackManager fallbackManager) {
         this.clientService = clientService;
         this.clientWriteService = clientWriteService;
         this.navigationService = navigationService;
         this.currentSelectionService = currentSelectionService;
         this.commonViewsLoader = commonViewsLoader;
+        this.toastManager = toastManager;
         this.fallbackManager = fallbackManager;
     }
 
@@ -78,13 +87,15 @@ private void loadClient(Integer clientId) {
     private Result<Client> handleClientResponse(Result<Client> result) {
         Platform.runLater(() -> {
             if (result.getError() != null) {
-                fallbackManager.setErrorMessage("Failed to load client.");
+                toastManager.addToast(new ToastInfo(
+                        "Error", "Failed to update client.", OperationOutcome.ERROR));
                 return;
             }
             client = result.getData();
             fallbackManager.setLoading(false);
 
-            nameField.setText(client.getName());
+            initializeFormFields();
+
             selectOrCreateLocationController.setSelectedLocation(client.getLocation());
         });
 
@@ -96,26 +107,49 @@ private Result<Client> handleClientException(Throwable ex) {
         return new Result<>();
     }
 
+    private void initializeFormFields() {
+        nameFormField.initialize(String::new, "Name", true, client.getName(), "Your input is not valid");
+    }
+
     @FXML
     private void handleSubmit() {
-        fallbackManager.reset();
-        fallbackManager.setLoading(true);
-
         UpdateClientDTO clientDTO = getUpdateClientDTO();
+        if (clientDTO == null) return;
         System.out.println(clientDTO);
 
+        fallbackManager.reset();
+        fallbackManager.setLoading(true);
+
         clientWriteService.updateClient(clientDTO)
                 .thenApply(this::handleUpdateClientResponse)
-                .exceptionally(ex -> {
-                    ex.printStackTrace();
-                    return new Result<>();
-                });
+                .exceptionally(this::handleUpdateClientException);
+    }
+
+    private UpdateClientDTO getUpdateClientDTO() {
+        UpdateClientDTO clientDTO = new UpdateClientDTO();
+        clientDTO.setId(client.getId());
+        try {
+            clientDTO.setName(nameFormField.handleSubmit());
+
+            if (selectOrCreateLocationController.isCreatingNewLocation()) {
+                clientDTO.setCreateLocation(true);
+                clientDTO.setLocation(selectOrCreateLocationController.getNewLocationDTO());
+            } else {
+                clientDTO.setCreateLocation(false);
+                clientDTO.setLocationId(selectOrCreateLocationController.getSelectedLocation().getId());
+            }
+        } catch (ValidationException e) {
+            return null;
+        }
+
+        return clientDTO;
     }
 
     private Result<Client> handleUpdateClientResponse(Result<Client> result) {
         Platform.runLater(() -> {
             if (result.getError() != null) {
-                fallbackManager.setErrorMessage("Failed to create client.");
+                toastManager.addToast(new ToastInfo(
+                        "Error", "Failed to update client.", OperationOutcome.ERROR));
                 return;
             }
             fallbackManager.setLoading(false);
@@ -130,20 +164,10 @@ private Result<Client> handleUpdateClientResponse(Result<Client> result) {
         return result;
     }
 
-    private UpdateClientDTO getUpdateClientDTO() {
-        UpdateClientDTO clientDTO = new UpdateClientDTO();
-        clientDTO.setId(client.getId());
-        clientDTO.setName(nameField.getText());
-
-        if (selectOrCreateLocationController.isCreatingNewLocation()) {
-            clientDTO.setCreateLocation(true);
-            clientDTO.setLocation(selectOrCreateLocationController.getNewLocationDTO());
-        } else {
-            clientDTO.setCreateLocation(false);
-            clientDTO.setLocationId(selectOrCreateLocationController.getSelectedLocation().getId());
-        }
-
-        return clientDTO;
+    private Result<Client> handleUpdateClientException(Throwable ex) {
+        Platform.runLater(() -> toastManager.addToast(new ToastInfo(
+                "An error occurred.", "Failed to update client.", OperationOutcome.ERROR)));
+        return new Result<>();
     }
 }
 
diff --git a/src/main/java/org/chainoptim/desktop/features/product/controller/CreateProductController.java b/src/main/java/org/chainoptim/desktop/features/product/controller/CreateProductController.java
index 902a3267..fb5757d7 100644
--- a/src/main/java/org/chainoptim/desktop/features/product/controller/CreateProductController.java
+++ b/src/main/java/org/chainoptim/desktop/features/product/controller/CreateProductController.java
@@ -68,7 +68,6 @@ public CreateProductController(
     public void initialize() {
         commonViewsLoader.loadFallbackManager(fallbackContainer);
         unitOfMeasurementController = commonViewsLoader.loadSelectOrCreateUnitOfMeasurement(unitOfMeasurementContainer);
-        unitOfMeasurementController.initialize();
 
         initializeFormFields();
     }
@@ -87,7 +86,6 @@ private void handleSubmit() {
         Integer organizationId = currentUser.getOrganization().getId();
 
         CreateProductDTO productDTO = getCreateProductDTO(organizationId);
-        System.out.println("CreateProduct: " + productDTO);
         if (productDTO == null) return;
 
         fallbackManager.reset();
@@ -104,16 +102,18 @@ private CreateProductDTO getCreateProductDTO(Integer organizationId) {
         try {
             productDTO.setName(nameFormField.handleSubmit());
             productDTO.setDescription(descriptionFormField.handleSubmit());
+            if (unitOfMeasurementController.isCreatingNewUnit()) {
+                productDTO.setCreateUnit(true);
+                productDTO.setUnitDTO(unitOfMeasurementController.getNewUnitDTO());
+            } else {
+                productDTO.setCreateUnit(false);
+                if (unitOfMeasurementController.getSelectedUnit() != null) {
+                    productDTO.setUnitId(unitOfMeasurementController.getSelectedUnit().getId());
+                }
+            }
         } catch (ValidationException e) {
             return null;
         }
-        if (unitOfMeasurementController.isCreatingNewUnit()) {
-            productDTO.setCreateUnit(true);
-            productDTO.setUnitDTO(unitOfMeasurementController.getNewUnitDTO());
-        } else {
-            productDTO.setCreateUnit(false);
-            productDTO.setUnitId(unitOfMeasurementController.getSelectedUnit().getId());
-        }
 
         return productDTO;
     }
@@ -121,11 +121,15 @@ private CreateProductDTO getCreateProductDTO(Integer organizationId) {
     private Result<Product> handleCreateProductResponse(Result<Product> result) {
         Platform.runLater(() -> {
             if (result.getError() != null) {
-                fallbackManager.setErrorMessage("Failed to create product.");
+                toastManager.addToast(new ToastInfo(
+                        "Error", "Failed to create product.", OperationOutcome.ERROR));
                 return;
             }
             Product product = result.getData();
             fallbackManager.setLoading(false);
+            toastManager.addToast(new ToastInfo
+                    ("Product created.", "Product has been successfully created.", OperationOutcome.SUCCESS));
+
             currentSelectionService.setSelectedId(product.getId());
             navigationService.switchView("Product?id=" + product.getId(), true);
         });
@@ -133,8 +137,8 @@ private Result<Product> handleCreateProductResponse(Result<Product> result) {
     }
 
     private Result<Product> handleCreateProductException(Throwable ex) {
-        Platform.runLater(() -> toastManager.addToast(
-                new ToastInfo("An error occurred.", "Failed to create product.", OperationOutcome.ERROR)));
+        Platform.runLater(() -> toastManager.addToast(new ToastInfo(
+                "An error occurred.", "Failed to create product.", OperationOutcome.ERROR)));
         return new Result<>();
     }
 }
diff --git a/src/main/java/org/chainoptim/desktop/features/productpipeline/controller/CreateComponentController.java b/src/main/java/org/chainoptim/desktop/features/productpipeline/controller/CreateComponentController.java
index 6c2aa86e..e727936b 100644
--- a/src/main/java/org/chainoptim/desktop/features/productpipeline/controller/CreateComponentController.java
+++ b/src/main/java/org/chainoptim/desktop/features/productpipeline/controller/CreateComponentController.java
@@ -8,8 +8,10 @@
 import org.chainoptim.desktop.features.productpipeline.dto.CreateComponentDTO;
 import org.chainoptim.desktop.features.productpipeline.model.Component;
 import org.chainoptim.desktop.features.productpipeline.service.ComponentService;
+import org.chainoptim.desktop.shared.common.uielements.forms.ValidationException;
 import org.chainoptim.desktop.shared.common.uielements.select.SelectOrCreateUnitOfMeasurementController;
 import org.chainoptim.desktop.shared.fallback.FallbackManager;
+import org.chainoptim.desktop.shared.util.resourceloader.CommonViewsLoader;
 import org.chainoptim.desktop.shared.util.resourceloader.FXMLLoaderService;
 
 import com.google.inject.Inject;
@@ -30,8 +32,7 @@ public class CreateComponentController implements Initializable {
     private final ComponentService componentService;
     private final NavigationService navigationService;
     private final CurrentSelectionService currentSelectionService;
-    private final FXMLLoaderService fxmlLoaderService;
-    private final ControllerFactory controllerFactory;
+    private final CommonViewsLoader commonViewsLoader;
     private final FallbackManager fallbackManager;
 
     private SelectOrCreateUnitOfMeasurementController unitOfMeasurementController;
@@ -51,52 +52,24 @@ public CreateComponentController(
             NavigationService navigationService,
             CurrentSelectionService currentSelectionService,
             FallbackManager fallbackManager,
-            FXMLLoaderService fxmlLoaderService,
-            ControllerFactory controllerFactory
+            CommonViewsLoader commonViewsLoader
     ) {
         this.componentService = componentService;
         this.navigationService = navigationService;
         this.currentSelectionService = currentSelectionService;
-        this.fxmlLoaderService = fxmlLoaderService;
-        this.controllerFactory = controllerFactory;
+        this.commonViewsLoader = commonViewsLoader;
         this.fallbackManager = fallbackManager;
     }
 
     @Override
     public void initialize(URL location, ResourceBundle resources) {
-        loadFallbackManager();
-        loadSelectOrCreateUnitOfMeasurement();
-    }
-
-    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 loadSelectOrCreateUnitOfMeasurement() {
-        FXMLLoader loader = fxmlLoaderService.setUpLoader(
-                "/org/chainoptim/desktop/shared/common/uielements/SelectOrCreateUnitOfMeasurementView.fxml",
-                controllerFactory::createController
-        );
-        try {
-            Node selectOrCreateUnitOfMeasurementView = loader.load();
-            unitOfMeasurementController = loader.getController();
-            unitOfMeasurementContainer.getChildren().add(selectOrCreateUnitOfMeasurementView);
-            unitOfMeasurementController.initialize();
-        } catch (IOException ex) {
-            ex.printStackTrace();
-        }
+        commonViewsLoader.loadFallbackManager(fallbackContainer);
+        unitOfMeasurementController = commonViewsLoader.loadSelectOrCreateUnitOfMeasurement(unitOfMeasurementContainer);
+        unitOfMeasurementController.initialize();
     }
 
     @FXML
     private void handleSubmit() {
-        fallbackManager.reset();
-        fallbackManager.setLoading(true);
-
         User currentUser = TenantContext.getCurrentUser();
         if (currentUser == null) {
             return;
@@ -104,7 +77,11 @@ private void handleSubmit() {
         Integer organizationId = currentUser.getOrganization().getId();
 
         CreateComponentDTO componentDTO = getCreateComponentDTO(organizationId);
-        System.out.println("CreateComponent: " + componentDTO.getUnitDTO());
+        System.out.println("CreateComponent: " + componentDTO);
+        if (componentDTO == null) return;
+
+        fallbackManager.reset();
+        fallbackManager.setLoading(true);
 
         componentService.createComponent(componentDTO)
                 .thenAccept(result ->
@@ -129,14 +106,18 @@ private void handleSubmit() {
     private CreateComponentDTO getCreateComponentDTO(Integer organizationId) {
         CreateComponentDTO componentDTO = new CreateComponentDTO();
         componentDTO.setOrganizationId(organizationId);
-        componentDTO.setName(nameField.getText());
-        componentDTO.setDescription(descriptionField.getText());
-        if (unitOfMeasurementController.isCreatingNewUnit()) {
-            componentDTO.setCreateUnit(true);
-            componentDTO.setUnitDTO(unitOfMeasurementController.getNewUnitDTO());
-        } else {
-            componentDTO.setCreateUnit(false);
-            componentDTO.setUnitId(unitOfMeasurementController.getSelectedUnit().getId());
+        try {
+            componentDTO.setName(nameField.getText());
+            componentDTO.setDescription(descriptionField.getText());
+            if (unitOfMeasurementController.isCreatingNewUnit()) {
+                componentDTO.setCreateUnit(true);
+                componentDTO.setUnitDTO(unitOfMeasurementController.getNewUnitDTO());
+            } else {
+                componentDTO.setCreateUnit(false);
+                componentDTO.setUnitId(unitOfMeasurementController.getSelectedUnit().getId());
+            }
+        } catch (ValidationException e) {
+            return null;
         }
 
         return componentDTO;
diff --git a/src/main/java/org/chainoptim/desktop/shared/common/uielements/forms/FormField.java b/src/main/java/org/chainoptim/desktop/shared/common/uielements/forms/FormField.java
index d5437e81..9c60188e 100644
--- a/src/main/java/org/chainoptim/desktop/shared/common/uielements/forms/FormField.java
+++ b/src/main/java/org/chainoptim/desktop/shared/common/uielements/forms/FormField.java
@@ -34,10 +34,14 @@ public void initialize(StringConverter<T> stringConverter, String formLabel, boo
 
         this.currentValue = initialValue != null ? initialValue.toString() : "";
 
-
         initializeUI();
     }
 
+    public void setInitialValue(T initialValue) {
+        this.currentValue = initialValue != null ? initialValue.toString() : "";
+        textField.setText(currentValue);
+    }
+
     private void initializeUI() {
         // Label
         TextFlow label = new TextFlow();
@@ -78,9 +82,11 @@ private void initializeUI() {
 
     private void checkValidity() {
         String input = textField.getText();
-        if (isMandatory && input.isEmpty()) {
-            toggleNodeVisibility(mandatoryErrorLabel, true);
-            return;
+        if (input.isEmpty()) {
+            if (isMandatory) {
+                toggleNodeVisibility(mandatoryErrorLabel, true);
+            }
+            return; // Skip parsing for non-mandatory fields
         }
         try {
             stringConverter.convert(input);
@@ -95,10 +101,14 @@ public T handleSubmit() throws ValidationException {
         hasTriedSubmitting = true;
 
         String input = textField.getText();
-        if (isMandatory && input.isEmpty()) {
-            toggleNodeVisibility(mandatoryErrorLabel, true);
-            throw new ValidationException("Mandatory field is empty.");
+        if (input.isEmpty()) {
+            if (isMandatory) {
+                toggleNodeVisibility(mandatoryErrorLabel, true);
+                throw new ValidationException("Mandatory field is empty.");
+            }
+            return null; // Skip parsing for non-mandatory fields
         }
+
         try {
             T value = stringConverter.convert(input);
             toggleNodeVisibility(errorLabel, false);
diff --git a/src/main/java/org/chainoptim/desktop/shared/common/uielements/select/SelectOrCreateLocationController.java b/src/main/java/org/chainoptim/desktop/shared/common/uielements/select/SelectOrCreateLocationController.java
index 0797eb96..5b3384ff 100644
--- a/src/main/java/org/chainoptim/desktop/shared/common/uielements/select/SelectOrCreateLocationController.java
+++ b/src/main/java/org/chainoptim/desktop/shared/common/uielements/select/SelectOrCreateLocationController.java
@@ -4,6 +4,8 @@
 import javafx.scene.layout.VBox;
 import org.chainoptim.desktop.core.context.TenantContext;
 import org.chainoptim.desktop.core.user.model.User;
+import org.chainoptim.desktop.shared.common.uielements.forms.FormField;
+import org.chainoptim.desktop.shared.common.uielements.forms.ValidationException;
 import org.chainoptim.desktop.shared.features.location.dto.CreateLocationDTO;
 import org.chainoptim.desktop.shared.features.location.model.Location;
 import org.chainoptim.desktop.shared.features.location.service.LocationService;
@@ -29,19 +31,19 @@ public class SelectOrCreateLocationController {
     @FXML
     private VBox createLocationForm;
     @FXML
-    private TextField addressField;
+    private FormField<String> addressField;
     @FXML
-    private TextField cityField;
+    private FormField<String> cityField;
     @FXML
-    private TextField stateField;
+    private FormField<String> stateField;
     @FXML
-    private TextField countryField;
+    private FormField<String> countryField;
     @FXML
-    private TextField zipCodeField;
+    private FormField<String> zipCodeField;
     @FXML
-    private TextField latitudeField;
+    private FormField<Double> latitudeField;
     @FXML
-    private TextField longitudeField;
+    private FormField<Double> longitudeField;
 
     private final ToggleGroup locationToggleGroup = new ToggleGroup();
 
@@ -62,9 +64,21 @@ public void initialize() {
             toggleVisibilityBasedOnSelection();
         });
 
+        initializeFormFields();
+
         loadLocations();
     }
 
+    private void initializeFormFields() {
+        addressField.initialize(String::new, "Address", false, null, "Your input is not valid.");
+        cityField.initialize(String::new, "City", false, null, "Your input is not valid.");
+        stateField.initialize(String::new, "State", false, null, "Your input is not valid.");
+        countryField.initialize(String::new, "Country", false, null, "Your input is not valid.");
+        zipCodeField.initialize(String::new, "Zip Code", false, null, "Your input is not valid.");
+        latitudeField.initialize(Double::parseDouble, "Latitude", false, null, "Your input is not a number.");
+        longitudeField.initialize(Double::parseDouble, "Longitude", false, null, "Your input is not a number.");
+    }
+
     private void loadLocations() {
         locationComboBox.setCellFactory(lv -> new ListCell<Location>() {
             @Override
@@ -129,34 +143,17 @@ public void setSelectedLocation(Location location) {
 
     }
 
-    public CreateLocationDTO getNewLocationDTO() {
+    public CreateLocationDTO getNewLocationDTO() throws ValidationException {
         CreateLocationDTO locationDTO = new CreateLocationDTO();
         locationDTO.setOrganizationId(organizationId);
-        locationDTO.setAddress(addressField.getText());
-        locationDTO.setCity(cityField.getText());
-        locationDTO.setState(stateField.getText());
-        locationDTO.setCountry(countryField.getText());
-        locationDTO.setZipCode(zipCodeField.getText());
-
-        String latitudeStr = latitudeField.getText().trim();
-        if (!latitudeStr.isEmpty()) {
-            try {
-                double latitude = Double.parseDouble(latitudeStr);
-                locationDTO.setLatitude(latitude);
-            } catch (NumberFormatException e) {
-                System.out.println("Error parsing longitude");
-            }
-        }
+        locationDTO.setAddress(addressField.handleSubmit());
+        locationDTO.setCity(cityField.handleSubmit());
+        locationDTO.setState(stateField.handleSubmit());
+        locationDTO.setCountry(countryField.handleSubmit());
+        locationDTO.setZipCode(zipCodeField.handleSubmit());
+        locationDTO.setLatitude(latitudeField.handleSubmit());
+        locationDTO.setLongitude(longitudeField.handleSubmit());
 
-        String longitudeStr = longitudeField.getText().trim();
-        if (!longitudeStr.isEmpty()) {
-            try {
-                double longitude = Double.parseDouble(longitudeStr);
-                locationDTO.setLongitude(longitude);
-            } catch (NumberFormatException e) {
-                System.out.println("Error parsing longitude");
-            }
-        }
         return locationDTO;
     }
 
diff --git a/src/main/java/org/chainoptim/desktop/shared/common/uielements/select/SelectOrCreateUnitOfMeasurementController.java b/src/main/java/org/chainoptim/desktop/shared/common/uielements/select/SelectOrCreateUnitOfMeasurementController.java
index 874b7ff1..b1c42d30 100644
--- a/src/main/java/org/chainoptim/desktop/shared/common/uielements/select/SelectOrCreateUnitOfMeasurementController.java
+++ b/src/main/java/org/chainoptim/desktop/shared/common/uielements/select/SelectOrCreateUnitOfMeasurementController.java
@@ -10,6 +10,8 @@
 import org.chainoptim.desktop.features.product.dto.CreateUnitOfMeasurementDTO;
 import org.chainoptim.desktop.features.product.model.UnitOfMeasurement;
 import org.chainoptim.desktop.features.product.service.UnitOfMeasurementService;
+import org.chainoptim.desktop.shared.common.uielements.forms.FormField;
+import org.chainoptim.desktop.shared.common.uielements.forms.ValidationException;
 import org.chainoptim.desktop.shared.httphandling.Result;
 
 import java.util.List;
@@ -28,9 +30,9 @@ public class SelectOrCreateUnitOfMeasurementController {
     @FXML
     private VBox createUnitForm;
     @FXML
-    private TextField nameField;
+    private FormField<String> unitNameField;
     @FXML
-    private TextField unitTypeField;
+    private FormField<String> unitTypeField;
 
     private final ToggleGroup unitToggleGroup = new ToggleGroup();
 
@@ -51,6 +53,8 @@ public void initialize() {
             toggleVisibilityBasedOnSelection();
         });
 
+        initializeFormFields();
+
         loadUnitsOfMeasurement();
     }
 
@@ -101,6 +105,11 @@ private Result<List<UnitOfMeasurement>> handleUnitsException(Throwable ex) {
         return new Result<>();
     }
 
+    private void initializeFormFields() {
+        unitNameField.initialize(String::new, "Name", true, null, "Your input is not valid.");
+        unitTypeField.initialize(String::new, "Type", true, null, "Your input is not valid.");
+    }
+
     private void toggleVisibilityBasedOnSelection() {
         boolean isSelectExistingSelected = selectExistingRadio.isSelected();
         unitComboBox.setVisible(isSelectExistingSelected);
@@ -113,11 +122,13 @@ public UnitOfMeasurement getSelectedUnit() {
         return unitComboBox.getSelectionModel().getSelectedItem();
     }
 
-    public CreateUnitOfMeasurementDTO getNewUnitDTO() {
+    public CreateUnitOfMeasurementDTO getNewUnitDTO() throws ValidationException {
         CreateUnitOfMeasurementDTO unitDTO = new CreateUnitOfMeasurementDTO();
         unitDTO.setOrganizationId(organizationId);
-        unitDTO.setName(nameField.getText());
-        unitDTO.setUnitType(unitTypeField.getText());
+
+        unitDTO.setName(unitNameField.handleSubmit());
+        unitDTO.setUnitType(unitTypeField.handleSubmit());
+
         return unitDTO;
     }
 
diff --git a/src/main/resources/org/chainoptim/desktop/shared/common/uielements/SelectOrCreateLocationView.fxml b/src/main/resources/org/chainoptim/desktop/shared/common/uielements/SelectOrCreateLocationView.fxml
index 0739ce00..76be1e26 100644
--- a/src/main/resources/org/chainoptim/desktop/shared/common/uielements/SelectOrCreateLocationView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/shared/common/uielements/SelectOrCreateLocationView.fxml
@@ -5,6 +5,8 @@
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.control.TextField?>
 
+<?import org.chainoptim.desktop.shared.common.uielements.forms.FormField?>
+
 <VBox xmlns="http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
       fx:controller="org.chainoptim.desktop.shared.common.uielements.select.SelectOrCreateLocationController" spacing="16">
@@ -19,25 +21,18 @@
 
     <!-- Create New Location subform -->
     <VBox fx:id="createLocationForm" visible="false">
-        <Label text="Address" styleClass="form-label"/>
-        <TextField fx:id="addressField" styleClass="custom-text-field"/>
+        <FormField fx:id="addressField"/>
 
-        <Label text="City" styleClass="form-label"/>
-        <TextField fx:id="cityField" styleClass="custom-text-field"/>
+        <FormField fx:id="cityField"/>
 
-        <Label text="State" styleClass="form-label"/>
-        <TextField fx:id="stateField" styleClass="custom-text-field"/>
+        <FormField fx:id="stateField"/>
 
-        <Label text="Country" styleClass="form-label"/>
-        <TextField fx:id="countryField" styleClass="custom-text-field"/>
+        <FormField fx:id="countryField"/>
 
-        <Label text="Zip Code" styleClass="form-label"/>
-        <TextField fx:id="zipCodeField" styleClass="custom-text-field"/>
+        <FormField fx:id="zipCodeField"/>
 
-        <Label text="Latitude" styleClass="form-label"/>
-        <TextField fx:id="latitudeField" styleClass="custom-text-field"/>
+        <FormField fx:id="latitudeField"/>
 
-        <Label text="Longitude" styleClass="form-label"/>
-        <TextField fx:id="longitudeField" styleClass="custom-text-field"/>
+        <FormField fx:id="longitudeField"/>
     </VBox>
 </VBox>
diff --git a/src/main/resources/org/chainoptim/desktop/shared/common/uielements/SelectOrCreateUnitOfMeasurementView.fxml b/src/main/resources/org/chainoptim/desktop/shared/common/uielements/SelectOrCreateUnitOfMeasurementView.fxml
index cb0cf8d0..07b787bb 100644
--- a/src/main/resources/org/chainoptim/desktop/shared/common/uielements/SelectOrCreateUnitOfMeasurementView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/shared/common/uielements/SelectOrCreateUnitOfMeasurementView.fxml
@@ -5,6 +5,7 @@
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.control.TextField?>
 
+<?import org.chainoptim.desktop.shared.common.uielements.forms.FormField?>
 <VBox xmlns="http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
       fx:controller="org.chainoptim.desktop.shared.common.uielements.select.SelectOrCreateUnitOfMeasurementController" spacing="16">
@@ -19,10 +20,8 @@
 
     <!-- Create New Unit subform -->
     <VBox fx:id="createUnitForm" visible="false">
-        <Label text="Name *" styleClass="form-label"/>
-        <TextField fx:id="nameField" styleClass="custom-text-field"/>
+        <FormField fx:id="unitNameField"/>
 
-        <Label text="Unit Type" styleClass="form-label"/>
-        <TextField fx:id="unitTypeField" styleClass="custom-text-field"/>
+        <FormField fx:id="unitTypeField"/>
     </VBox>
 </VBox>

From 3d8d669d3552f6bf94a85f0f0786e66e6c83802d Mon Sep 17 00:00:00 2001
From: TudorOrban <130213626+TudorOrban@users.noreply.github.com>
Date: Thu, 25 Apr 2024 22:58:11 +0300
Subject: [PATCH 09/10] Finish refactoring forms for main features

---
 .../controller/CreateClientController.java    | 19 ++--
 .../controller/UpdateClientController.java    | 32 ++++---
 .../controller/CreateFactoryController.java   | 69 ++++++++++----
 .../controller/UpdateFactoryController.java   | 94 ++++++++++++-------
 .../controller/CreateProductController.java   |  2 +-
 .../controller/CreateSupplierController.java  | 84 +++++++++++------
 .../controller/UpdateSupplierController.java  | 89 ++++++++++++------
 .../controller/CreateWarehouseController.java | 86 +++++++++++------
 .../controller/UpdateWarehouseController.java | 94 ++++++++++++-------
 .../features/client/CreateClientView.fxml     |  4 +-
 .../features/client/UpdateClientView.fxml     |  4 +-
 .../features/factory/CreateFactoryView.fxml   |  6 +-
 .../features/factory/UpdateFactoryView.fxml   |  4 +-
 .../features/supplier/CreateSupplierView.fxml |  6 +-
 .../features/supplier/UpdateSupplierView.fxml | 27 +++---
 .../warehouse/CreateWarehouseView.fxml        |  4 +-
 .../warehouse/UpdateWarehouseView.fxml        |  4 +-
 17 files changed, 398 insertions(+), 230 deletions(-)

diff --git a/src/main/java/org/chainoptim/desktop/features/client/controller/CreateClientController.java b/src/main/java/org/chainoptim/desktop/features/client/controller/CreateClientController.java
index 56717f3f..d0fef093 100644
--- a/src/main/java/org/chainoptim/desktop/features/client/controller/CreateClientController.java
+++ b/src/main/java/org/chainoptim/desktop/features/client/controller/CreateClientController.java
@@ -28,6 +28,7 @@
 
 public class CreateClientController implements Initializable {
 
+    // Services
     private final ClientWriteService clientWriteService;
     private final NavigationService navigationService;
     private final CurrentSelectionService currentSelectionService;
@@ -35,8 +36,10 @@ public class CreateClientController implements Initializable {
     private final ToastManager toastManager;
     private final FallbackManager fallbackManager;
 
+    // Controllers
     private SelectOrCreateLocationController selectOrCreateLocationController;
 
+    // FXML
     @FXML
     private StackPane fallbackContainer;
     @FXML
@@ -44,14 +47,14 @@ public class CreateClientController implements Initializable {
     @FXML
     private FormField<String> nameFormField;
 
-
     @Inject
-    public CreateClientController(ClientWriteService clientWriteService,
-                                  NavigationService navigationService,
-                                  CurrentSelectionService currentSelectionService,
-                                  ToastManager toastManager,
-                                  FallbackManager fallbackManager,
-                                  CommonViewsLoader commonViewsLoader) {
+    public CreateClientController(
+            ClientWriteService clientWriteService,
+            NavigationService navigationService,
+            CurrentSelectionService currentSelectionService,
+            ToastManager toastManager,
+            FallbackManager fallbackManager,
+            CommonViewsLoader commonViewsLoader) {
         this.clientWriteService = clientWriteService;
         this.navigationService = navigationService;
         this.currentSelectionService = currentSelectionService;
@@ -112,13 +115,13 @@ private CreateClientDTO getCreateClientDTO(Integer organizationId) {
 
     private Result<Client> handleCreateClientResponse(Result<Client> result) {
         Platform.runLater(() -> {
+            fallbackManager.setLoading(false);
             if (result.getError() != null) {
                 toastManager.addToast(new ToastInfo(
                         "Error", "Failed to create client.", OperationOutcome.ERROR));
                 return;
             }
             Client client = result.getData();
-            fallbackManager.setLoading(false);
             toastManager.addToast(new ToastInfo(
                     "Success", "Client created successfully.", OperationOutcome.SUCCESS));
 
diff --git a/src/main/java/org/chainoptim/desktop/features/client/controller/UpdateClientController.java b/src/main/java/org/chainoptim/desktop/features/client/controller/UpdateClientController.java
index 681f24e7..1091b325 100644
--- a/src/main/java/org/chainoptim/desktop/features/client/controller/UpdateClientController.java
+++ b/src/main/java/org/chainoptim/desktop/features/client/controller/UpdateClientController.java
@@ -31,6 +31,7 @@
 
 public class UpdateClientController implements Initializable {
 
+    // Services
     private final ClientService clientService;
     private final ClientWriteService clientWriteService;
     private final NavigationService navigationService;
@@ -39,10 +40,13 @@ public class UpdateClientController implements Initializable {
     private final ToastManager toastManager;
     private final FallbackManager fallbackManager;
 
-    private Client client;
-
+    // Controllers
     private SelectOrCreateLocationController selectOrCreateLocationController;
 
+    // State
+    private Client client;
+
+    // FXML
     @FXML
     private StackPane fallbackContainer;
     @FXML
@@ -51,13 +55,14 @@ public class UpdateClientController implements Initializable {
     private FormField<String> nameFormField;
 
     @Inject
-    public UpdateClientController(ClientService clientService,
-                                  ClientWriteService clientWriteService,
-                                  NavigationService navigationService,
-                                  CurrentSelectionService currentSelectionService,
-                                  CommonViewsLoader commonViewsLoader,
-                                  ToastManager toastManager,
-                                  FallbackManager fallbackManager) {
+    public UpdateClientController(
+            ClientService clientService,
+            ClientWriteService clientWriteService,
+            NavigationService navigationService,
+            CurrentSelectionService currentSelectionService,
+            CommonViewsLoader commonViewsLoader,
+            ToastManager toastManager,
+            FallbackManager fallbackManager) {
         this.clientService = clientService;
         this.clientWriteService = clientWriteService;
         this.navigationService = navigationService;
@@ -71,7 +76,6 @@ public UpdateClientController(ClientService clientService,
     public void initialize(URL location, ResourceBundle resources) {
         commonViewsLoader.loadFallbackManager(fallbackContainer);
         selectOrCreateLocationController = commonViewsLoader.loadSelectOrCreateLocation(selectOrCreateLocationContainer);
-        selectOrCreateLocationController.initialize();
         loadClient(currentSelectionService.getSelectedId());
     }
 
@@ -87,15 +91,13 @@ private void loadClient(Integer clientId) {
     private Result<Client> handleClientResponse(Result<Client> result) {
         Platform.runLater(() -> {
             if (result.getError() != null) {
-                toastManager.addToast(new ToastInfo(
-                        "Error", "Failed to update client.", OperationOutcome.ERROR));
+                fallbackManager.setErrorMessage("Failed to load client.");
                 return;
             }
             client = result.getData();
             fallbackManager.setLoading(false);
 
             initializeFormFields();
-
             selectOrCreateLocationController.setSelectedLocation(client.getLocation());
         });
 
@@ -147,12 +149,14 @@ private UpdateClientDTO getUpdateClientDTO() {
 
     private Result<Client> handleUpdateClientResponse(Result<Client> result) {
         Platform.runLater(() -> {
+            fallbackManager.setLoading(false);
             if (result.getError() != null) {
                 toastManager.addToast(new ToastInfo(
                         "Error", "Failed to update client.", OperationOutcome.ERROR));
                 return;
             }
-            fallbackManager.setLoading(false);
+            toastManager.addToast(new ToastInfo(
+                    "Success", "Client updated successfully.", OperationOutcome.SUCCESS));
 
             // Manage navigation, invalidating previous client cache
             Client updatedClient = result.getData();
diff --git a/src/main/java/org/chainoptim/desktop/features/factory/controller/CreateFactoryController.java b/src/main/java/org/chainoptim/desktop/features/factory/controller/CreateFactoryController.java
index 952986f2..00d46bf3 100644
--- a/src/main/java/org/chainoptim/desktop/features/factory/controller/CreateFactoryController.java
+++ b/src/main/java/org/chainoptim/desktop/features/factory/controller/CreateFactoryController.java
@@ -7,16 +7,20 @@
 import org.chainoptim.desktop.features.factory.dto.CreateFactoryDTO;
 import org.chainoptim.desktop.features.factory.model.Factory;
 import org.chainoptim.desktop.features.factory.service.FactoryWriteService;
+import org.chainoptim.desktop.shared.common.uielements.forms.FormField;
+import org.chainoptim.desktop.shared.common.uielements.forms.ValidationException;
 import org.chainoptim.desktop.shared.common.uielements.select.SelectOrCreateLocationController;
+import org.chainoptim.desktop.shared.enums.OperationOutcome;
 import org.chainoptim.desktop.shared.fallback.FallbackManager;
 import org.chainoptim.desktop.shared.httphandling.Result;
+import org.chainoptim.desktop.shared.toast.controller.ToastManager;
+import org.chainoptim.desktop.shared.toast.model.ToastInfo;
 import org.chainoptim.desktop.shared.util.resourceloader.CommonViewsLoader;
 
 import com.google.inject.Inject;
 import javafx.application.Platform;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
-import javafx.scene.control.TextField;
 import javafx.scene.layout.StackPane;
 
 import java.net.URL;
@@ -24,20 +28,24 @@
 
 public class CreateFactoryController implements Initializable {
 
+    // Services
     private final FactoryWriteService factoryWriteService;
     private final NavigationService navigationService;
     private final CurrentSelectionService currentSelectionService;
     private final CommonViewsLoader commonViewsLoader;
+    private final ToastManager toastManager;
     private final FallbackManager fallbackManager;
 
+    // Controllers
     private SelectOrCreateLocationController selectOrCreateLocationController;
 
+    // FXML
     @FXML
     private StackPane fallbackContainer;
     @FXML
     private StackPane selectOrCreateLocationContainer;
     @FXML
-    private TextField nameField;
+    private FormField<String> nameFormField;
 
 
     @Inject
@@ -46,12 +54,14 @@ public CreateFactoryController(
             NavigationService navigationService,
             CurrentSelectionService currentSelectionService,
             CommonViewsLoader commonViewsLoader,
+            ToastManager toastManager,
             FallbackManager fallbackManager
     ) {
         this.factoryWriteService = factoryWriteService;
         this.navigationService = navigationService;
         this.currentSelectionService = currentSelectionService;
         this.commonViewsLoader = commonViewsLoader;
+        this.toastManager = toastManager;
         this.fallbackManager = fallbackManager;
     }
 
@@ -59,14 +69,16 @@ public CreateFactoryController(
     public void initialize(URL location, ResourceBundle resources) {
         commonViewsLoader.loadFallbackManager(fallbackContainer);
         selectOrCreateLocationController = commonViewsLoader.loadSelectOrCreateLocation(selectOrCreateLocationContainer);
-        selectOrCreateLocationController.initialize();
+
+        initializeFormFields();
+    }
+
+    private void initializeFormFields() {
+        nameFormField.initialize(String::new, "Name", true, null, "Your input is not valid");
     }
 
     @FXML
     private void handleSubmit() {
-        fallbackManager.reset();
-        fallbackManager.setLoading(true);
-
         User currentUser = TenantContext.getCurrentUser();
         if (currentUser == null) {
             return;
@@ -74,23 +86,29 @@ private void handleSubmit() {
         Integer organizationId = currentUser.getOrganization().getId();
 
         CreateFactoryDTO factoryDTO = getCreateFactoryDTO(organizationId);
+        if (factoryDTO == null) return;
+
+        fallbackManager.reset();
+        fallbackManager.setLoading(true);
 
         factoryWriteService.createFactory(factoryDTO)
                 .thenApply(this::handleCreateFactoryResponse)
-                .exceptionally(ex -> {
-                    ex.printStackTrace();
-                    return new Result<>();
-                });
+                .exceptionally(this::handleCreateClientException);
     }
 
     private Result<Factory> handleCreateFactoryResponse(Result<Factory> result) {
         Platform.runLater(() -> {
+            fallbackManager.setLoading(false);
+
             if (result.getError() != null) {
-                fallbackManager.setErrorMessage("Failed to create factory.");
+                toastManager.addToast(new ToastInfo(
+                        "Error", "Failed to create factory.", OperationOutcome.ERROR));
                 return;
             }
             Factory factory = result.getData();
-            fallbackManager.setLoading(false);
+            toastManager.addToast(new ToastInfo(
+                    "Success", "Factory created successfully.", OperationOutcome.SUCCESS));
+
             currentSelectionService.setSelectedId(factory.getId());
             navigationService.switchView("Factory?id=" + factory.getId(), true);
         });
@@ -99,17 +117,28 @@ private Result<Factory> handleCreateFactoryResponse(Result<Factory> result) {
 
     private CreateFactoryDTO getCreateFactoryDTO(Integer organizationId) {
         CreateFactoryDTO factoryDTO = new CreateFactoryDTO();
-        factoryDTO.setName(nameField.getText());
-        factoryDTO.setOrganizationId(organizationId);
-        if (selectOrCreateLocationController.isCreatingNewLocation()) {
-            factoryDTO.setCreateLocation(true);
-            factoryDTO.setLocation(selectOrCreateLocationController.getNewLocationDTO());
-        } else {
-            factoryDTO.setCreateLocation(false);
-            factoryDTO.setLocationId(selectOrCreateLocationController.getSelectedLocation().getId());
+        try {
+            factoryDTO.setName(nameFormField.handleSubmit());
+            factoryDTO.setOrganizationId(organizationId);
+            if (selectOrCreateLocationController.isCreatingNewLocation()) {
+                factoryDTO.setCreateLocation(true);
+                factoryDTO.setLocation(selectOrCreateLocationController.getNewLocationDTO());
+            } else {
+                factoryDTO.setCreateLocation(false);
+                factoryDTO.setLocationId(selectOrCreateLocationController.getSelectedLocation().getId());
+            }
+        } catch (ValidationException e) {
+            return null;
         }
 
         return factoryDTO;
     }
+
+    private Result<Factory> handleCreateClientException(Throwable ex) {
+        Platform.runLater(() ->
+                toastManager.addToast(new ToastInfo(
+                        "Error", "Failed to create factory.", OperationOutcome.ERROR)));
+        return new Result<>();
+    }
 }
 
diff --git a/src/main/java/org/chainoptim/desktop/features/factory/controller/UpdateFactoryController.java b/src/main/java/org/chainoptim/desktop/features/factory/controller/UpdateFactoryController.java
index 50936286..1f6a32af 100644
--- a/src/main/java/org/chainoptim/desktop/features/factory/controller/UpdateFactoryController.java
+++ b/src/main/java/org/chainoptim/desktop/features/factory/controller/UpdateFactoryController.java
@@ -7,41 +7,49 @@
 import org.chainoptim.desktop.features.factory.model.Factory;
 import org.chainoptim.desktop.features.factory.service.FactoryService;
 import org.chainoptim.desktop.features.factory.service.FactoryWriteService;
+import org.chainoptim.desktop.shared.common.uielements.forms.FormField;
+import org.chainoptim.desktop.shared.common.uielements.forms.ValidationException;
 import org.chainoptim.desktop.shared.common.uielements.select.SelectOrCreateLocationController;
+import org.chainoptim.desktop.shared.enums.OperationOutcome;
 import org.chainoptim.desktop.shared.fallback.FallbackManager;
 import org.chainoptim.desktop.shared.httphandling.Result;
+import org.chainoptim.desktop.shared.toast.controller.ToastManager;
+import org.chainoptim.desktop.shared.toast.model.ToastInfo;
 import org.chainoptim.desktop.shared.util.resourceloader.CommonViewsLoader;
 
 import com.google.inject.Inject;
 import javafx.fxml.Initializable;
 import javafx.application.Platform;
 import javafx.fxml.FXML;
-import javafx.scene.control.TextField;
 import javafx.scene.layout.StackPane;
 
 import java.net.URL;
-import java.util.Optional;
 import java.util.ResourceBundle;
 
 public class UpdateFactoryController implements Initializable {
 
+    // Services
     private final FactoryService factoryService;
     private final FactoryWriteService factoryWriteService;
     private final NavigationService navigationService;
     private final CurrentSelectionService currentSelectionService;
     private final CommonViewsLoader commonViewsLoader;
+    private final ToastManager toastManager;
     private final FallbackManager fallbackManager;
 
-    private Factory factory;
-
+    // Controllers
     private SelectOrCreateLocationController selectOrCreateLocationController;
 
+    // State
+    private Factory factory;
+
+    // FXML
     @FXML
     private StackPane fallbackContainer;
     @FXML
     private StackPane selectOrCreateLocationContainer;
     @FXML
-    private TextField nameField;
+    private FormField<String> nameFormField;
 
     @Inject
     public UpdateFactoryController(
@@ -49,13 +57,15 @@ public UpdateFactoryController(
             FactoryWriteService factoryWriteService,
             NavigationService navigationService,
             CurrentSelectionService currentSelectionService,
-            FallbackManager fallbackManager,
-            CommonViewsLoader commonViewsLoader
+            CommonViewsLoader commonViewsLoader,
+            ToastManager toastManager,
+            FallbackManager fallbackManager
     ) {
         this.factoryService = factoryService;
         this.factoryWriteService = factoryWriteService;
         this.navigationService = navigationService;
         this.currentSelectionService = currentSelectionService;
+        this.toastManager = toastManager;
         this.commonViewsLoader = commonViewsLoader;
         this.fallbackManager = fallbackManager;
     }
@@ -63,8 +73,7 @@ public UpdateFactoryController(
     @FXML
     public void initialize(URL location, ResourceBundle resources) {
         commonViewsLoader.loadFallbackManager(fallbackContainer);
-        selectOrCreateLocationController =commonViewsLoader.loadSelectOrCreateLocation(selectOrCreateLocationContainer);
-        selectOrCreateLocationController.initialize();
+        selectOrCreateLocationController = commonViewsLoader.loadSelectOrCreateLocation(selectOrCreateLocationContainer);
         loadFactory(currentSelectionService.getSelectedId());
     }
 
@@ -74,8 +83,7 @@ private void loadFactory(Integer factoryId) {
 
         factoryService.getFactoryById(factoryId)
                 .thenApply(this::handleFactoryResponse)
-                .exceptionally(this::handleFactoryException)
-                .thenRun(() -> Platform.runLater(() -> fallbackManager.setLoading(false)));
+                .exceptionally(this::handleFactoryException);
     }
 
     private Result<Factory> handleFactoryResponse(Result<Factory> result) {
@@ -85,8 +93,9 @@ private Result<Factory> handleFactoryResponse(Result<Factory> result) {
                 return;
             }
             factory = result.getData();
+            fallbackManager.setLoading(false);
 
-            nameField.setText(factory.getName());
+            initializeFormFields();
             selectOrCreateLocationController.setSelectedLocation(factory.getLocation());
         });
         return result;
@@ -97,29 +106,54 @@ private Result<Factory> handleFactoryException(Throwable ex) {
         return new Result<>();
     }
 
+    private void initializeFormFields() {
+        nameFormField.initialize(String::new, "Name", true, factory.getName(), "Your input is not valid");
+    }
+
     @FXML
     private void handleSubmit() {
-        fallbackManager.reset();
-        fallbackManager.setLoading(true);
-
         UpdateFactoryDTO factoryDTO = getUpdateFactoryDTO();
+        if (factoryDTO == null) return;
         System.out.println(factoryDTO);
 
+        fallbackManager.reset();
+        fallbackManager.setLoading(true);
+
         factoryWriteService.updateFactory(factoryDTO)
                 .thenApply(this::handleUpdateFactoryResponse)
-                .exceptionally(ex -> {
-                    ex.printStackTrace();
-                    return new Result<>();
-                });
+                .exceptionally(this::handleUpdateFactoryException);
+    }
+
+    private UpdateFactoryDTO getUpdateFactoryDTO() {
+        UpdateFactoryDTO factoryDTO = new UpdateFactoryDTO();
+        try {
+            factoryDTO.setId(factory.getId());
+            factoryDTO.setName(nameFormField.handleSubmit());
+
+            if (selectOrCreateLocationController.isCreatingNewLocation()) {
+                factoryDTO.setCreateLocation(true);
+                factoryDTO.setLocation(selectOrCreateLocationController.getNewLocationDTO());
+            } else {
+                factoryDTO.setCreateLocation(false);
+                factoryDTO.setLocationId(selectOrCreateLocationController.getSelectedLocation().getId());
+            }
+        } catch (ValidationException e) {
+            return null;
+        }
+
+        return factoryDTO;
     }
 
     private Result<Factory> handleUpdateFactoryResponse(Result<Factory> result) {
         Platform.runLater(() -> {
+            fallbackManager.setLoading(false);
             if (result.getError() != null) {
-                fallbackManager.setErrorMessage("Failed to create factory.");
+                toastManager.addToast(new ToastInfo(
+                        "Error", "Failed to update factory.", OperationOutcome.ERROR));
                 return;
             }
-            fallbackManager.setLoading(false);
+            toastManager.addToast(new ToastInfo(
+                    "Success", "Factory updated successfully.", OperationOutcome.SUCCESS));
 
             // Manage navigation, invalidating previous factory cache
             Factory updatedFactory = result.getData();
@@ -131,20 +165,10 @@ private Result<Factory> handleUpdateFactoryResponse(Result<Factory> result) {
         return result;
     }
 
-    private UpdateFactoryDTO getUpdateFactoryDTO() {
-        UpdateFactoryDTO factoryDTO = new UpdateFactoryDTO();
-        factoryDTO.setId(factory.getId());
-        factoryDTO.setName(nameField.getText());
-
-        if (selectOrCreateLocationController.isCreatingNewLocation()) {
-            factoryDTO.setCreateLocation(true);
-            factoryDTO.setLocation(selectOrCreateLocationController.getNewLocationDTO());
-        } else {
-            factoryDTO.setCreateLocation(false);
-            factoryDTO.setLocationId(selectOrCreateLocationController.getSelectedLocation().getId());
-        }
-
-        return factoryDTO;
+    private Result<Factory> handleUpdateFactoryException(Throwable ex) {
+        Platform.runLater(() -> toastManager.addToast(new ToastInfo(
+                "An error occurred.", "Failed to update factory.", OperationOutcome.ERROR)));
+        return new Result<>();
     }
 }
 
diff --git a/src/main/java/org/chainoptim/desktop/features/product/controller/CreateProductController.java b/src/main/java/org/chainoptim/desktop/features/product/controller/CreateProductController.java
index fb5757d7..e8e52079 100644
--- a/src/main/java/org/chainoptim/desktop/features/product/controller/CreateProductController.java
+++ b/src/main/java/org/chainoptim/desktop/features/product/controller/CreateProductController.java
@@ -34,8 +34,8 @@ public class CreateProductController {
     private final NavigationService navigationService;
     private final CurrentSelectionService currentSelectionService;
     private final CommonViewsLoader commonViewsLoader;
-    private final FallbackManager fallbackManager;
     private final ToastManager toastManager;
+    private final FallbackManager fallbackManager;
 
     private SelectOrCreateUnitOfMeasurementController unitOfMeasurementController;
 
diff --git a/src/main/java/org/chainoptim/desktop/features/supplier/controller/CreateSupplierController.java b/src/main/java/org/chainoptim/desktop/features/supplier/controller/CreateSupplierController.java
index 269c48ca..ccc15b73 100644
--- a/src/main/java/org/chainoptim/desktop/features/supplier/controller/CreateSupplierController.java
+++ b/src/main/java/org/chainoptim/desktop/features/supplier/controller/CreateSupplierController.java
@@ -7,16 +7,20 @@
 import org.chainoptim.desktop.features.supplier.dto.CreateSupplierDTO;
 import org.chainoptim.desktop.features.supplier.model.Supplier;
 import org.chainoptim.desktop.features.supplier.service.SupplierWriteService;
+import org.chainoptim.desktop.shared.common.uielements.forms.FormField;
+import org.chainoptim.desktop.shared.common.uielements.forms.ValidationException;
 import org.chainoptim.desktop.shared.common.uielements.select.SelectOrCreateLocationController;
+import org.chainoptim.desktop.shared.enums.OperationOutcome;
 import org.chainoptim.desktop.shared.fallback.FallbackManager;
 import org.chainoptim.desktop.shared.httphandling.Result;
+import org.chainoptim.desktop.shared.toast.controller.ToastManager;
+import org.chainoptim.desktop.shared.toast.model.ToastInfo;
 import org.chainoptim.desktop.shared.util.resourceloader.CommonViewsLoader;
 
 import com.google.inject.Inject;
 import javafx.application.Platform;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
-import javafx.scene.control.TextField;
 import javafx.scene.layout.StackPane;
 
 import java.net.URL;
@@ -24,33 +28,38 @@
 
 public class CreateSupplierController implements Initializable {
 
+    // Services
     private final SupplierWriteService supplierWriteService;
     private final NavigationService navigationService;
     private final CurrentSelectionService currentSelectionService;
     private final CommonViewsLoader commonViewsLoader;
+    private final ToastManager toastManager;
     private final FallbackManager fallbackManager;
 
+    // Controllers
     private SelectOrCreateLocationController selectOrCreateLocationController;
 
+    // FXML
     @FXML
     private StackPane fallbackContainer;
     @FXML
     private StackPane selectOrCreateLocationContainer;
     @FXML
-    private TextField nameField;
-
+    private FormField<String> nameFormField;
+    
     @Inject
     public CreateSupplierController(
             SupplierWriteService supplierWriteService,
             NavigationService navigationService,
             CurrentSelectionService currentSelectionService,
+            ToastManager toastManager,
             FallbackManager fallbackManager,
-            CommonViewsLoader commonViewsLoader
-    ) {
+            CommonViewsLoader commonViewsLoader) {
         this.supplierWriteService = supplierWriteService;
         this.navigationService = navigationService;
         this.currentSelectionService = currentSelectionService;
         this.commonViewsLoader = commonViewsLoader;
+        this.toastManager = toastManager;
         this.fallbackManager = fallbackManager;
     }
 
@@ -58,14 +67,16 @@ public CreateSupplierController(
     public void initialize(URL location, ResourceBundle resources) {
         commonViewsLoader.loadFallbackManager(fallbackContainer);
         selectOrCreateLocationController = commonViewsLoader.loadSelectOrCreateLocation(selectOrCreateLocationContainer);
-        selectOrCreateLocationController.initialize();
+
+        initializeFormFields();
+    }
+
+    private void initializeFormFields() {
+        nameFormField.initialize(String::new, "Name", true, null, "Your input is not valid.");
     }
 
     @FXML
     private void handleSubmit() {
-        fallbackManager.reset();
-        fallbackManager.setLoading(true);
-
         User currentUser = TenantContext.getCurrentUser();
         if (currentUser == null) {
             return;
@@ -73,42 +84,59 @@ private void handleSubmit() {
         Integer organizationId = currentUser.getOrganization().getId();
 
         CreateSupplierDTO supplierDTO = getCreateSupplierDTO(organizationId);
+        if (supplierDTO == null) return;
+
+        fallbackManager.reset();
+        fallbackManager.setLoading(true);
 
         supplierWriteService.createSupplier(supplierDTO)
                 .thenApply(this::handleCreateSupplierResponse)
-                .exceptionally(ex -> {
-                    ex.printStackTrace();
-                    return new Result<>();
-                });
+                .exceptionally(this::handleCreateSupplierException);
+    }
+
+    private CreateSupplierDTO getCreateSupplierDTO(Integer organizationId) {
+        CreateSupplierDTO supplierDTO = new CreateSupplierDTO();
+        try {
+            supplierDTO.setName(nameFormField.handleSubmit());
+            supplierDTO.setOrganizationId(organizationId);
+            if (selectOrCreateLocationController.isCreatingNewLocation()) {
+                supplierDTO.setCreateLocation(true);
+                supplierDTO.setLocation(selectOrCreateLocationController.getNewLocationDTO());
+            } else {
+                supplierDTO.setCreateLocation(false);
+                supplierDTO.setLocationId(selectOrCreateLocationController.getSelectedLocation().getId());
+            }
+        } catch (ValidationException e) {
+            return null;
+        }
+
+        return supplierDTO;
     }
 
     private Result<Supplier> handleCreateSupplierResponse(Result<Supplier> result) {
         Platform.runLater(() -> {
+            fallbackManager.setLoading(false);
             if (result.getError() != null) {
-                fallbackManager.setErrorMessage("Failed to create supplier.");
+                toastManager.addToast(new ToastInfo(
+                        "Error", "Failed to create supplier.", OperationOutcome.ERROR));
                 return;
             }
             Supplier supplier = result.getData();
-            fallbackManager.setLoading(false);
+            toastManager.addToast(new ToastInfo(
+                    "Success", "Supplier created successfully.", OperationOutcome.SUCCESS));
+
             currentSelectionService.setSelectedId(supplier.getId());
             navigationService.switchView("Supplier?id=" + supplier.getId(), true);
         });
         return result;
     }
 
-    private CreateSupplierDTO getCreateSupplierDTO(Integer organizationId) {
-        CreateSupplierDTO supplierDTO = new CreateSupplierDTO();
-        supplierDTO.setOrganizationId(organizationId);
-        supplierDTO.setName(nameField.getText());
-        if (selectOrCreateLocationController.isCreatingNewLocation()) {
-            supplierDTO.setCreateLocation(true);
-            supplierDTO.setLocation(selectOrCreateLocationController.getNewLocationDTO());
-        } else {
-            supplierDTO.setCreateLocation(false);
-            supplierDTO.setLocationId(selectOrCreateLocationController.getSelectedLocation().getId());
-        }
-
-        return supplierDTO;
+    private Result<Supplier> handleCreateSupplierException(Throwable ex) {
+        Platform.runLater(() ->
+            toastManager.addToast(new ToastInfo(
+                    "Error", "Failed to create supplier.", OperationOutcome.ERROR)));
+        return new Result<>();
     }
+
 }
 
diff --git a/src/main/java/org/chainoptim/desktop/features/supplier/controller/UpdateSupplierController.java b/src/main/java/org/chainoptim/desktop/features/supplier/controller/UpdateSupplierController.java
index 6923c9bf..df99247b 100644
--- a/src/main/java/org/chainoptim/desktop/features/supplier/controller/UpdateSupplierController.java
+++ b/src/main/java/org/chainoptim/desktop/features/supplier/controller/UpdateSupplierController.java
@@ -7,39 +7,49 @@
 import org.chainoptim.desktop.features.supplier.model.Supplier;
 import org.chainoptim.desktop.features.supplier.service.SupplierService;
 import org.chainoptim.desktop.features.supplier.service.SupplierWriteService;
+import org.chainoptim.desktop.shared.common.uielements.forms.FormField;
+import org.chainoptim.desktop.shared.common.uielements.forms.ValidationException;
 import org.chainoptim.desktop.shared.common.uielements.select.SelectOrCreateLocationController;
+import org.chainoptim.desktop.shared.enums.OperationOutcome;
 import org.chainoptim.desktop.shared.fallback.FallbackManager;
 import org.chainoptim.desktop.shared.httphandling.Result;
+import org.chainoptim.desktop.shared.toast.controller.ToastManager;
+import org.chainoptim.desktop.shared.toast.model.ToastInfo;
 import org.chainoptim.desktop.shared.util.resourceloader.CommonViewsLoader;
 
 import com.google.inject.Inject;
 import javafx.application.Platform;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
-import javafx.scene.control.TextField;
 import javafx.scene.layout.StackPane;
+
 import java.net.URL;
 import java.util.ResourceBundle;
 
 public class UpdateSupplierController implements Initializable {
 
+    // Services
     private final SupplierService supplierService;
     private final SupplierWriteService supplierWriteService;
     private final NavigationService navigationService;
     private final CurrentSelectionService currentSelectionService;
     private final CommonViewsLoader commonViewsLoader;
+    private final ToastManager toastManager;
     private final FallbackManager fallbackManager;
 
-    private Supplier supplier;
-
+    // Controllers
     private SelectOrCreateLocationController selectOrCreateLocationController;
 
+    // State
+    private Supplier supplier;
+
+    // FXML
     @FXML
     private StackPane fallbackContainer;
     @FXML
     private StackPane selectOrCreateLocationContainer;
     @FXML
-    private TextField nameField;
+    private FormField<String> nameFormField;
 
     @Inject
     public UpdateSupplierController(
@@ -47,14 +57,15 @@ public UpdateSupplierController(
             SupplierWriteService supplierWriteService,
             NavigationService navigationService,
             CurrentSelectionService currentSelectionService,
-            FallbackManager fallbackManager,
-            CommonViewsLoader commonViewsLoader
-    ) {
+            CommonViewsLoader commonViewsLoader,
+            ToastManager toastManager,
+            FallbackManager fallbackManager) {
         this.supplierService = supplierService;
         this.supplierWriteService = supplierWriteService;
         this.navigationService = navigationService;
         this.currentSelectionService = currentSelectionService;
         this.commonViewsLoader = commonViewsLoader;
+        this.toastManager = toastManager;
         this.fallbackManager = fallbackManager;
     }
 
@@ -62,7 +73,6 @@ public UpdateSupplierController(
     public void initialize(URL location, ResourceBundle resources) {
         commonViewsLoader.loadFallbackManager(fallbackContainer);
         selectOrCreateLocationController = commonViewsLoader.loadSelectOrCreateLocation(selectOrCreateLocationContainer);
-        selectOrCreateLocationController.initialize();
         loadSupplier(currentSelectionService.getSelectedId());
     }
 
@@ -84,9 +94,10 @@ private Result<Supplier> handleSupplierResponse(Result<Supplier> result) {
             supplier = result.getData();
             fallbackManager.setLoading(false);
 
-            nameField.setText(supplier.getName());
+            initializeFormFields();
             selectOrCreateLocationController.setSelectedLocation(supplier.getLocation());
         });
+
         return result;
     }
 
@@ -95,28 +106,54 @@ private Result<Supplier> handleSupplierException(Throwable ex) {
         return new Result<>();
     }
 
+    private void initializeFormFields() {
+        nameFormField.initialize(String::new, "Name", true, supplier.getName(), "Your input is not valid");
+    }
+
     @FXML
     private void handleSubmit() {
+        UpdateSupplierDTO supplierDTO = getUpdateSupplierDTO();
+        if (supplierDTO == null) return;
+        System.out.println(supplierDTO);
+
         fallbackManager.reset();
         fallbackManager.setLoading(true);
 
-        UpdateSupplierDTO supplierDTO = getUpdateSupplierDTO();
-
         supplierWriteService.updateSupplier(supplierDTO)
                 .thenApply(this::handleUpdateSupplierResponse)
-                .exceptionally(ex -> {
-                    ex.printStackTrace();
-                    return new Result<>();
-                });
+                .exceptionally(this::handleUpdateSupplierException);
+    }
+
+    private UpdateSupplierDTO getUpdateSupplierDTO() {
+        UpdateSupplierDTO supplierDTO = new UpdateSupplierDTO();
+        supplierDTO.setId(supplier.getId());
+        try {
+            supplierDTO.setName(nameFormField.handleSubmit());
+
+            if (selectOrCreateLocationController.isCreatingNewLocation()) {
+                supplierDTO.setCreateLocation(true);
+                supplierDTO.setLocation(selectOrCreateLocationController.getNewLocationDTO());
+            } else {
+                supplierDTO.setCreateLocation(false);
+                supplierDTO.setLocationId(selectOrCreateLocationController.getSelectedLocation().getId());
+            }
+        } catch (ValidationException e) {
+            return null;
+        }
+
+        return supplierDTO;
     }
 
     private Result<Supplier> handleUpdateSupplierResponse(Result<Supplier> result) {
         Platform.runLater(() -> {
+            fallbackManager.setLoading(false);
             if (result.getError() != null) {
-                fallbackManager.setErrorMessage("Failed to create supplier.");
+                toastManager.addToast(new ToastInfo(
+                        "Error", "Failed to update supplier.", OperationOutcome.ERROR));
                 return;
             }
-            fallbackManager.setLoading(false);
+            toastManager.addToast(new ToastInfo(
+                    "Success", "Supplier updated successfully.", OperationOutcome.SUCCESS));
 
             // Manage navigation, invalidating previous supplier cache
             Supplier updatedSupplier = result.getData();
@@ -128,20 +165,10 @@ private Result<Supplier> handleUpdateSupplierResponse(Result<Supplier> result) {
         return result;
     }
 
-    private UpdateSupplierDTO getUpdateSupplierDTO() {
-        UpdateSupplierDTO supplierDTO = new UpdateSupplierDTO();
-        supplierDTO.setId(supplier.getId());
-        supplierDTO.setName(nameField.getText());
-
-        if (selectOrCreateLocationController.isCreatingNewLocation()) {
-            supplierDTO.setCreateLocation(true);
-            supplierDTO.setLocation(selectOrCreateLocationController.getNewLocationDTO());
-        } else {
-            supplierDTO.setCreateLocation(false);
-            supplierDTO.setLocationId(selectOrCreateLocationController.getSelectedLocation().getId());
-        }
-
-        return supplierDTO;
+    private Result<Supplier> handleUpdateSupplierException(Throwable ex) {
+        Platform.runLater(() -> toastManager.addToast(new ToastInfo(
+                "An error occurred.", "Failed to update supplier.", OperationOutcome.ERROR)));
+        return new Result<>();
     }
 }
 
diff --git a/src/main/java/org/chainoptim/desktop/features/warehouse/controller/CreateWarehouseController.java b/src/main/java/org/chainoptim/desktop/features/warehouse/controller/CreateWarehouseController.java
index 07a1170e..25eddca6 100644
--- a/src/main/java/org/chainoptim/desktop/features/warehouse/controller/CreateWarehouseController.java
+++ b/src/main/java/org/chainoptim/desktop/features/warehouse/controller/CreateWarehouseController.java
@@ -7,16 +7,20 @@
 import org.chainoptim.desktop.features.warehouse.dto.CreateWarehouseDTO;
 import org.chainoptim.desktop.features.warehouse.model.Warehouse;
 import org.chainoptim.desktop.features.warehouse.service.WarehouseWriteService;
+import org.chainoptim.desktop.shared.common.uielements.forms.FormField;
+import org.chainoptim.desktop.shared.common.uielements.forms.ValidationException;
 import org.chainoptim.desktop.shared.common.uielements.select.SelectOrCreateLocationController;
+import org.chainoptim.desktop.shared.enums.OperationOutcome;
 import org.chainoptim.desktop.shared.fallback.FallbackManager;
 import org.chainoptim.desktop.shared.httphandling.Result;
+import org.chainoptim.desktop.shared.toast.controller.ToastManager;
+import org.chainoptim.desktop.shared.toast.model.ToastInfo;
 import org.chainoptim.desktop.shared.util.resourceloader.CommonViewsLoader;
 
 import com.google.inject.Inject;
 import javafx.application.Platform;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
-import javafx.scene.control.TextField;
 import javafx.scene.layout.StackPane;
 
 import java.net.URL;
@@ -24,35 +28,38 @@
 
 public class CreateWarehouseController implements Initializable {
 
+    // Services
     private final WarehouseWriteService warehouseWriteService;
     private final NavigationService navigationService;
     private final CurrentSelectionService currentSelectionService;
     private final CommonViewsLoader commonViewsLoader;
+    private final ToastManager toastManager;
     private final FallbackManager fallbackManager;
 
+    // Controllers
     private SelectOrCreateLocationController selectOrCreateLocationController;
 
+    // FXML
     @FXML
     private StackPane fallbackContainer;
     @FXML
     private StackPane selectOrCreateLocationContainer;
     @FXML
-    private TextField nameField;
-    @FXML
-    private TextField locationIdField;
+    private FormField<String> nameFormField;
 
     @Inject
     public CreateWarehouseController(
             WarehouseWriteService warehouseWriteService,
             NavigationService navigationService,
             CurrentSelectionService currentSelectionService,
+            ToastManager toastManager,
             FallbackManager fallbackManager,
-            CommonViewsLoader commonViewsLoader
-    ) {
+            CommonViewsLoader commonViewsLoader) {
         this.warehouseWriteService = warehouseWriteService;
         this.navigationService = navigationService;
         this.currentSelectionService = currentSelectionService;
         this.commonViewsLoader = commonViewsLoader;
+        this.toastManager = toastManager;
         this.fallbackManager = fallbackManager;
     }
 
@@ -60,14 +67,16 @@ public CreateWarehouseController(
     public void initialize(URL location, ResourceBundle resources) {
         commonViewsLoader.loadFallbackManager(fallbackContainer);
         selectOrCreateLocationController = commonViewsLoader.loadSelectOrCreateLocation(selectOrCreateLocationContainer);
-        selectOrCreateLocationController.initialize();
+
+        initializeFormFields();
+    }
+
+    private void initializeFormFields() {
+        nameFormField.initialize(String::new, "Name", true, null, "Your input is not valid.");
     }
 
     @FXML
     private void handleSubmit() {
-        fallbackManager.reset();
-        fallbackManager.setLoading(true);
-
         User currentUser = TenantContext.getCurrentUser();
         if (currentUser == null) {
             return;
@@ -75,42 +84,59 @@ private void handleSubmit() {
         Integer organizationId = currentUser.getOrganization().getId();
 
         CreateWarehouseDTO warehouseDTO = getCreateWarehouseDTO(organizationId);
-        System.out.println(warehouseDTO);
+        if (warehouseDTO == null) return;
+
+        fallbackManager.reset();
+        fallbackManager.setLoading(true);
 
         warehouseWriteService.createWarehouse(warehouseDTO)
                 .thenApply(this::handleCreateWarehouseResponse)
-                .exceptionally(ex -> {
-                    ex.printStackTrace();
-                    return new Result<>();
-                });
+                .exceptionally(this::handleCreateWarehouseException);
+    }
+
+    private CreateWarehouseDTO getCreateWarehouseDTO(Integer organizationId) {
+        CreateWarehouseDTO warehouseDTO = new CreateWarehouseDTO();
+        try {
+            warehouseDTO.setName(nameFormField.handleSubmit());
+            warehouseDTO.setOrganizationId(organizationId);
+            if (selectOrCreateLocationController.isCreatingNewLocation()) {
+                warehouseDTO.setCreateLocation(true);
+                warehouseDTO.setLocation(selectOrCreateLocationController.getNewLocationDTO());
+            } else {
+                warehouseDTO.setCreateLocation(false);
+                warehouseDTO.setLocationId(selectOrCreateLocationController.getSelectedLocation().getId());
+            }
+        } catch (ValidationException e) {
+            return null;
+        }
+
+        return warehouseDTO;
     }
 
     private Result<Warehouse> handleCreateWarehouseResponse(Result<Warehouse> result) {
         Platform.runLater(() -> {
+            fallbackManager.setLoading(false);
             if (result.getError() != null) {
-                fallbackManager.setErrorMessage("Failed to create new warehouse.");
+                toastManager.addToast(new ToastInfo(
+                        "Error", "Failed to create warehouse.", OperationOutcome.ERROR));
                 return;
             }
             Warehouse warehouse = result.getData();
-            fallbackManager.setLoading(false);
+            toastManager.addToast(new ToastInfo(
+                    "Success", "Warehouse created successfully.", OperationOutcome.SUCCESS));
+
             currentSelectionService.setSelectedId(warehouse.getId());
             navigationService.switchView("Warehouse?id=" + warehouse.getId(), true);
         });
         return result;
     }
 
-    private CreateWarehouseDTO getCreateWarehouseDTO(Integer organizationId) {
-        CreateWarehouseDTO warehouseDTO = new CreateWarehouseDTO();
-        warehouseDTO.setName(nameField.getText());
-        warehouseDTO.setOrganizationId(organizationId);
-        if (selectOrCreateLocationController.isCreatingNewLocation()) {
-            warehouseDTO.setCreateLocation(true);
-            warehouseDTO.setLocation(selectOrCreateLocationController.getNewLocationDTO());
-        } else {
-            warehouseDTO.setCreateLocation(false);
-            warehouseDTO.setLocationId(selectOrCreateLocationController.getSelectedLocation().getId());
-        }
-
-        return warehouseDTO;
+    private Result<Warehouse> handleCreateWarehouseException(Throwable ex) {
+        Platform.runLater(() ->
+            toastManager.addToast(new ToastInfo(
+                    "Error", "Failed to create warehouse.", OperationOutcome.ERROR)));
+        return new Result<>();
     }
+
 }
+
diff --git a/src/main/java/org/chainoptim/desktop/features/warehouse/controller/UpdateWarehouseController.java b/src/main/java/org/chainoptim/desktop/features/warehouse/controller/UpdateWarehouseController.java
index 03dff155..77273548 100644
--- a/src/main/java/org/chainoptim/desktop/features/warehouse/controller/UpdateWarehouseController.java
+++ b/src/main/java/org/chainoptim/desktop/features/warehouse/controller/UpdateWarehouseController.java
@@ -7,41 +7,49 @@
 import org.chainoptim.desktop.features.warehouse.model.Warehouse;
 import org.chainoptim.desktop.features.warehouse.service.WarehouseService;
 import org.chainoptim.desktop.features.warehouse.service.WarehouseWriteService;
+import org.chainoptim.desktop.shared.common.uielements.forms.FormField;
+import org.chainoptim.desktop.shared.common.uielements.forms.ValidationException;
 import org.chainoptim.desktop.shared.common.uielements.select.SelectOrCreateLocationController;
+import org.chainoptim.desktop.shared.enums.OperationOutcome;
 import org.chainoptim.desktop.shared.fallback.FallbackManager;
 import org.chainoptim.desktop.shared.httphandling.Result;
+import org.chainoptim.desktop.shared.toast.controller.ToastManager;
+import org.chainoptim.desktop.shared.toast.model.ToastInfo;
 import org.chainoptim.desktop.shared.util.resourceloader.CommonViewsLoader;
 
 import com.google.inject.Inject;
 import javafx.application.Platform;
 import javafx.fxml.FXML;
 import javafx.fxml.Initializable;
-import javafx.scene.control.TextField;
 import javafx.scene.layout.StackPane;
 
 import java.net.URL;
-import java.util.Optional;
 import java.util.ResourceBundle;
 
 public class UpdateWarehouseController implements Initializable {
 
+    // Services
     private final WarehouseService warehouseService;
     private final WarehouseWriteService warehouseWriteService;
     private final NavigationService navigationService;
     private final CurrentSelectionService currentSelectionService;
     private final CommonViewsLoader commonViewsLoader;
+    private final ToastManager toastManager;
     private final FallbackManager fallbackManager;
 
-    private Warehouse warehouse;
-
+    // Controllers
     private SelectOrCreateLocationController selectOrCreateLocationController;
 
+    // State
+    private Warehouse warehouse;
+
+    // FXML
     @FXML
     private StackPane fallbackContainer;
     @FXML
     private StackPane selectOrCreateLocationContainer;
     @FXML
-    private TextField nameField;
+    private FormField<String> nameFormField;
 
     @Inject
     public UpdateWarehouseController(
@@ -49,14 +57,15 @@ public UpdateWarehouseController(
             WarehouseWriteService warehouseWriteService,
             NavigationService navigationService,
             CurrentSelectionService currentSelectionService,
-            FallbackManager fallbackManager,
-            CommonViewsLoader commonViewsLoader
-    ) {
+            CommonViewsLoader commonViewsLoader,
+            ToastManager toastManager,
+            FallbackManager fallbackManager) {
         this.warehouseService = warehouseService;
         this.warehouseWriteService = warehouseWriteService;
         this.navigationService = navigationService;
         this.currentSelectionService = currentSelectionService;
         this.commonViewsLoader = commonViewsLoader;
+        this.toastManager = toastManager;
         this.fallbackManager = fallbackManager;
     }
 
@@ -64,7 +73,6 @@ public UpdateWarehouseController(
     public void initialize(URL location, ResourceBundle resources) {
         commonViewsLoader.loadFallbackManager(fallbackContainer);
         selectOrCreateLocationController = commonViewsLoader.loadSelectOrCreateLocation(selectOrCreateLocationContainer);
-        selectOrCreateLocationController.initialize();
         loadWarehouse(currentSelectionService.getSelectedId());
     }
 
@@ -74,8 +82,7 @@ private void loadWarehouse(Integer warehouseId) {
 
         warehouseService.getWarehouseById(warehouseId)
                 .thenApply(this::handleWarehouseResponse)
-                .exceptionally(this::handleWarehouseException)
-                .thenRun(() -> Platform.runLater(() -> fallbackManager.setLoading(false)));
+                .exceptionally(this::handleWarehouseException);
     }
 
     private Result<Warehouse> handleWarehouseResponse(Result<Warehouse> result) {
@@ -85,10 +92,12 @@ private Result<Warehouse> handleWarehouseResponse(Result<Warehouse> result) {
                 return;
             }
             warehouse = result.getData();
+            fallbackManager.setLoading(false);
 
-            nameField.setText(warehouse.getName());
+            initializeFormFields();
             selectOrCreateLocationController.setSelectedLocation(warehouse.getLocation());
         });
+
         return result;
     }
 
@@ -97,29 +106,54 @@ private Result<Warehouse> handleWarehouseException(Throwable ex) {
         return new Result<>();
     }
 
+    private void initializeFormFields() {
+        nameFormField.initialize(String::new, "Name", true, warehouse.getName(), "Your input is not valid");
+    }
+
     @FXML
     private void handleSubmit() {
-        fallbackManager.reset();
-        fallbackManager.setLoading(true);
-
         UpdateWarehouseDTO warehouseDTO = getUpdateWarehouseDTO();
+        if (warehouseDTO == null) return;
         System.out.println(warehouseDTO);
 
+        fallbackManager.reset();
+        fallbackManager.setLoading(true);
+
         warehouseWriteService.updateWarehouse(warehouseDTO)
                 .thenApply(this::handleUpdateWarehouseResponse)
-                .exceptionally(ex -> {
-                    ex.printStackTrace();
-                    return new Result<>();
-                });
+                .exceptionally(this::handleUpdateWarehouseException);
+    }
+
+    private UpdateWarehouseDTO getUpdateWarehouseDTO() {
+        UpdateWarehouseDTO warehouseDTO = new UpdateWarehouseDTO();
+        warehouseDTO.setId(warehouse.getId());
+        try {
+            warehouseDTO.setName(nameFormField.handleSubmit());
+
+            if (selectOrCreateLocationController.isCreatingNewLocation()) {
+                warehouseDTO.setCreateLocation(true);
+                warehouseDTO.setLocation(selectOrCreateLocationController.getNewLocationDTO());
+            } else {
+                warehouseDTO.setCreateLocation(false);
+                warehouseDTO.setLocationId(selectOrCreateLocationController.getSelectedLocation().getId());
+            }
+        } catch (ValidationException e) {
+            return null;
+        }
+
+        return warehouseDTO;
     }
 
     private Result<Warehouse> handleUpdateWarehouseResponse(Result<Warehouse> result) {
         Platform.runLater(() -> {
+            fallbackManager.setLoading(false);
             if (result.getError() != null) {
-                fallbackManager.setErrorMessage("Failed to create warehouse.");
+                toastManager.addToast(new ToastInfo(
+                        "Error", "Failed to update warehouse.", OperationOutcome.ERROR));
                 return;
             }
-            fallbackManager.setLoading(false);
+            toastManager.addToast(new ToastInfo(
+                    "Success", "Warehouse updated successfully.", OperationOutcome.SUCCESS));
 
             // Manage navigation, invalidating previous warehouse cache
             Warehouse updatedWarehouse = result.getData();
@@ -131,20 +165,10 @@ private Result<Warehouse> handleUpdateWarehouseResponse(Result<Warehouse> result
         return result;
     }
 
-    private UpdateWarehouseDTO getUpdateWarehouseDTO() {
-        UpdateWarehouseDTO warehouseDTO = new UpdateWarehouseDTO();
-        warehouseDTO.setId(warehouse.getId());
-        warehouseDTO.setName(nameField.getText());
-
-        if (selectOrCreateLocationController.isCreatingNewLocation()) {
-            warehouseDTO.setCreateLocation(true);
-            warehouseDTO.setLocation(selectOrCreateLocationController.getNewLocationDTO());
-        } else {
-            warehouseDTO.setCreateLocation(false);
-            warehouseDTO.setLocationId(selectOrCreateLocationController.getSelectedLocation().getId());
-        }
-
-        return warehouseDTO;
+    private Result<Warehouse> handleUpdateWarehouseException(Throwable ex) {
+        Platform.runLater(() -> toastManager.addToast(new ToastInfo(
+                "An error occurred.", "Failed to update warehouse.", OperationOutcome.ERROR)));
+        return new Result<>();
     }
 }
 
diff --git a/src/main/resources/org/chainoptim/desktop/features/client/CreateClientView.fxml b/src/main/resources/org/chainoptim/desktop/features/client/CreateClientView.fxml
index c27d15b6..09c755f3 100644
--- a/src/main/resources/org/chainoptim/desktop/features/client/CreateClientView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/client/CreateClientView.fxml
@@ -3,6 +3,7 @@
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.text.Text?>
 
+<?import org.chainoptim.desktop.shared.common.uielements.forms.FormField?>
 <VBox xmlns="http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
       fx:controller="org.chainoptim.desktop.features.client.controller.CreateClientController">
@@ -13,8 +14,7 @@
                 <Text text="Create Client" styleClass="form-title"/>
             </HBox>
 
-            <Label text="Name *:" styleClass="form-label"/>
-            <TextField fx:id="nameField" styleClass="custom-text-field"/>
+            <FormField fx:id="nameFormField"/>
 
             <Label text="Location:" styleClass="form-label"/>
             <StackPane fx:id="selectOrCreateLocationContainer"/>
diff --git a/src/main/resources/org/chainoptim/desktop/features/client/UpdateClientView.fxml b/src/main/resources/org/chainoptim/desktop/features/client/UpdateClientView.fxml
index 9936d19a..38bd2140 100644
--- a/src/main/resources/org/chainoptim/desktop/features/client/UpdateClientView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/client/UpdateClientView.fxml
@@ -3,6 +3,7 @@
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.text.Text?>
 
+<?import org.chainoptim.desktop.shared.common.uielements.forms.FormField?>
 <VBox xmlns="http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
       fx:controller="org.chainoptim.desktop.features.client.controller.UpdateClientController">
@@ -13,8 +14,7 @@
                 <Text text="Update Client" styleClass="form-title"/>
             </HBox>
 
-            <Label text="Name *:" styleClass="form-label"/>
-            <TextField fx:id="nameField" styleClass="custom-text-field"/>
+            <FormField fx:id="nameFormField"/>
 
             <Label text="Location:" styleClass="form-label"/>
             <StackPane fx:id="selectOrCreateLocationContainer"/>
diff --git a/src/main/resources/org/chainoptim/desktop/features/factory/CreateFactoryView.fxml b/src/main/resources/org/chainoptim/desktop/features/factory/CreateFactoryView.fxml
index 5588d5b8..1b2657bd 100644
--- a/src/main/resources/org/chainoptim/desktop/features/factory/CreateFactoryView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/factory/CreateFactoryView.fxml
@@ -3,6 +3,7 @@
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.text.Text?>
 
+<?import org.chainoptim.desktop.shared.common.uielements.forms.FormField?>
 <VBox xmlns="http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
       fx:controller="org.chainoptim.desktop.features.factory.controller.CreateFactoryController">
@@ -13,13 +14,12 @@
                 <Text text="Create Factory" styleClass="form-title"/>
             </HBox>
 
-            <Label text="Name *:" styleClass="form-label"/>
-            <TextField fx:id="nameField" styleClass="custom-text-field"/>
+            <FormField fx:id="nameFormField"/>
 
             <Label text="Location:" styleClass="form-label"/>
             <StackPane fx:id="selectOrCreateLocationContainer"/>
 
-            <HBox alignment="TOP_RIGHT" style="-fx-padding: 10px 0px;">
+            <HBox alignment="TOP_RIGHT" style="-fx-padding: 16px 0px 0px 0px;">
                 <Button text="Create" onAction="#handleSubmit" styleClass="standard-write-button"/>
             </HBox>
         </VBox>
diff --git a/src/main/resources/org/chainoptim/desktop/features/factory/UpdateFactoryView.fxml b/src/main/resources/org/chainoptim/desktop/features/factory/UpdateFactoryView.fxml
index 42763d80..7827c22c 100644
--- a/src/main/resources/org/chainoptim/desktop/features/factory/UpdateFactoryView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/factory/UpdateFactoryView.fxml
@@ -3,6 +3,7 @@
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.text.Text?>
 
+<?import org.chainoptim.desktop.shared.common.uielements.forms.FormField?>
 <VBox xmlns="http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
       fx:controller="org.chainoptim.desktop.features.factory.controller.UpdateFactoryController">
@@ -13,8 +14,7 @@
                 <Text text="Update Factory" styleClass="form-title"/>
             </HBox>
 
-            <Label text="Name *:" styleClass="form-label"/>
-            <TextField fx:id="nameField" styleClass="custom-text-field"/>
+            <FormField fx:id="nameFormField"/>
 
             <Label text="Location:" styleClass="form-label"/>
             <StackPane fx:id="selectOrCreateLocationContainer"/>
diff --git a/src/main/resources/org/chainoptim/desktop/features/supplier/CreateSupplierView.fxml b/src/main/resources/org/chainoptim/desktop/features/supplier/CreateSupplierView.fxml
index 3fa31e2f..251d5ad2 100644
--- a/src/main/resources/org/chainoptim/desktop/features/supplier/CreateSupplierView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/supplier/CreateSupplierView.fxml
@@ -3,9 +3,10 @@
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.text.Text?>
 
+<?import org.chainoptim.desktop.shared.common.uielements.forms.FormField?>
 <VBox xmlns="http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
-      fx:controller="org.chainoptim.desktop.features.supplier.controller.CreateSupplierController" >
+      fx:controller="org.chainoptim.desktop.features.supplier.controller.CreateSupplierController">
 
     <ScrollPane fitToHeight="true" fitToWidth="true" VBox.vgrow="ALWAYS">
         <VBox VBox.vgrow="ALWAYS" styleClass="form-container">
@@ -13,8 +14,7 @@
                 <Text text="Create Supplier" styleClass="form-title"/>
             </HBox>
 
-            <Label text="Name *:" styleClass="form-label"/>
-            <TextField fx:id="nameField" styleClass="custom-text-field"/>
+            <FormField fx:id="nameFormField"/>
 
             <Label text="Location:" styleClass="form-label"/>
             <StackPane fx:id="selectOrCreateLocationContainer"/>
diff --git a/src/main/resources/org/chainoptim/desktop/features/supplier/UpdateSupplierView.fxml b/src/main/resources/org/chainoptim/desktop/features/supplier/UpdateSupplierView.fxml
index 2f6e3fc7..2b17b5c9 100644
--- a/src/main/resources/org/chainoptim/desktop/features/supplier/UpdateSupplierView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/supplier/UpdateSupplierView.fxml
@@ -3,24 +3,27 @@
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.text.Text?>
 
+<?import org.chainoptim.desktop.shared.common.uielements.forms.FormField?>
 <VBox xmlns="http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
-      fx:controller="org.chainoptim.desktop.features.supplier.controller.UpdateSupplierController"
-      styleClass="form-container">
+      fx:controller="org.chainoptim.desktop.features.supplier.controller.UpdateSupplierController">
 
-    <HBox styleClass="title-container">
-        <Text text="Update Supplier" styleClass="form-title"/>
-    </HBox>
+    <ScrollPane fitToHeight="true" fitToWidth="true" VBox.vgrow="ALWAYS">
+        <VBox VBox.vgrow="ALWAYS" styleClass="form-container">
+            <HBox styleClass="title-container">
+                <Text text="Update Supplier" styleClass="form-title"/>
+            </HBox>
 
-    <Label text="Name *:" styleClass="form-label"/>
-    <TextField fx:id="nameField" styleClass="custom-text-field"/>
+            <FormField fx:id="nameFormField"/>
 
-    <Label text="Location:" styleClass="form-label"/>
-    <StackPane fx:id="selectOrCreateLocationContainer"/>
+            <Label text="Location:" styleClass="form-label"/>
+            <StackPane fx:id="selectOrCreateLocationContainer"/>
 
-    <HBox alignment="TOP_RIGHT" style="-fx-padding: 10px 0px;">
-        <Button text="Update" onAction="#handleSubmit" styleClass="standard-write-button"/>
-    </HBox>
+            <HBox alignment="TOP_RIGHT" style="-fx-padding: 16px 0px 0px 0px;">
+                <Button text="Update" onAction="#handleSubmit" styleClass="standard-write-button"/>
+            </HBox>
+        </VBox>
+    </ScrollPane>
 
     <StackPane fx:id="fallbackContainer" VBox.vgrow="ALWAYS"/>
 
diff --git a/src/main/resources/org/chainoptim/desktop/features/warehouse/CreateWarehouseView.fxml b/src/main/resources/org/chainoptim/desktop/features/warehouse/CreateWarehouseView.fxml
index b253dc07..b295c52a 100644
--- a/src/main/resources/org/chainoptim/desktop/features/warehouse/CreateWarehouseView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/warehouse/CreateWarehouseView.fxml
@@ -3,6 +3,7 @@
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.text.Text?>
 
+<?import org.chainoptim.desktop.shared.common.uielements.forms.FormField?>
 <VBox xmlns="http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
       fx:controller="org.chainoptim.desktop.features.warehouse.controller.CreateWarehouseController">
@@ -13,8 +14,7 @@
                 <Text text="Create Warehouse" styleClass="form-title"/>
             </HBox>
 
-            <Label text="Name *:" styleClass="form-label"/>
-            <TextField fx:id="nameField" styleClass="custom-text-field"/>
+            <FormField fx:id="nameFormField"/>
 
             <Label text="Location:" styleClass="form-label"/>
             <StackPane fx:id="selectOrCreateLocationContainer"/>
diff --git a/src/main/resources/org/chainoptim/desktop/features/warehouse/UpdateWarehouseView.fxml b/src/main/resources/org/chainoptim/desktop/features/warehouse/UpdateWarehouseView.fxml
index 94a48710..eac00294 100644
--- a/src/main/resources/org/chainoptim/desktop/features/warehouse/UpdateWarehouseView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/warehouse/UpdateWarehouseView.fxml
@@ -3,6 +3,7 @@
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.text.Text?>
 
+<?import org.chainoptim.desktop.shared.common.uielements.forms.FormField?>
 <VBox xmlns="http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
       fx:controller="org.chainoptim.desktop.features.warehouse.controller.UpdateWarehouseController">
@@ -13,8 +14,7 @@
                 <Text text="Update Warehouse" styleClass="form-title"/>
             </HBox>
 
-            <Label text="Name *:" styleClass="form-label"/>
-            <TextField fx:id="nameField" styleClass="custom-text-field"/>
+            <FormField fx:id="nameFormField"/>
 
             <Label text="Location:" styleClass="form-label"/>
             <StackPane fx:id="selectOrCreateLocationContainer"/>

From 43329c3e24682719f1faf5d02ce69b999a6d1ef7 Mon Sep 17 00:00:00 2001
From: TudorOrban <130213626+TudorOrban@users.noreply.github.com>
Date: Thu, 25 Apr 2024 23:27:45 +0300
Subject: [PATCH 10/10] Add Update Organization form

---
 .../main/service/NavigationServiceImpl.java   |   1 +
 .../controller/OrganizationController.java    |  27 ++--
 .../UpdateOrganizationController.java         | 135 ++++++++++++++++++
 .../dto/UpdateOrganizationDTO.java            |  16 +++
 .../service/OrganizationService.java          |   2 +
 .../service/OrganizationServiceImpl.java      |  11 ++
 src/main/resources/css/sidebar.css            |   2 +-
 .../chainoptim/desktop/core/main/AppView.fxml |   2 +-
 .../organization/UpdateOrganizationView.fxml  |  31 ++++
 .../features/client/CreateClientView.fxml     |   2 +-
 .../features/client/UpdateClientView.fxml     |   2 +-
 .../features/factory/CreateFactoryView.fxml   |   2 +-
 .../features/factory/UpdateFactoryView.fxml   |   2 +-
 .../AddProductionRecordView.fxml              |   2 +-
 .../CreateFactoryStageView.fxml               |   2 +-
 .../UpdateFactoryStageView.fxml               |   2 +-
 .../features/product/CreateProductView.fxml   |   2 +-
 .../features/supplier/CreateSupplierView.fxml |   2 +-
 .../features/supplier/UpdateSupplierView.fxml |   2 +-
 .../warehouse/CreateWarehouseView.fxml        |   2 +-
 .../warehouse/UpdateWarehouseView.fxml        |   2 +-
 21 files changed, 222 insertions(+), 29 deletions(-)
 create mode 100644 src/main/java/org/chainoptim/desktop/core/organization/controller/UpdateOrganizationController.java
 create mode 100644 src/main/java/org/chainoptim/desktop/core/organization/dto/UpdateOrganizationDTO.java
 create mode 100644 src/main/resources/org/chainoptim/desktop/core/organization/UpdateOrganizationView.fxml

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 2233dfa1..4b3c5126 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
@@ -51,6 +51,7 @@ public NavigationServiceImpl(FXMLLoaderService fxmlLoaderService,
             Map.entry("Overview", "/org/chainoptim/desktop/core/overview/OverviewView.fxml"),
 
             Map.entry("Organization", "/org/chainoptim/desktop/core/organization/OrganizationView.fxml"),
+            Map.entry("Update-Organization", "/org/chainoptim/desktop/core/organization/UpdateOrganizationView.fxml"),
             Map.entry("Add-New-Members", "/org/chainoptim/desktop/core/organization/AddNewMembersView.fxml"),
 
             Map.entry("Products", "/org/chainoptim/desktop/features/product/ProductsView.fxml"),
diff --git a/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationController.java b/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationController.java
index 3b86f7d3..b94ad98f 100644
--- a/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationController.java
+++ b/src/main/java/org/chainoptim/desktop/core/organization/controller/OrganizationController.java
@@ -2,6 +2,7 @@
 
 import org.chainoptim.desktop.core.abstraction.ControllerFactory;
 import org.chainoptim.desktop.core.context.TenantContext;
+import org.chainoptim.desktop.core.main.service.NavigationService;
 import org.chainoptim.desktop.core.organization.model.CustomRole;
 import org.chainoptim.desktop.core.organization.model.Organization;
 import org.chainoptim.desktop.core.organization.model.OrganizationViewData;
@@ -11,6 +12,7 @@
 import org.chainoptim.desktop.shared.fallback.FallbackManager;
 import org.chainoptim.desktop.shared.httphandling.Result;
 import org.chainoptim.desktop.shared.util.DataReceiver;
+import org.chainoptim.desktop.shared.util.resourceloader.CommonViewsLoader;
 import org.chainoptim.desktop.shared.util.resourceloader.FXMLLoaderService;
 
 import com.google.inject.Inject;
@@ -36,7 +38,8 @@ public class OrganizationController implements Initializable {
     // Services
     private final OrganizationService organizationService;
     private final CustomRoleService customRoleService;
-    private final FXMLLoaderService fxmlLoaderService;
+    private final NavigationService navigationService;
+    private final CommonViewsLoader commonViewsLoader;
     private final ControllerFactory controllerFactory;
 
     private final FallbackManager fallbackManager;
@@ -69,21 +72,24 @@ public class OrganizationController implements Initializable {
 
     @Inject
     public OrganizationController(OrganizationService organizationService,
-                                    CustomRoleService customRoleService,
-                                  FXMLLoaderService fxmlLoaderService,
+                                  CustomRoleService customRoleService,
+                                  NavigationService navigationService,
+                                  CommonViewsLoader commonViewsLoader,
                                   ControllerFactory controllerFactory,
                                   FallbackManager fallbackManager) {
         this.organizationService = organizationService;
         this.customRoleService = customRoleService;
-        this.fxmlLoaderService = fxmlLoaderService;
+        this.navigationService = navigationService;
+        this.commonViewsLoader = commonViewsLoader;
         this.controllerFactory = controllerFactory;
         this.fallbackManager = fallbackManager;
     }
 
     @Override
     public void initialize(URL location, ResourceBundle resourceBundle) {
-        loadFallbackManager();
+        commonViewsLoader.loadFallbackManager(fallbackContainer);
         setupListeners();
+
         User currentUser = TenantContext.getCurrentUser();
         if (currentUser == null) {
             Platform.runLater(() -> fallbackManager.setLoading(false));
@@ -102,15 +108,6 @@ public void initialize(URL location, ResourceBundle resourceBundle) {
         loadCustomRoles(); // Multi-thread this as custom roles are not immediately needed
     }
 
-    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 setupListeners() {
         overviewTab.selectedProperty().addListener((observable, wasSelected, isNowSelected) -> {
             if (Boolean.TRUE.equals(isNowSelected) && overviewTab.getContent() == null) {
@@ -223,6 +220,6 @@ private Result<Organization> handleOrganizationException(Throwable ex) {
 
     @FXML
     private void handleEditOrganization() {
-        System.out.println("Edit organization clicked");
+        navigationService.switchView("Update-Organization", true);
     }
 }
diff --git a/src/main/java/org/chainoptim/desktop/core/organization/controller/UpdateOrganizationController.java b/src/main/java/org/chainoptim/desktop/core/organization/controller/UpdateOrganizationController.java
new file mode 100644
index 00000000..e77fba0a
--- /dev/null
+++ b/src/main/java/org/chainoptim/desktop/core/organization/controller/UpdateOrganizationController.java
@@ -0,0 +1,135 @@
+package org.chainoptim.desktop.core.organization.controller;
+
+import org.chainoptim.desktop.core.context.TenantContext;
+import org.chainoptim.desktop.core.main.service.CurrentSelectionService;
+import org.chainoptim.desktop.core.main.service.NavigationService;
+import org.chainoptim.desktop.core.main.service.NavigationServiceImpl;
+import org.chainoptim.desktop.core.organization.dto.UpdateOrganizationDTO;
+import org.chainoptim.desktop.core.organization.model.Organization;
+import org.chainoptim.desktop.core.organization.service.OrganizationService;
+import org.chainoptim.desktop.features.warehouse.dto.UpdateWarehouseDTO;
+import org.chainoptim.desktop.features.warehouse.model.Warehouse;
+import org.chainoptim.desktop.features.warehouse.service.WarehouseService;
+import org.chainoptim.desktop.features.warehouse.service.WarehouseWriteService;
+import org.chainoptim.desktop.shared.common.uielements.forms.FormField;
+import org.chainoptim.desktop.shared.common.uielements.forms.ValidationException;
+import org.chainoptim.desktop.shared.common.uielements.select.SelectOrCreateLocationController;
+import org.chainoptim.desktop.shared.enums.OperationOutcome;
+import org.chainoptim.desktop.shared.fallback.FallbackManager;
+import org.chainoptim.desktop.shared.httphandling.Result;
+import org.chainoptim.desktop.shared.toast.controller.ToastManager;
+import org.chainoptim.desktop.shared.toast.model.ToastInfo;
+import org.chainoptim.desktop.shared.util.resourceloader.CommonViewsLoader;
+import com.google.inject.Inject;
+import javafx.application.Platform;
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.scene.layout.StackPane;
+
+import java.net.URL;
+import java.util.ResourceBundle;
+
+public class UpdateOrganizationController implements Initializable {
+
+    // Services
+    private final OrganizationService organizationService;
+    private final NavigationService navigationService;
+    private final CommonViewsLoader commonViewsLoader;
+    private final ToastManager toastManager;
+    private final FallbackManager fallbackManager;
+
+    // State
+    private Organization organization;
+
+    // FXML
+    @FXML
+    private StackPane fallbackContainer;
+    @FXML
+    private FormField<String> nameFormField;
+    @FXML
+    private FormField<String> addressFormField;
+    @FXML
+    private FormField<String> contactInfoFormField;
+
+    @Inject
+    public UpdateOrganizationController(
+            OrganizationService organizationService,
+            NavigationService navigationService,
+            CommonViewsLoader commonViewsLoader,
+            ToastManager toastManager,
+            FallbackManager fallbackManager) {
+        this.organizationService = organizationService;
+        this.navigationService = navigationService;
+        this.commonViewsLoader = commonViewsLoader;
+        this.toastManager = toastManager;
+        this.fallbackManager = fallbackManager;
+    }
+
+    @FXML
+    public void initialize(URL location, ResourceBundle resources) {
+        this.organization = TenantContext.getCurrentUser().getOrganization();
+        commonViewsLoader.loadFallbackManager(fallbackContainer);
+        initializeFormFields();
+    }
+
+    private void initializeFormFields() {
+        nameFormField.initialize(String::new, "Name", true, organization.getName(), "Your input is not valid");
+        addressFormField.initialize(String::new, "Address", true, organization.getAddress(), "Your input is not valid");
+        contactInfoFormField.initialize(String::new, "Contact Info", true, organization.getContactInfo(), "Your input is not valid");
+    }
+
+    @FXML
+    private void handleSubmit() {
+        UpdateOrganizationDTO organizationDTO = getUpdateOrganizationDTO();
+        if (organizationDTO == null) return;
+        System.out.println(organizationDTO);
+
+        fallbackManager.reset();
+        fallbackManager.setLoading(true);
+
+        organizationService.updateOrganization(organizationDTO)
+                .thenApply(this::handleUpdateOrganizationResponse)
+                .exceptionally(this::handleUpdateOrganizationException);
+    }
+
+    private UpdateOrganizationDTO getUpdateOrganizationDTO() {
+        UpdateOrganizationDTO organizationDTO = new UpdateOrganizationDTO();
+        organizationDTO.setId(organization.getId());
+        organizationDTO.setId(organizationDTO.getId());
+        try {
+            organizationDTO.setName(nameFormField.handleSubmit());
+            organizationDTO.setAddress(addressFormField.handleSubmit());
+            organizationDTO.setContactInfo(contactInfoFormField.handleSubmit());
+        } catch (ValidationException e) {
+            return null;
+        }
+
+        return organizationDTO;
+    }
+
+    private Result<Organization> handleUpdateOrganizationResponse(Result<Organization> result) {
+        Platform.runLater(() -> {
+            fallbackManager.setLoading(false);
+            if (result.getError() != null) {
+                toastManager.addToast(new ToastInfo(
+                        "Error", "Failed to update organization.", OperationOutcome.ERROR));
+                return;
+            }
+            toastManager.addToast(new ToastInfo(
+                    "Success", "Organization updated successfully.", OperationOutcome.SUCCESS));
+
+            // Manage navigation, invalidating previous warehouse cache
+            String organizationPage = "Organization";
+            NavigationServiceImpl.invalidateViewCache(organizationPage);
+            navigationService.switchView(organizationPage, true);
+        });
+        return result;
+    }
+
+    private Result<Organization> handleUpdateOrganizationException(Throwable ex) {
+        Platform.runLater(() -> toastManager.addToast(new ToastInfo(
+                "An error occurred.", "Failed to update organization.", OperationOutcome.ERROR)));
+        return new Result<>();
+    }
+}
+
diff --git a/src/main/java/org/chainoptim/desktop/core/organization/dto/UpdateOrganizationDTO.java b/src/main/java/org/chainoptim/desktop/core/organization/dto/UpdateOrganizationDTO.java
new file mode 100644
index 00000000..fac9035c
--- /dev/null
+++ b/src/main/java/org/chainoptim/desktop/core/organization/dto/UpdateOrganizationDTO.java
@@ -0,0 +1,16 @@
+package org.chainoptim.desktop.core.organization.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class UpdateOrganizationDTO {
+
+    private Integer id;
+    private String name;
+    private String address;
+    private String contactInfo;
+}
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 59cb929a..52f86c8f 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,5 +1,6 @@
 package org.chainoptim.desktop.core.organization.service;
 
+import org.chainoptim.desktop.core.organization.dto.UpdateOrganizationDTO;
 import org.chainoptim.desktop.core.organization.model.Organization;
 import org.chainoptim.desktop.core.user.model.User;
 import org.chainoptim.desktop.shared.httphandling.Result;
@@ -10,5 +11,6 @@
 public interface OrganizationService {
 
     CompletableFuture<Result<Organization>> getOrganizationById(Integer organizationId, boolean includeUsers);
+    CompletableFuture<Result<Organization>> updateOrganization(UpdateOrganizationDTO organizationDTO);
 
 }
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 4ede8f4b..5c10e4ec 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,7 +1,9 @@
 package org.chainoptim.desktop.core.organization.service;
 
+import org.chainoptim.desktop.core.organization.dto.UpdateOrganizationDTO;
 import org.chainoptim.desktop.core.organization.model.Organization;
 import org.chainoptim.desktop.core.user.service.TokenManager;
+import org.chainoptim.desktop.shared.httphandling.HttpMethod;
 import org.chainoptim.desktop.shared.httphandling.RequestBuilder;
 import org.chainoptim.desktop.shared.httphandling.RequestHandler;
 import org.chainoptim.desktop.shared.httphandling.Result;
@@ -34,4 +36,13 @@ public CompletableFuture<Result<Organization>> getOrganizationById(Integer organ
 
         return requestHandler.sendRequest(request, new TypeReference<Organization>() {});
     }
+
+    public CompletableFuture<Result<Organization>> updateOrganization(UpdateOrganizationDTO organizationDTO) {
+        String routeAddress = "http://localhost:8080/api/v1/organizations/update";
+
+        HttpRequest request = requestBuilder.buildWriteRequest(
+                HttpMethod.POST, routeAddress, tokenManager.getToken(), organizationDTO);
+
+        return requestHandler.sendRequest(request, new TypeReference<Organization>() {});
+    }
 }
diff --git a/src/main/resources/css/sidebar.css b/src/main/resources/css/sidebar.css
index 8b019b40..9a96e1db 100644
--- a/src/main/resources/css/sidebar.css
+++ b/src/main/resources/css/sidebar.css
@@ -21,7 +21,7 @@
 }
 
 .sidebar-title-container-collapsed {
-    -fx-padding: 16px 12px;
+    -fx-padding: 16px 12px 24px 12px;
 }
 
 .sidebar-title {
diff --git a/src/main/resources/org/chainoptim/desktop/core/main/AppView.fxml b/src/main/resources/org/chainoptim/desktop/core/main/AppView.fxml
index ec0e3852..df43b623 100644
--- a/src/main/resources/org/chainoptim/desktop/core/main/AppView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/core/main/AppView.fxml
@@ -4,7 +4,7 @@
 <BorderPane xmlns = "http://javafx.com/javafx"
       xmlns:fx="http://javafx.com/fxml"
       fx:controller="org.chainoptim.desktop.core.main.controller.AppController"
-      style="-fx-background-color: #ffffff; -fx-border-width: 1px 0px; -fx-border-color: #c0c0c0;">
+      style="-fx-background-color: #ffffff; -fx-border-width: 1px 0px 0px 0px; -fx-border-color: #c0c0c0;">
 
     <!-- Sidebar -->
     <left>
diff --git a/src/main/resources/org/chainoptim/desktop/core/organization/UpdateOrganizationView.fxml b/src/main/resources/org/chainoptim/desktop/core/organization/UpdateOrganizationView.fxml
new file mode 100644
index 00000000..725f34a6
--- /dev/null
+++ b/src/main/resources/org/chainoptim/desktop/core/organization/UpdateOrganizationView.fxml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?import javafx.scene.control.*?>
+<?import javafx.scene.layout.*?>
+<?import javafx.scene.text.Text?>
+
+<?import org.chainoptim.desktop.shared.common.uielements.forms.FormField?>
+<VBox xmlns="http://javafx.com/javafx"
+      xmlns:fx="http://javafx.com/fxml"
+      fx:controller="org.chainoptim.desktop.core.organization.controller.UpdateOrganizationController">
+
+    <ScrollPane fitToHeight="true" fitToWidth="true" VBox.vgrow="ALWAYS">
+        <VBox VBox.vgrow="ALWAYS" styleClass="form-container">
+            <HBox styleClass="title-container">
+                <Text text="Update Organization" styleClass="form-title"/>
+            </HBox>
+
+            <FormField fx:id="nameFormField"/>
+
+            <FormField fx:id="addressFormField"/>
+
+            <FormField fx:id="contactInfoFormField"/>
+
+            <HBox alignment="TOP_RIGHT" style="-fx-padding: 20px 0px 0px 0px;">
+                <Button text="Update" onAction="#handleSubmit" styleClass="standard-write-button"/>
+            </HBox>
+        </VBox>
+    </ScrollPane>
+
+    <StackPane fx:id="fallbackContainer" VBox.vgrow="ALWAYS"/>
+
+</VBox>
diff --git a/src/main/resources/org/chainoptim/desktop/features/client/CreateClientView.fxml b/src/main/resources/org/chainoptim/desktop/features/client/CreateClientView.fxml
index 09c755f3..d1d29d8a 100644
--- a/src/main/resources/org/chainoptim/desktop/features/client/CreateClientView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/client/CreateClientView.fxml
@@ -19,7 +19,7 @@
             <Label text="Location:" styleClass="form-label"/>
             <StackPane fx:id="selectOrCreateLocationContainer"/>
 
-            <HBox alignment="TOP_RIGHT" style="-fx-padding: 16px 0px 0px 0px;">
+            <HBox alignment="TOP_RIGHT" style="-fx-padding: 20px 0px 0px 0px;">
                 <Button text="Create" onAction="#handleSubmit" styleClass="standard-write-button"/>
             </HBox>
         </VBox>
diff --git a/src/main/resources/org/chainoptim/desktop/features/client/UpdateClientView.fxml b/src/main/resources/org/chainoptim/desktop/features/client/UpdateClientView.fxml
index 38bd2140..72235798 100644
--- a/src/main/resources/org/chainoptim/desktop/features/client/UpdateClientView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/client/UpdateClientView.fxml
@@ -19,7 +19,7 @@
             <Label text="Location:" styleClass="form-label"/>
             <StackPane fx:id="selectOrCreateLocationContainer"/>
 
-            <HBox alignment="TOP_RIGHT" style="-fx-padding: 16px 0px 0px 0px;">
+            <HBox alignment="TOP_RIGHT" style="-fx-padding: 20px 0px 0px 0px;">
                 <Button text="Update" onAction="#handleSubmit" styleClass="standard-write-button"/>
             </HBox>
         </VBox>
diff --git a/src/main/resources/org/chainoptim/desktop/features/factory/CreateFactoryView.fxml b/src/main/resources/org/chainoptim/desktop/features/factory/CreateFactoryView.fxml
index 1b2657bd..9b1a588f 100644
--- a/src/main/resources/org/chainoptim/desktop/features/factory/CreateFactoryView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/factory/CreateFactoryView.fxml
@@ -19,7 +19,7 @@
             <Label text="Location:" styleClass="form-label"/>
             <StackPane fx:id="selectOrCreateLocationContainer"/>
 
-            <HBox alignment="TOP_RIGHT" style="-fx-padding: 16px 0px 0px 0px;">
+            <HBox alignment="TOP_RIGHT" style="-fx-padding: 20px 0px 0px 0px;">
                 <Button text="Create" onAction="#handleSubmit" styleClass="standard-write-button"/>
             </HBox>
         </VBox>
diff --git a/src/main/resources/org/chainoptim/desktop/features/factory/UpdateFactoryView.fxml b/src/main/resources/org/chainoptim/desktop/features/factory/UpdateFactoryView.fxml
index 7827c22c..c412a1f2 100644
--- a/src/main/resources/org/chainoptim/desktop/features/factory/UpdateFactoryView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/factory/UpdateFactoryView.fxml
@@ -19,7 +19,7 @@
             <Label text="Location:" styleClass="form-label"/>
             <StackPane fx:id="selectOrCreateLocationContainer"/>
 
-            <HBox alignment="TOP_RIGHT" style="-fx-padding: 16px 0px 0px 0px;">
+            <HBox alignment="TOP_RIGHT" style="-fx-padding: 20px 0px 0px 0px;">
                 <Button text="Update" onAction="#handleSubmit" styleClass="standard-write-button"/>
             </HBox>
         </VBox>
diff --git a/src/main/resources/org/chainoptim/desktop/features/factory/factoryproduction/AddProductionRecordView.fxml b/src/main/resources/org/chainoptim/desktop/features/factory/factoryproduction/AddProductionRecordView.fxml
index ac599eff..1fced178 100644
--- a/src/main/resources/org/chainoptim/desktop/features/factory/factoryproduction/AddProductionRecordView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/factory/factoryproduction/AddProductionRecordView.fxml
@@ -44,7 +44,7 @@
         <Label text="Allocations" styleClass="general-label-extra-large" style="-fx-padding: 16px 0px 0px 0px;"/>
         <VBox fx:id="allocationsVBox"/>
 
-        <HBox alignment="TOP_RIGHT" style="-fx-padding: 16px 0px 0px 0px;">
+        <HBox alignment="TOP_RIGHT" style="-fx-padding: 20px 0px 0px 0px;">
             <Button text="Add Record" onAction="#handleSubmit" styleClass="standard-write-button"/>
         </HBox>
     </VBox>
diff --git a/src/main/resources/org/chainoptim/desktop/features/factory/factoryproduction/CreateFactoryStageView.fxml b/src/main/resources/org/chainoptim/desktop/features/factory/factoryproduction/CreateFactoryStageView.fxml
index d364f4ad..2cf18dee 100644
--- a/src/main/resources/org/chainoptim/desktop/features/factory/factoryproduction/CreateFactoryStageView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/factory/factoryproduction/CreateFactoryStageView.fxml
@@ -35,7 +35,7 @@
         <Label text="Minimum Required Capacity:" styleClass="form-label"/>
         <TextField fx:id="minimumRequiredCapacityField" styleClass="custom-text-field"/>
 
-        <HBox alignment="TOP_RIGHT" style="-fx-padding: 16px 0px 0px 0px;">
+        <HBox alignment="TOP_RIGHT" style="-fx-padding: 20px 0px 0px 0px;">
             <Button text="Add Stage" onAction="#handleSubmit" styleClass="standard-write-button"/>
         </HBox>
 
diff --git a/src/main/resources/org/chainoptim/desktop/features/factory/factoryproduction/UpdateFactoryStageView.fxml b/src/main/resources/org/chainoptim/desktop/features/factory/factoryproduction/UpdateFactoryStageView.fxml
index 3b758ed9..4d7465bf 100644
--- a/src/main/resources/org/chainoptim/desktop/features/factory/factoryproduction/UpdateFactoryStageView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/factory/factoryproduction/UpdateFactoryStageView.fxml
@@ -30,7 +30,7 @@
         <Label text="Minimum Required Capacity:" styleClass="form-label"/>
         <TextField fx:id="minimumRequiredCapacityField" styleClass="custom-text-field"/>
 
-        <HBox alignment="TOP_RIGHT" style="-fx-padding: 16px 0px 0px 0px;">
+        <HBox alignment="TOP_RIGHT" style="-fx-padding: 20px 0px 0px 0px;">
             <Button text="Update Stage" onAction="#handleSubmit" styleClass="standard-write-button"/>
         </HBox>
 
diff --git a/src/main/resources/org/chainoptim/desktop/features/product/CreateProductView.fxml b/src/main/resources/org/chainoptim/desktop/features/product/CreateProductView.fxml
index c970307a..ea4b1a83 100644
--- a/src/main/resources/org/chainoptim/desktop/features/product/CreateProductView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/product/CreateProductView.fxml
@@ -23,7 +23,7 @@
             <Label text="Unit of Measurement:" styleClass="form-label"/>
             <StackPane fx:id="unitOfMeasurementContainer"/>
 
-            <HBox alignment="TOP_RIGHT" style="-fx-padding: 16px 0px 0px 0px;">
+            <HBox alignment="TOP_RIGHT" style="-fx-padding: 20px 0px 0px 0px;">
                 <Button text="Create" onAction="#handleSubmit" styleClass="standard-write-button"/>
             </HBox>
         </VBox>
diff --git a/src/main/resources/org/chainoptim/desktop/features/supplier/CreateSupplierView.fxml b/src/main/resources/org/chainoptim/desktop/features/supplier/CreateSupplierView.fxml
index 251d5ad2..eac006be 100644
--- a/src/main/resources/org/chainoptim/desktop/features/supplier/CreateSupplierView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/supplier/CreateSupplierView.fxml
@@ -19,7 +19,7 @@
             <Label text="Location:" styleClass="form-label"/>
             <StackPane fx:id="selectOrCreateLocationContainer"/>
 
-            <HBox alignment="TOP_RIGHT" style="-fx-padding: 16px 0px 0px 0px;">
+            <HBox alignment="TOP_RIGHT" style="-fx-padding: 20px 0px 0px 0px;">
                 <Button text="Create" onAction="#handleSubmit" styleClass="standard-write-button"/>
             </HBox>
         </VBox>
diff --git a/src/main/resources/org/chainoptim/desktop/features/supplier/UpdateSupplierView.fxml b/src/main/resources/org/chainoptim/desktop/features/supplier/UpdateSupplierView.fxml
index 2b17b5c9..09e5d070 100644
--- a/src/main/resources/org/chainoptim/desktop/features/supplier/UpdateSupplierView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/supplier/UpdateSupplierView.fxml
@@ -19,7 +19,7 @@
             <Label text="Location:" styleClass="form-label"/>
             <StackPane fx:id="selectOrCreateLocationContainer"/>
 
-            <HBox alignment="TOP_RIGHT" style="-fx-padding: 16px 0px 0px 0px;">
+            <HBox alignment="TOP_RIGHT" style="-fx-padding: 20px 0px 0px 0px;">
                 <Button text="Update" onAction="#handleSubmit" styleClass="standard-write-button"/>
             </HBox>
         </VBox>
diff --git a/src/main/resources/org/chainoptim/desktop/features/warehouse/CreateWarehouseView.fxml b/src/main/resources/org/chainoptim/desktop/features/warehouse/CreateWarehouseView.fxml
index b295c52a..8a907336 100644
--- a/src/main/resources/org/chainoptim/desktop/features/warehouse/CreateWarehouseView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/warehouse/CreateWarehouseView.fxml
@@ -19,7 +19,7 @@
             <Label text="Location:" styleClass="form-label"/>
             <StackPane fx:id="selectOrCreateLocationContainer"/>
 
-            <HBox alignment="TOP_RIGHT" style="-fx-padding: 16px 0px 0px 0px;">
+            <HBox alignment="TOP_RIGHT" style="-fx-padding: 20px 0px 0px 0px;">
                 <Button text="Create" onAction="#handleSubmit" styleClass="standard-write-button"/>
             </HBox>
         </VBox>
diff --git a/src/main/resources/org/chainoptim/desktop/features/warehouse/UpdateWarehouseView.fxml b/src/main/resources/org/chainoptim/desktop/features/warehouse/UpdateWarehouseView.fxml
index eac00294..0861a9d2 100644
--- a/src/main/resources/org/chainoptim/desktop/features/warehouse/UpdateWarehouseView.fxml
+++ b/src/main/resources/org/chainoptim/desktop/features/warehouse/UpdateWarehouseView.fxml
@@ -19,7 +19,7 @@
             <Label text="Location:" styleClass="form-label"/>
             <StackPane fx:id="selectOrCreateLocationContainer"/>
 
-            <HBox alignment="TOP_RIGHT" style="-fx-padding: 16px 0px 0px 0px;">
+            <HBox alignment="TOP_RIGHT" style="-fx-padding: 20px 0px 0px 0px;">
                 <Button text="Update" onAction="#handleSubmit" styleClass="standard-write-button"/>
             </HBox>
         </VBox>