Merge branch 'idkwhattonameitanymore' into 'main'
added pop up view ingredients + warnings for delete Closes #53 and #55 See merge request cse1105/2025-2026/teams/csep-team-76!48
This commit is contained in:
commit
3c33f2ff35
9 changed files with 571 additions and 8 deletions
131
client/src/main/java/client/Ingredient/IngredientController.java
Normal file
131
client/src/main/java/client/Ingredient/IngredientController.java
Normal file
|
|
@ -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<Ingredient> 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<IngredientUsageResponse> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,4 +26,8 @@ public class UI extends Application {
|
||||||
var mainCtrl = INJECTOR.getInstance(MainCtrl.class);
|
var mainCtrl = INJECTOR.getInstance(MainCtrl.class);
|
||||||
mainCtrl.setup(primaryStage, foodpal);
|
mainCtrl.setup(primaryStage, foodpal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static MyFXML getFXML() {
|
||||||
|
return FXML;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import java.util.logging.Logger;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import client.exception.InvalidModificationException;
|
import client.exception.InvalidModificationException;
|
||||||
|
import client.scenes.recipe.IngredientsPopupCtrl;
|
||||||
import client.scenes.recipe.RecipeDetailCtrl;
|
import client.scenes.recipe.RecipeDetailCtrl;
|
||||||
|
|
||||||
import client.utils.Config;
|
import client.utils.Config;
|
||||||
|
|
@ -20,6 +21,7 @@ import client.utils.LocaleManager;
|
||||||
import client.utils.ServerUtils;
|
import client.utils.ServerUtils;
|
||||||
import client.utils.WebSocketDataService;
|
import client.utils.WebSocketDataService;
|
||||||
import client.utils.WebSocketUtils;
|
import client.utils.WebSocketUtils;
|
||||||
|
import commons.Ingredient;
|
||||||
import commons.Recipe;
|
import commons.Recipe;
|
||||||
|
|
||||||
import commons.ws.Topics;
|
import commons.ws.Topics;
|
||||||
|
|
@ -36,17 +38,20 @@ import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.ListCell;
|
import javafx.scene.control.ListCell;
|
||||||
import javafx.scene.control.ListView;
|
import javafx.scene.control.ListView;
|
||||||
|
import javafx.scene.control.TextInputDialog;
|
||||||
import javafx.scene.control.ToggleButton;
|
import javafx.scene.control.ToggleButton;
|
||||||
import org.apache.commons.lang3.NotImplementedException;
|
import org.apache.commons.lang3.NotImplementedException;
|
||||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
|
|
||||||
public class FoodpalApplicationCtrl implements LocaleAware {
|
public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
private final ServerUtils server;
|
private final ServerUtils server;
|
||||||
private final WebSocketUtils webSocketUtils;
|
private final WebSocketUtils webSocketUtils;
|
||||||
private final LocaleManager localeManager;
|
private final LocaleManager localeManager;
|
||||||
private final WebSocketDataService<Long, Recipe> dataService;
|
private final WebSocketDataService<Long, Recipe> dataService;
|
||||||
private final Logger logger = Logger.getLogger(FoodpalApplicationCtrl.class.getName());
|
private final Logger logger = Logger.getLogger(FoodpalApplicationCtrl.class.getName());
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private RecipeDetailCtrl recipeDetailController;
|
private RecipeDetailCtrl recipeDetailController;
|
||||||
|
|
||||||
|
|
@ -60,6 +65,9 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
@FXML
|
@FXML
|
||||||
public ListView<Recipe> recipeList;
|
public ListView<Recipe> recipeList;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ListView<Ingredient> ingredientListView;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Button addRecipeButton;
|
private Button addRecipeButton;
|
||||||
|
|
||||||
|
|
@ -93,6 +101,7 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
|
|
||||||
this.configService = configService;
|
this.configService = configService;
|
||||||
this.dataService = recipeDataService;
|
this.dataService = recipeDataService;
|
||||||
|
|
||||||
setupDataService();
|
setupDataService();
|
||||||
logger.info("WebSocket processor initialized.");
|
logger.info("WebSocket processor initialized.");
|
||||||
initializeWebSocket();
|
initializeWebSocket();
|
||||||
|
|
@ -400,6 +409,139 @@ public class FoodpalApplicationCtrl implements LocaleAware {
|
||||||
this.recipeDetailController.refreshFavouriteButton();
|
this.recipeDetailController.refreshFavouriteButton();
|
||||||
this.recipeDetailController.setVisible(!recipeList.getItems().isEmpty());
|
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<String> 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<Ingredient>() {
|
||||||
|
@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 {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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<Ingredient> 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<String> 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<Ingredient> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package client.utils;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
import commons.Ingredient;
|
||||||
import commons.Recipe;
|
import commons.Recipe;
|
||||||
import commons.RecipeIngredient;
|
import commons.RecipeIngredient;
|
||||||
import jakarta.ws.rs.ProcessingException;
|
import jakarta.ws.rs.ProcessingException;
|
||||||
|
|
@ -207,4 +208,84 @@ public class ServerUtils {
|
||||||
|
|
||||||
updateRecipe(recipe);
|
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<String> 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<String> 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<Ingredient> getIngredients() throws IOException, InterruptedException {
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(SERVER + "/ingredients"))
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> 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<List<Ingredient>>() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
//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<String> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,8 @@
|
||||||
<Label fx:id="recipesLabel" text="Recipes">
|
<Label fx:id="recipesLabel" text="Recipes">
|
||||||
<font>
|
<font>
|
||||||
<Font name="System Bold" size="15.0" />
|
<Font name="System Bold" size="15.0" />
|
||||||
</font></Label>
|
</font>
|
||||||
|
</Label>
|
||||||
|
|
||||||
<fx:include source="SearchBar.fxml" fx:id="searchBar" />
|
<fx:include source="SearchBar.fxml" fx:id="searchBar" />
|
||||||
|
|
||||||
|
|
@ -53,9 +54,14 @@
|
||||||
<HBox spacing="10">
|
<HBox spacing="10">
|
||||||
<Button fx:id="addRecipeButton" onAction="#addRecipe" text="Add Recipe" />
|
<Button fx:id="addRecipeButton" onAction="#addRecipe" text="Add Recipe" />
|
||||||
<Button fx:id="removeRecipeButton" onAction="#removeSelectedRecipe" text="Remove Recipe" />
|
<Button fx:id="removeRecipeButton" onAction="#removeSelectedRecipe" text="Remove Recipe" />
|
||||||
<Button fx:id= "cloneRecipeButton" mnemonicParsing="false" onAction="#cloneRecipe" text="Clone" />
|
<Button fx:id="cloneRecipeButton" mnemonicParsing="false" onAction="#cloneRecipe" text="Clone" />
|
||||||
<ToggleButton fx:id="favouritesOnlyToggle" text="Favourites" onAction="#toggleFavouritesView"/>
|
<ToggleButton fx:id="favouritesOnlyToggle" text="Favourites" onAction="#toggleFavouritesView" />
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|
||||||
|
<Button fx:id="manageIngredientsButton"
|
||||||
|
onAction="#openIngredientsPopup"
|
||||||
|
text="Ingredients..." />
|
||||||
|
|
||||||
</VBox>
|
</VBox>
|
||||||
</left>
|
</left>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.ButtonBar?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.ListView?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
|
||||||
|
<VBox xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1"
|
||||||
|
fx:controller="client.scenes.recipe.IngredientsPopupCtrl"
|
||||||
|
spacing="10" prefWidth="420" prefHeight="520">
|
||||||
|
|
||||||
|
<padding>
|
||||||
|
<Insets top="12" right="12" bottom="12" left="12"/>
|
||||||
|
</padding>
|
||||||
|
|
||||||
|
<Label text="Ingredients" style="-fx-font-size: 18px; -fx-font-weight: bold;"/>
|
||||||
|
|
||||||
|
<ListView fx:id="ingredientListView" VBox.vgrow="ALWAYS"/>
|
||||||
|
|
||||||
|
<ButtonBar>
|
||||||
|
<buttons>
|
||||||
|
<Button text="Add" onAction="#addIngredient"/>
|
||||||
|
<Button text="Refresh" onAction="#refresh"/>
|
||||||
|
<Button text="Delete" onAction="#deleteSelected"/>
|
||||||
|
<Button text="Close" onAction="#close"/>
|
||||||
|
</buttons>
|
||||||
|
</ButtonBar>
|
||||||
|
</VBox>
|
||||||
|
|
@ -62,6 +62,8 @@ public class Ingredient {
|
||||||
this.carbsPer100g = carbsPer100g;
|
this.carbsPer100g = carbsPer100g;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public double kcalPer100g() {
|
public double kcalPer100g() {
|
||||||
return proteinPer100g * KCAL_PER_GRAM_PROTEIN
|
return proteinPer100g * KCAL_PER_GRAM_PROTEIN
|
||||||
+ carbsPer100g * KCAL_PER_GRAM_CARBS
|
+ carbsPer100g * KCAL_PER_GRAM_CARBS
|
||||||
|
|
@ -92,6 +94,10 @@ public class Ingredient {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setName(String name){
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import commons.ws.Topics;
|
||||||
import commons.ws.messages.CreateIngredientMessage;
|
import commons.ws.messages.CreateIngredientMessage;
|
||||||
import commons.ws.messages.DeleteIngredientMessage;
|
import commons.ws.messages.DeleteIngredientMessage;
|
||||||
import commons.ws.messages.UpdateIngredientMessage;
|
import commons.ws.messages.UpdateIngredientMessage;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
|
@ -115,6 +116,8 @@ public class IngredientController {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update an existing ingredient by its ID.
|
* Update an existing ingredient by its ID.
|
||||||
* Maps to <code>PATCH /api/ingredients/{id}</code>
|
* Maps to <code>PATCH /api/ingredients/{id}</code>
|
||||||
|
|
@ -166,18 +169,26 @@ public class IngredientController {
|
||||||
*/
|
*/
|
||||||
@PostMapping("/ingredients")
|
@PostMapping("/ingredients")
|
||||||
public ResponseEntity<Ingredient> createIngredient(@RequestBody Ingredient ingredient) {
|
public ResponseEntity<Ingredient> createIngredient(@RequestBody Ingredient ingredient) {
|
||||||
if (ingredient.name == null || ingredient.name.isEmpty()) {
|
if (ingredient == null
|
||||||
|
|| ingredient.name == null
|
||||||
|
|| ingredient.name.trim().isEmpty()) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ingredientService.create(ingredient)
|
return ingredientService.create(ingredient)
|
||||||
.map(saved -> {
|
.map(saved -> {
|
||||||
messagingTemplate.convertAndSend(Topics.INGREDIENTS, new CreateIngredientMessage(saved));
|
messagingTemplate.convertAndSend(
|
||||||
|
Topics.INGREDIENTS,
|
||||||
|
new CreateIngredientMessage(saved)
|
||||||
|
);
|
||||||
return ResponseEntity.ok(saved);
|
return ResponseEntity.ok(saved);
|
||||||
})
|
})
|
||||||
.orElseGet(() -> ResponseEntity.badRequest().build());
|
.orElseGet(() -> ResponseEntity.status
|
||||||
|
(HttpStatus.CONFLICT).build()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete an ingredient by its ID.
|
* Delete an ingredient by its ID.
|
||||||
* Maps to <code>DELETE /api/ingredients/{id}</code>
|
* Maps to <code>DELETE /api/ingredients/{id}</code>
|
||||||
|
|
@ -194,13 +205,24 @@ public class IngredientController {
|
||||||
*/
|
*/
|
||||||
@DeleteMapping("/ingredients/{id}")
|
@DeleteMapping("/ingredients/{id}")
|
||||||
public ResponseEntity<Boolean> deleteIngredient(@PathVariable Long id) {
|
public ResponseEntity<Boolean> deleteIngredient(@PathVariable Long id) {
|
||||||
|
//check if the ingredient is used in any recipe
|
||||||
|
long usageCount = ingredientService.countUsage(id);
|
||||||
|
|
||||||
|
if (usageCount > 0) {
|
||||||
|
// If used in recipes, return a warning response (HTTP 400)
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(false); // Ingredient in use, don't delete
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete if the ingredient is not in use
|
||||||
if (!ingredientService.delete(id)) {
|
if (!ingredientService.delete(id)) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
messagingTemplate.convertAndSend(Topics.INGREDIENTS, new DeleteIngredientMessage(id));
|
messagingTemplate.convertAndSend(Topics.INGREDIENTS, new DeleteIngredientMessage(id));
|
||||||
return ResponseEntity.ok(true);
|
return ResponseEntity.ok(true); // deleted~
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public record IngredientUsageResponse(Long ingredientId, long usedInRecipes){}
|
public record IngredientUsageResponse(Long ingredientId, long usedInRecipes){}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue