diff --git a/README.md b/README.md index ded91de..946ee4a 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,62 @@ -# CSEP Template Project +# CSEP FoodPal Application (Team 76) -This repository contains the template for the CSE project. Please extend this README.md with sufficient instructions that will illustrate for your TA and the course staff how they can run your project. +## Usage -To run the template project from the command line, you either need to have [Maven](https://maven.apache.org/install.html) installed on your local system (`mvn`) or you need to use the Maven wrapper (`mvnw`). You can then execute +The project uses Java 25. Make sure you have the correct Java version installed. - mvn -pl server -am spring-boot:run +### Client -to run the server and +The client needs to be launched **after** a server is already running, see Usage.Server section: - mvn -pl client -am javafx:run +``` +mvn -pl client -am javafx:run +``` -to run the client. Please note that the server needs to be running, before you can start the client. +### Server -Get the template project running from the command line first to ensure you have the required tools on your sytem. +By default, the server listens to the port `8080`. +[TODO(1)]:: Configurable port. -Once it is working, you can try importing the project into your favorite IDE. Especially the client is a bit more tricky to set up there due to the dependency on a JavaFX SDK. -To help you get started, you can find additional instructions in the corresponding README of the client project. \ No newline at end of file +``` +mvn -pl server -am spring-boot:run +``` + +## Features + +- Recipe tracking in an intuitive GUI. +- Ability to input arbitrary amounts of an ingredient in a recipe. The design is very human. See Manual.Ingredients for more usage details. +- Native localization in more than 2, and less than 4 languages. +- Configurable via JSON, See Manual.Configuration. + +## Manual + +### Configuration + +The configuration is with JSON, read from `config.json` in the working directory. We illustrate an example configuration below. +```json +{ + "language": "en", + "serverUrl": "http://localhost:8080", + "favourites": [ + 1, + ], + "shoppingList": [ + "Ingredient A", + ], +} +``` +#### Options + +- `language: string` + - One of \[en, nl, pl\] (as of Jan 11 2026) +- `serverUrl: string` + - The host that the FoodPal server runs on, see configuration example. +- `favourites: [number]` + - The list of recipe IDs that the user has marked as favourite. +- `shoppingList: [string]` + - The list of ingredients that the user has in their shopping list. + +### Ingredients + +- To input a **formal** ingredient, you write the numeric amount in the first input box, then the Unit in the selection dropdown, and then write the name of the ingredient, e.g. salt, apples, etc. should it be not visible already on the platform. +- To input an **informal** ingredient, describe the amount in the first input box, like "some of", or "a sprinkle of", then select "" in the unit selection box, and write the name of your ingredient or pick from one of the availables from the dropdown. 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..f2f658d 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(); @@ -268,7 +277,10 @@ public class FoodpalApplicationCtrl implements LocaleAware { public void refresh() { List recipes; try { - recipes = server.getRecipesFiltered(searchBarController.getFilter()); + recipes = server.getRecipesFiltered( + searchBarController.getFilter(), + this.configService.getConfig().getRecipeLanguages() + ); } catch (IOException | InterruptedException e) { recipes = Collections.emptyList(); String msg = "Failed to load recipes: " + e.getMessage(); @@ -400,6 +412,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 +555,8 @@ public class FoodpalApplicationCtrl implements LocaleAware { + + + + + diff --git a/client/src/main/java/client/scenes/LanguageFilterCtrl.java b/client/src/main/java/client/scenes/LanguageFilterCtrl.java new file mode 100644 index 0000000..56d79af --- /dev/null +++ b/client/src/main/java/client/scenes/LanguageFilterCtrl.java @@ -0,0 +1,87 @@ +package client.scenes; + +import client.utils.ConfigService; +import client.utils.LocaleAware; +import client.utils.LocaleManager; +import com.google.inject.Inject; +import javafx.fxml.FXML; +import javafx.scene.control.CheckMenuItem; +import javafx.scene.control.MenuButton; + +import java.util.ArrayList; +import java.util.List; + +public class LanguageFilterCtrl implements LocaleAware { + private final LocaleManager manager; + private final ConfigService configService; + private final FoodpalApplicationCtrl appCtrl; + + @Inject + public LanguageFilterCtrl(LocaleManager manager, ConfigService configService, FoodpalApplicationCtrl appCtrl) { + this.manager = manager; + this.configService = configService; + this.appCtrl = appCtrl; + } + + @FXML + MenuButton langFilterMenu; + + List selectedLanguages = new ArrayList<>(); + + private String getSelectedLanguagesDisplay() { + String joined = String.join(", ", selectedLanguages); + if (joined.isEmpty()) { + return "none"; + } else { + return joined; + } + } + + private void updateMenuButtonDisplay() { + langFilterMenu.setText(getLocaleString("menu.label.selected-langs") + ": " + this.getSelectedLanguagesDisplay()); + } + + @Override + public void updateText() { + this.updateMenuButtonDisplay(); + } + + @Override + public LocaleManager getLocaleManager() { + return this.manager; + } + + public void initializeComponents() { + var items = this.langFilterMenu.getItems(); + + final List languages = List.of("en", "nl", "pl"); + this.selectedLanguages = this.configService.getConfig().getRecipeLanguages(); + this.updateMenuButtonDisplay(); + + items.clear(); + + languages.forEach(lang -> { + CheckMenuItem it = new CheckMenuItem(lang); + + if (selectedLanguages.contains(it.getText())) { + it.setSelected(true); + } + + it.selectedProperty().addListener((observable, _, value) -> { + if (value) { + selectedLanguages.add(it.getText()); + } else { + selectedLanguages.remove(it.getText()); + } + + configService.save(); + selectedLanguages.sort(String::compareTo); + appCtrl.refresh(); + + this.updateMenuButtonDisplay(); + }); + + items.add(it); + }); + } +} diff --git a/client/src/main/java/client/scenes/SearchBarCtrl.java b/client/src/main/java/client/scenes/SearchBarCtrl.java index 4f6113b..da40e3a 100644 --- a/client/src/main/java/client/scenes/SearchBarCtrl.java +++ b/client/src/main/java/client/scenes/SearchBarCtrl.java @@ -1,5 +1,6 @@ package client.scenes; +import client.utils.ConfigService; import client.utils.LocaleAware; import client.utils.LocaleManager; import client.utils.ServerUtils; @@ -28,6 +29,7 @@ public class SearchBarCtrl implements LocaleAware { private final LocaleManager localeManager; private final ServerUtils serverUtils; + private final ConfigService configService; private Consumer> onSearchCallback; @@ -41,9 +43,10 @@ public class SearchBarCtrl implements LocaleAware { private Task> currentSearchTask = null; @Inject - public SearchBarCtrl(LocaleManager localeManager, ServerUtils serverUtils) { + public SearchBarCtrl(LocaleManager localeManager, ServerUtils serverUtils, ConfigService configService) { this.localeManager = localeManager; this.serverUtils = serverUtils; + this.configService = configService; } @FXML @@ -96,7 +99,7 @@ public class SearchBarCtrl implements LocaleAware { currentSearchTask = new Task<>() { @Override protected List call() throws IOException, InterruptedException { - return serverUtils.getRecipesFiltered(filter); + return serverUtils.getRecipesFiltered(filter, configService.getConfig().getRecipeLanguages()); } }; diff --git a/client/src/main/java/client/scenes/recipe/IngredientListCell.java b/client/src/main/java/client/scenes/recipe/IngredientListCell.java index c4c0d25..b6c22bf 100644 --- a/client/src/main/java/client/scenes/recipe/IngredientListCell.java +++ b/client/src/main/java/client/scenes/recipe/IngredientListCell.java @@ -75,6 +75,10 @@ public class IngredientListCell extends OrderedEditableListCell 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/scenes/recipe/RecipeDetailCtrl.java b/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java index 716a128..62d98bd 100644 --- a/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java +++ b/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java @@ -19,6 +19,7 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ListView; import javafx.scene.control.TextField; +import javafx.scene.control.ComboBox; import javafx.scene.input.KeyCode; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; @@ -77,6 +78,9 @@ public class RecipeDetailCtrl implements LocaleAware { @FXML private Button favouriteButton; + @FXML + private ComboBox langSelector; + private ListView getParentRecipeList() { return this.appCtrl.recipeList; } @@ -138,6 +142,7 @@ public class RecipeDetailCtrl implements LocaleAware { this.showName(recipe.getName()); this.ingredientListController.refetchFromRecipe(recipe); this.stepListController.refetchFromRecipe(recipe); + this.langSelector.setValue(recipe.getLocale()); this.refreshFavouriteButton(); } @@ -294,6 +299,20 @@ public class RecipeDetailCtrl implements LocaleAware { editableTitleArea.getChildren().add(nameLabel); } + /** + * Switch the recipe's language. + */ + @FXML + void changeLanguage() { + recipe.setLocale(this.langSelector.getValue()); + + try { + server.updateRecipe(this.recipe); + } catch (IOException | InterruptedException e) { + throw new UpdateException("Error occurred when updating recipe locale!"); + } + } + @Override public void updateText() { editRecipeTitleButton.setText(getLocaleString("menu.button.edit")); @@ -309,5 +328,7 @@ public class RecipeDetailCtrl implements LocaleAware { @Override public void initializeComponents() { initStepsIngredientsList(); + + langSelector.getItems().addAll("en", "nl", "pl"); } } diff --git a/client/src/main/java/client/utils/Config.java b/client/src/main/java/client/utils/Config.java index 8b0accf..0880d82 100644 --- a/client/src/main/java/client/utils/Config.java +++ b/client/src/main/java/client/utils/Config.java @@ -6,7 +6,6 @@ import java.util.List; public class Config { private String language; private String serverUrl; - private List favourites; private List shoppingList; @@ -68,5 +67,28 @@ public class Config { public void removeFavourite(long recipeId) { getFavourites().remove(recipeId); } + + /** + * Get a list of languages that should filter the displayed recipes. + * + * @return The desired languages the user would like to see. + */ + public List getRecipeLanguages() { + return this.recipeLanguages; + } + + /** + * Add a language to the list of filtering languages. + */ + public void addRecipeLanguage(String lang) { + this.recipeLanguages.add(lang); + } + + /** + * Remove a language from the list of filtering languages. + */ + public void removeRecipeLanguage(String lang) { + this.recipeLanguages.remove(lang); + } } diff --git a/client/src/main/java/client/utils/DefaultValueFactory.java b/client/src/main/java/client/utils/DefaultValueFactory.java index 7d7f52d..299ae53 100644 --- a/client/src/main/java/client/utils/DefaultValueFactory.java +++ b/client/src/main/java/client/utils/DefaultValueFactory.java @@ -26,6 +26,7 @@ public class DefaultValueFactory { return new Recipe( null, "Untitled recipe", + "en", List.of(), List.of()); } diff --git a/client/src/main/java/client/utils/ServerUtils.java b/client/src/main/java/client/utils/ServerUtils.java index 9c89508..101486a 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; @@ -39,9 +40,16 @@ public class ServerUtils { * Gets all the recipes from the backend. * @return a JSON string with all the recipes */ - public List getRecipes() throws IOException, InterruptedException { + public List getRecipes(List locales) throws IOException, InterruptedException { + + String uri = + SERVER + + "/recipes" + + "?locales=" + + String.join(",", locales); + HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(SERVER + "/recipes")) + .uri(URI.create(uri)) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); @@ -56,9 +64,9 @@ public class ServerUtils { return list; // JSON string-> List (Jackson) } - public List getRecipesFiltered(String filter) throws IOException, InterruptedException { + public List getRecipesFiltered(String filter, List locales) throws IOException, InterruptedException { // TODO: implement filtering on server side - return this.getRecipes(); + return this.getRecipes(locales); } /** @@ -87,7 +95,7 @@ public class ServerUtils { */ public Recipe addRecipe(Recipe newRecipe) throws IOException, InterruptedException { //Make sure the name of the newRecipe is unique - List allRecipes = getRecipes(); + List allRecipes = getRecipes(List.of()); newRecipe.setId(null); // otherwise the id is the same as the original, and that's wrong // now that each recipeIngredient has its own ID in the database, // we set that to null too to force a new persist value on the server @@ -207,4 +215,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..dcbc79d 100644 --- a/client/src/main/resources/client/scenes/FoodpalApplication.fxml +++ b/client/src/main/resources/client/scenes/FoodpalApplication.fxml @@ -30,7 +30,7 @@ - + @@ -44,18 +44,25 @@ + + +