diff --git a/client/src/main/java/client/Ingredient/IngredientController.java b/client/src/main/java/client/Ingredient/IngredientController.java new file mode 100644 index 0000000..14d4197 --- /dev/null +++ b/client/src/main/java/client/Ingredient/IngredientController.java @@ -0,0 +1,131 @@ +package client.Ingredient; + +import commons.Ingredient; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Alert.AlertType; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.ListView; +import javafx.scene.control.Button; +import org.springframework.web.client.RestTemplate; +import org.springframework.http.ResponseEntity; +import org.springframework.http.HttpStatus; + +public class IngredientController { + + @FXML + private ListView ingredientListView; + + @FXML + private Button deleteButton; + + private final RestTemplate restTemplate = new RestTemplate(); // Simplified REST client + + @FXML + private void handleDeleteIngredient(ActionEvent event) { + // Get selected ingredient + Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem(); + if (selectedIngredient == null) { + showError("No ingredient selected", "Please select an ingredient to delete."); + return; + } + + // Check if the ingredient is used in any recipe + checkIngredientUsage(selectedIngredient); + } + + private void checkIngredientUsage(Ingredient ingredient) { + String url = "http://localhost:8080/api/ingredients/" + ingredient.getId() + "/usage"; + + ResponseEntity response = restTemplate.getForEntity(url, IngredientUsageResponse.class); + + if (response.getStatusCode() == HttpStatus.NOT_FOUND) { + showError("Ingredient not found", "The ingredient does not exist."); + return; + } + + IngredientUsageResponse usageResponse = response.getBody(); + long usedInRecipes = usageResponse != null ? usageResponse.getUsedInRecipes() : 0; + + if (usedInRecipes > 0) { + // If ingredient is in use, show warning to the user + showWarningDialog(ingredient, usedInRecipes); + } else { + // delete if not in use + deleteIngredient(ingredient); + } + } + + private void showWarningDialog(Ingredient ingredient, long usedInRecipes) { + Alert alert = new Alert(AlertType.WARNING); + alert.setTitle("Warning"); + alert.setHeaderText("Ingredient in Use"); + alert.setContentText("The ingredient '" + ingredient.getName() + "' is used in " + usedInRecipes + " recipe(s). Are you sure you want to delete it?"); + + ButtonType deleteButton = new ButtonType("Delete Anyway"); + ButtonType cancelButton = new ButtonType("Cancel"); + + alert.getButtonTypes().setAll(deleteButton, cancelButton); + + alert.showAndWait().ifPresent(response -> { + if (response == deleteButton) { + // delete if the user confirms + deleteIngredient(ingredient); + } + }); + } + + private void deleteIngredient(Ingredient ingredient) { + String url = "http://localhost:8080/api/ingredients/" + ingredient.getId(); + + restTemplate.delete(url); + showConfirmation("Deletion Successful", "The ingredient '" + ingredient.getName() + "' has been successfully deleted."); + + // refresh + refreshIngredientList(); + } + + private void showError(String title, String message) { + Alert alert = new Alert(AlertType.ERROR); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } + + private void showConfirmation(String title, String message) { + Alert alert = new Alert(AlertType.INFORMATION); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } + + private void refreshIngredientList() { + // Refresh + ingredientListView.getItems().clear(); + } + + // Inner class for usage response + public static class IngredientUsageResponse { + private long ingredientId; + private long usedInRecipes; + + public long getIngredientId() { + return ingredientId; + } + + public void setIngredientId(long ingredientId) { + this.ingredientId = ingredientId; + } + + public long getUsedInRecipes() { + return usedInRecipes; + } + + public void setUsedInRecipes(long usedInRecipes) { + this.usedInRecipes = usedInRecipes; + } + } +} diff --git a/client/src/main/java/client/UI.java b/client/src/main/java/client/UI.java index 9e1217d..327ca82 100644 --- a/client/src/main/java/client/UI.java +++ b/client/src/main/java/client/UI.java @@ -26,4 +26,8 @@ public class UI extends Application { var mainCtrl = INJECTOR.getInstance(MainCtrl.class); mainCtrl.setup(primaryStage, foodpal); } + + public static MyFXML getFXML() { + return FXML; + } } diff --git a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java index cdbd565..1bcee34 100644 --- a/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java +++ b/client/src/main/java/client/scenes/FoodpalApplicationCtrl.java @@ -10,6 +10,7 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import client.exception.InvalidModificationException; +import client.scenes.recipe.IngredientsPopupCtrl; import client.scenes.recipe.RecipeDetailCtrl; import client.utils.Config; @@ -20,6 +21,7 @@ import client.utils.LocaleManager; import client.utils.ServerUtils; import client.utils.WebSocketDataService; import client.utils.WebSocketUtils; +import commons.Ingredient; import commons.Recipe; import commons.ws.Topics; @@ -36,17 +38,20 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; +import javafx.scene.control.TextInputDialog; import javafx.scene.control.ToggleButton; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; + public class FoodpalApplicationCtrl implements LocaleAware { private final ServerUtils server; private final WebSocketUtils webSocketUtils; private final LocaleManager localeManager; private final WebSocketDataService dataService; private final Logger logger = Logger.getLogger(FoodpalApplicationCtrl.class.getName()); + @FXML private RecipeDetailCtrl recipeDetailController; @@ -60,6 +65,9 @@ public class FoodpalApplicationCtrl implements LocaleAware { @FXML public ListView recipeList; + @FXML + private ListView ingredientListView; + @FXML private Button addRecipeButton; @@ -93,6 +101,7 @@ public class FoodpalApplicationCtrl implements LocaleAware { this.configService = configService; this.dataService = recipeDataService; + setupDataService(); logger.info("WebSocket processor initialized."); initializeWebSocket(); @@ -400,6 +409,139 @@ public class FoodpalApplicationCtrl implements LocaleAware { this.recipeDetailController.refreshFavouriteButton(); this.recipeDetailController.setVisible(!recipeList.getItems().isEmpty()); } + + //Delete Ingredient button click + @FXML + private void handleDeleteIngredient() { + // Get selected ingredient + Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem(); + + if (selectedIngredient == null) { + // Show an error message if no ingredient is selected + showError("No ingredient selected", "Please select an ingredient to delete."); + return; + } + + // Check if the ingredient is used in any recipe + checkIngredientUsage(selectedIngredient); + } + + // Check if ingredient is used in any recipe before deleting + private void checkIngredientUsage(Ingredient ingredient) { + try { + long usageCount = server.getIngredientUsage(ingredient.getId()); // Check ingredient usage via ServerUtils + + if (usageCount > 0) { + // If ingredient is used, show a warning dialog + showWarningDialog(ingredient, usageCount); + } else { + // If not used, delete + deleteIngredient(ingredient); + } + } catch (IOException | InterruptedException e) { + showError("Error", "Failed to check ingredient usage: " + e.getMessage()); + } + } + + private void deleteIngredient(Ingredient ingredient) { + try { + server.deleteIngredient(ingredient.getId()); // Call ServerUtils to delete the ingredient + showConfirmation("Success", "Ingredient '" + ingredient.getName() + "' has been deleted."); + refreshIngredientList(); // refresh the ingredient list + } catch (IOException | InterruptedException e) { + showError("Error", "Failed to delete ingredient: " + e.getMessage()); + } + } + + private void showWarningDialog(Ingredient ingredient, long usedInRecipes) { + Alert alert = new Alert(Alert.AlertType.WARNING); + } + + private void showError(String title, String message) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } + + private void showConfirmation(String title, String message) { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } + + private void refreshIngredientList() { + // Refresh + ingredientListView.getItems().clear(); + + } + + + @FXML + private void handleAddIngredient() { + //ask the user for the ingredient name + TextInputDialog dialog = new TextInputDialog(); + dialog.setTitle("Add Ingredient"); + dialog.setHeaderText("Enter the ingredient name:"); + dialog.setContentText("Ingredient:"); + + // Wait for the user to enter a value + Optional result = dialog.showAndWait(); + + result.ifPresent(name -> { + // Create a new Ingredient object + Ingredient newIngredient = new Ingredient(); + newIngredient.setName(name); + + // Add the new ingredient to the ListView + ingredientListView.getItems().add(newIngredient); + }); + } + + // display ingredient name + @FXML + private void initializeIngredients() { + ingredientListView.setCellFactory(list -> new ListCell() { + @Override + protected void updateItem(Ingredient item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + setText(item.getName()); // Display the ingredient name in the ListView + } + } + }); + } + + @FXML + private void openIngredientsPopup() { + try { + var pair = client.UI.getFXML().load( + IngredientsPopupCtrl.class, + "client", "scenes", "recipe", "IngredientsPopup.fxml" + ); + + var root = pair.getValue(); + + var stage = new javafx.stage.Stage(); + stage.setTitle("Ingredients"); + stage.initModality(javafx.stage.Modality.APPLICATION_MODAL); + stage.setScene(new javafx.scene.Scene(root)); + stage.showAndWait(); + } catch (Exception e) { + var alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText("Failed to open ingredients popup"); + alert.setContentText(e.getMessage()); + alert.showAndWait(); + e.printStackTrace(); + } + } + } @@ -410,3 +552,8 @@ public class FoodpalApplicationCtrl implements LocaleAware { + + + + + diff --git a/client/src/main/java/client/scenes/recipe/IngredientsPopupCtrl.java b/client/src/main/java/client/scenes/recipe/IngredientsPopupCtrl.java new file mode 100644 index 0000000..5c55684 --- /dev/null +++ b/client/src/main/java/client/scenes/recipe/IngredientsPopupCtrl.java @@ -0,0 +1,136 @@ +package client.scenes.recipe; + +import client.utils.ServerUtils; +import commons.Ingredient; +import jakarta.inject.Inject; +import javafx.fxml.FXML; +import javafx.scene.control.Alert; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.TextInputDialog; +import javafx.stage.Stage; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +//TODO and check for capital letter milk and MILK are seen as different + + +public class IngredientsPopupCtrl { + + private final ServerUtils server; + + @FXML + private ListView ingredientListView; + + @Inject + public IngredientsPopupCtrl(ServerUtils server) { + this.server = server; + } + + @FXML + public void initialize() { + ingredientListView.setCellFactory(list -> new ListCell<>() { + @Override + protected void updateItem(Ingredient item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + setText(item.getName()); + } + } + }); + + refresh(); + } + @FXML + private void addIngredient() { + TextInputDialog dialog = new TextInputDialog(); + dialog.setTitle("Add Ingredient"); + dialog.setHeaderText("Create a new ingredient"); + dialog.setContentText("Name:"); + + Optional result = dialog.showAndWait(); + if (result.isEmpty()) { + return; + } + + String name = result.get().trim(); + if (name.isEmpty()) { + showError("Ingredient name cannot be empty."); + return; + } + + try { + server.createIngredient(name); // calls POST /api/ingredients + refresh(); // reload list from server + } catch (IOException | InterruptedException e) { + showError("Failed to create ingredient: " + e.getMessage()); + } + } + + + @FXML + private void refresh() { + try { + List ingredients = server.getIngredients(); + ingredientListView.getItems().setAll(ingredients); + } catch (IOException | InterruptedException e) { + showError("Failed to load ingredients: " + e.getMessage()); + } + } + + @FXML + private void deleteSelected() { + Ingredient selected = ingredientListView.getSelectionModel().getSelectedItem(); + if (selected == null) { + showError("No ingredient selected."); + return; + } + + try { + long usageCount = server.getIngredientUsage(selected.getId()); + if (usageCount > 0) { + boolean proceed = confirmDeleteUsed(selected.getName(), usageCount); + if (!proceed) { + return; + } + } + + server.deleteIngredient(selected.getId()); + refresh(); + } catch (IOException | InterruptedException e) { + showError("Failed to delete ingredient: " + e.getMessage()); + } + } + + @FXML + private void close() { + Stage stage = (Stage) ingredientListView.getScene().getWindow(); + stage.close(); + } + + private boolean confirmDeleteUsed(String name, long usedInRecipes) { + Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setTitle("Warning"); + alert.setHeaderText("Ingredient in use"); + alert.setContentText("Ingredient '" + name + "' is used in " + usedInRecipes + + " recipe(s). Delete anyway?"); + + var delete = new javafx.scene.control.ButtonType("Delete Anyway"); + var cancel = new javafx.scene.control.ButtonType("Cancel"); + alert.getButtonTypes().setAll(delete, cancel); + + return alert.showAndWait().orElse(cancel) == delete; + } + + private void showError(String msg) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText(null); + alert.setContentText(msg); + alert.showAndWait(); + } +} diff --git a/client/src/main/java/client/utils/ServerUtils.java b/client/src/main/java/client/utils/ServerUtils.java index 9c89508..6685a46 100644 --- a/client/src/main/java/client/utils/ServerUtils.java +++ b/client/src/main/java/client/utils/ServerUtils.java @@ -3,6 +3,7 @@ package client.utils; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.inject.Inject; +import commons.Ingredient; import commons.Recipe; import commons.RecipeIngredient; import jakarta.ws.rs.ProcessingException; @@ -207,4 +208,84 @@ public class ServerUtils { updateRecipe(recipe); } + + + // how many ingredients are getting used in recipes + + public long getIngredientUsage(long ingredientId) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(SERVER + "/ingredients/" + ingredientId + "/usage")) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != statusOK) { + throw new IOException("Failed to get usage for ingredient with id: " + ingredientId + + " body: " + response.body()); + } + + record IngredientUsageResponse(long ingredientId, long usedInRecipes) {} + IngredientUsageResponse usage = + objectMapper.readValue(response.body(), IngredientUsageResponse.class); + + return usage.usedInRecipes(); + } + + + public void deleteIngredient(long ingredientId) throws IOException, InterruptedException { + // Send delete request to remove the ingredient + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(SERVER + "/ingredients/" + ingredientId)) + .DELETE() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != statusOK) { + throw new IOException("Failed to delete ingredient with id: " + ingredientId + " body: " + response.body()); + } + + logger.info("Successfully deleted ingredient with id: " + ingredientId); + } + + + //retrieves the list of ingredients saved to backend + + public List getIngredients() throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(SERVER + "/ingredients")) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != statusOK) { + throw new IOException("Failed to fetch ingredients. Server responds with: " + response.body()); + } + + return objectMapper.readValue(response.body(), new com.fasterxml.jackson.core.type.TypeReference>() {}); + } + + //creates new ingredients in the ingredient list + + public Ingredient createIngredient(String name) throws IOException, InterruptedException { + Ingredient ingredient = new Ingredient(name, 0.0, 0.0, 0.0); + String json = objectMapper.writeValueAsString(ingredient); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(SERVER + "/ingredients")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(json)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != statusOK) { + throw new IOException("Failed to create ingredient. Server responds with: " + response.body()); + } + + return objectMapper.readValue(response.body(), Ingredient.class); + } + + + + } diff --git a/client/src/main/resources/client/scenes/FoodpalApplication.fxml b/client/src/main/resources/client/scenes/FoodpalApplication.fxml index 310e78d..0262c9c 100644 --- a/client/src/main/resources/client/scenes/FoodpalApplication.fxml +++ b/client/src/main/resources/client/scenes/FoodpalApplication.fxml @@ -30,7 +30,7 @@ - + @@ -44,7 +44,8 @@ + + @@ -53,9 +54,14 @@