From a7628b3ce37a5c59835c33813b9d07f0ebd54fa8 Mon Sep 17 00:00:00 2001 From: TudorOrban <130213626+TudorOrban@users.noreply.github.com> Date: Sun, 21 Jul 2024 15:54:45 +0300 Subject: [PATCH] Create SupplierPerformances view displaying scores --- .../main/service/NavigationServiceImpl.java | 1 + .../controller/SupplierController.java | 3 - .../SupplierPerformancesController.java | 218 ++++++++++++++++++ .../features/supplier/model/Supplier.java | 7 + .../uielements/performance/ScoreDisplay.java | 6 + .../supplier/SupplierPerformancesView.fxml | 24 ++ 6 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/chainoptim/desktop/features/supplier/controller/SupplierPerformancesController.java create mode 100644 src/main/resources/org/chainoptim/desktop/features/supplier/SupplierPerformancesView.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 d26d9462..fab100fe 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 @@ -79,6 +79,7 @@ public NavigationServiceImpl(FXMLLoaderService fxmlLoaderService, Map.entry("Update-Supplier", "/org/chainoptim/desktop/features/supplier/UpdateSupplierView.fxml"), Map.entry("Supplier-Orders", "/org/chainoptim/desktop/features/supplier/SupplierOrdersView.fxml"), Map.entry("Supplier-Shipments", "/org/chainoptim/desktop/features/supplier/SupplierShipmentsView.fxml"), + Map.entry("Supplier-Performances", "/org/chainoptim/desktop/features/supplier/SupplierPerformancesView.fxml"), Map.entry("Clients", "/org/chainoptim/desktop/features/client/ClientsView.fxml"), Map.entry("Client", "/org/chainoptim/desktop/features/client/ClientView.fxml"), diff --git a/src/main/java/org/chainoptim/desktop/features/supplier/controller/SupplierController.java b/src/main/java/org/chainoptim/desktop/features/supplier/controller/SupplierController.java index 9e366987..216be3a7 100644 --- a/src/main/java/org/chainoptim/desktop/features/supplier/controller/SupplierController.java +++ b/src/main/java/org/chainoptim/desktop/features/supplier/controller/SupplierController.java @@ -33,7 +33,6 @@ public class SupplierController implements Initializable { private final SupplierService supplierService; private final NavigationService navigationService; private final CurrentSelectionService currentSelectionService; - private final FXMLLoaderService fxmlLoaderService; private final ControllerFactory controllerFactory; private final FallbackManager fallbackManager; @@ -58,13 +57,11 @@ public class SupplierController implements Initializable { public SupplierController(SupplierService supplierService, NavigationService navigationService, CurrentSelectionService currentSelectionService, - FXMLLoaderService fxmlLoaderService, ControllerFactory controllerFactory, FallbackManager fallbackManager) { this.supplierService = supplierService; this.navigationService = navigationService; this.currentSelectionService = currentSelectionService; - this.fxmlLoaderService = fxmlLoaderService; this.controllerFactory = controllerFactory; this.fallbackManager = fallbackManager; } diff --git a/src/main/java/org/chainoptim/desktop/features/supplier/controller/SupplierPerformancesController.java b/src/main/java/org/chainoptim/desktop/features/supplier/controller/SupplierPerformancesController.java new file mode 100644 index 00000000..fa0bfed6 --- /dev/null +++ b/src/main/java/org/chainoptim/desktop/features/supplier/controller/SupplierPerformancesController.java @@ -0,0 +1,218 @@ +package org.chainoptim.desktop.features.supplier.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.user.model.User; +import org.chainoptim.desktop.features.supplier.model.Supplier; +import org.chainoptim.desktop.features.supplier.service.SupplierService; +import org.chainoptim.desktop.shared.common.uielements.performance.ScoreDisplay; +import org.chainoptim.desktop.shared.enums.Feature; +import org.chainoptim.desktop.shared.fallback.FallbackManager; +import org.chainoptim.desktop.shared.httphandling.Result; +import org.chainoptim.desktop.shared.search.controller.ListHeaderController; +import org.chainoptim.desktop.shared.search.controller.PageSelectorController; +import org.chainoptim.desktop.shared.search.model.ListHeaderParams; +import org.chainoptim.desktop.shared.search.model.PaginatedResults; +import org.chainoptim.desktop.shared.search.model.SearchParams; +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.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.*; + +import java.net.URL; +import java.util.Map; +import java.util.ResourceBundle; + +public class SupplierPerformancesController implements Initializable { + + // Services + private final SupplierService supplierService; + private final NavigationService navigationService; + private final CurrentSelectionService currentSelectionService; + private final CommonViewsLoader commonViewsLoader; + + // State + private final FallbackManager fallbackManager; + private final SearchParams searchParams; + private long totalCount; + private final Map sortOptions = Map.of( + "createdAt", "Created At", + "updatedAt", "Updated At" + ); + + // Controllers + private ListHeaderController headerController; + private PageSelectorController pageSelectorController; + + // FXML + @FXML + private ScrollPane suppliersScrollPane; + @FXML + private VBox suppliersVBox; + @FXML + private StackPane headerContainer; + @FXML + private StackPane fallbackContainer; + @FXML + private StackPane pageSelectorContainer; + + @Inject + public SupplierPerformancesController(SupplierService supplierService, + NavigationService navigationService, + CurrentSelectionService currentSelectionService, + CommonViewsLoader commonViewsLoader, + FallbackManager fallbackManager, + SearchParams searchParams) { + this.supplierService = supplierService; + this.navigationService = navigationService; + this.currentSelectionService = currentSelectionService; + this.commonViewsLoader = commonViewsLoader; + this.fallbackManager = fallbackManager; + this.searchParams = searchParams; + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + headerController = commonViewsLoader.loadListHeader(headerContainer); + headerController.initializeHeader(new ListHeaderParams(null, searchParams, "Suppliers", "/img/truck-arrow-right-solid.png", Feature.SUPPLIER, sortOptions, null, this::loadSuppliers, "Supplier", "Create-Supplier")); + commonViewsLoader.loadFallbackManager(fallbackContainer); + setUpListeners(); + loadSuppliers(); + pageSelectorController = commonViewsLoader.loadPageSelector(pageSelectorContainer); + } + + private void setUpListeners() { + searchParams.getSearchQueryProperty().addListener((observable, oldValue, newValue) -> loadSuppliers()); + searchParams.getAscendingProperty().addListener((observable, oldValue, newValue) -> loadSuppliers()); + searchParams.getSortOptionProperty().addListener((observable, oldValue, newValue) -> loadSuppliers()); + searchParams.getPageProperty().addListener((observable, oldPage, newPage) -> loadSuppliers()); + + // Listen to empty fallback state + fallbackManager.isEmptyProperty().addListener((observable, oldValue, newValue) -> { + suppliersScrollPane.setVisible(newValue); + suppliersScrollPane.setManaged(newValue); + fallbackContainer.setVisible(!newValue); + fallbackContainer.setManaged(!newValue); + }); + } + + private void loadSuppliers() { + fallbackManager.reset(); + fallbackManager.setLoading(true); + + User currentUser = TenantContext.getCurrentUser(); + if (currentUser == null) { + Platform.runLater(() -> fallbackManager.setLoading(false)); + return; + } + Integer organizationId = currentUser.getOrganization().getId(); + + supplierService.getSuppliersByOrganizationIdAdvanced(organizationId, searchParams) + .thenApply(this::handleSupplierResponse) + .exceptionally(this::handleSupplierException); + } + + private Result> handleSupplierResponse(Result> result) { + Platform.runLater(() -> { + if (result.getError() != null) { + fallbackManager.setErrorMessage("Failed to load suppliers."); + return; + } + PaginatedResults paginatedResults = result.getData(); + fallbackManager.setLoading(false); + + totalCount = paginatedResults.getTotalCount(); + pageSelectorController.initialize(searchParams, totalCount); + int suppliersLimit = TenantContext.getCurrentUser().getOrganization().getSubscriptionPlan().getMaxSuppliers(); + headerController.disableCreateButton(suppliersLimit != -1 && totalCount >= suppliersLimit, "You have reached the limit of suppliers allowed by your current subscription plan."); + + suppliersVBox.getChildren().clear(); + if (paginatedResults.results.isEmpty()) { + fallbackManager.setNoResults(true); + return; + } + + for (Supplier supplier : paginatedResults.results) { + drawSupplierCardUI(supplier); + } + fallbackManager.setNoResults(false); + }); + return result; + } + + private Result> handleSupplierException(Throwable ex) { + Platform.runLater(() -> fallbackManager.setErrorMessage("Failed to load suppliers.")); + return new Result<>(); + } + + private void drawSupplierCardUI(Supplier supplier) { + HBox cardHBox = new HBox(); + cardHBox.setAlignment(Pos.CENTER); + + // Left side + VBox supplierBox = new VBox(); + cardHBox.getChildren().add(supplierBox); + + Label supplierName = new Label(supplier.getName()); + supplierName.getStyleClass().add("entity-name-label"); + supplierBox.getChildren().add(supplierName); + Label supplierLocation = new Label(); + if (supplier.getLocation() != null) { + supplierLocation.setText(supplier.getLocation().getFormattedLocation()); + } else { + supplierLocation.setText(""); + } + supplierLocation.getStyleClass().add("entity-description-label"); + supplierBox.getChildren().add(supplierLocation); + + // Separator + Region separator = new Region(); + HBox.setHgrow(separator, Priority.ALWAYS); + cardHBox.getChildren().add(separator); + + // Right side + HBox supplierScoresHBox = new HBox(8); + cardHBox.getChildren().add(supplierScoresHBox); + drawSupplierScores(supplier, supplierScoresHBox); + + Button supplierButton = new Button(); + supplierButton.getStyleClass().add("entity-card"); + supplierButton.setGraphic(cardHBox); + supplierButton.setMaxWidth(Double.MAX_VALUE); + supplierButton.prefWidthProperty().bind(suppliersVBox.widthProperty()); + supplierButton.setOnAction(event -> openSupplierDetails(supplier.getId())); + + suppliersVBox.getChildren().add(supplierButton); + } + + private void drawSupplierScores(Supplier supplier, HBox supplierScoresHBox) { + addScore(supplier.getOverallScore(), supplierScoresHBox, "Overall Score"); + addScore(supplier.getTimelinessScore(), supplierScoresHBox, "Timeliness Score"); + addScore(supplier.getAvailabilityScore(), supplierScoresHBox, "Availability Score"); + addScore(supplier.getQualityScore(), supplierScoresHBox, "Quality Score"); + } + + private void addScore(Float score, HBox supplierScoresHBox, String tooltipText) { + ScoreDisplay scoreDisplay = new ScoreDisplay(); + int scoreValue = score != null ? Math.round(score) : 0; + scoreDisplay.setScore(scoreValue); + scoreDisplay.setTooltipText(tooltipText); + supplierScoresHBox.getChildren().add(scoreDisplay); + } + + private void openSupplierDetails(Integer supplierId) { + currentSelectionService.setSelectedId(supplierId); + currentSelectionService.setSelectedPage("Supplier"); + + navigationService.switchView("Supplier?id=" + supplierId, true, null); + } +} + diff --git a/src/main/java/org/chainoptim/desktop/features/supplier/model/Supplier.java b/src/main/java/org/chainoptim/desktop/features/supplier/model/Supplier.java index a10f33f6..8969f7cf 100644 --- a/src/main/java/org/chainoptim/desktop/features/supplier/model/Supplier.java +++ b/src/main/java/org/chainoptim/desktop/features/supplier/model/Supplier.java @@ -14,4 +14,11 @@ public class Supplier { private LocalDateTime updatedAt; private Integer organizationId; private Location location; + + // Performance + private Float overallScore; + private Float timelinessScore; + private Float quantityPerTimeScore; + private Float availabilityScore; + private Float qualityScore; } diff --git a/src/main/java/org/chainoptim/desktop/shared/common/uielements/performance/ScoreDisplay.java b/src/main/java/org/chainoptim/desktop/shared/common/uielements/performance/ScoreDisplay.java index 4dd608cb..75a3d27f 100644 --- a/src/main/java/org/chainoptim/desktop/shared/common/uielements/performance/ScoreDisplay.java +++ b/src/main/java/org/chainoptim/desktop/shared/common/uielements/performance/ScoreDisplay.java @@ -1,6 +1,7 @@ package org.chainoptim.desktop.shared.common.uielements.performance; import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.shape.Arc; @@ -46,4 +47,9 @@ public void setScore(int score) { arcDisplay.getStyleClass().add("good-arc"); } } + + public void setTooltipText(String text) { + Tooltip tooltip = new Tooltip(text); + Tooltip.install(this, tooltip); + } } diff --git a/src/main/resources/org/chainoptim/desktop/features/supplier/SupplierPerformancesView.fxml b/src/main/resources/org/chainoptim/desktop/features/supplier/SupplierPerformancesView.fxml new file mode 100644 index 00000000..9b06aef5 --- /dev/null +++ b/src/main/resources/org/chainoptim/desktop/features/supplier/SupplierPerformancesView.fxml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + +