diff --git a/recipes.sql b/recipes.sql new file mode 100644 index 0000000..437f207 --- /dev/null +++ b/recipes.sql @@ -0,0 +1,121 @@ +CREATE TABLE keylookup ( + id INTEGER NOT NULL, + word TEXT, + item TEXT, + ingkey TEXT, + count INTEGER, + PRIMARY KEY (id) +); +CREATE TABLE info ( + version_super INTEGER, + version_major INTEGER, + version_minor INTEGER, + last_access INTEGER, + rowid INTEGER NOT NULL, + PRIMARY KEY (rowid) +); +CREATE TABLE recipe ( + id INTEGER NOT NULL, + title TEXT, + instructions TEXT, + modifications TEXT, + cuisine TEXT, + rating INTEGER, + description TEXT, + source TEXT, + preptime INTEGER, + cooktime INTEGER, + servings FLOAT, + yields FLOAT, + yield_unit VARCHAR(32), + image BLOB, + thumb BLOB, + deleted BOOLEAN, + recipe_hash VARCHAR(32), + ingredient_hash VARCHAR(32), + link TEXT, + last_modified INTEGER, + PRIMARY KEY (id), + CHECK (deleted IN (0, 1)) +); +CREATE TABLE plugin_info ( + plugin TEXT, + id INTEGER NOT NULL, + version_super INTEGER, + version_major INTEGER, + version_minor INTEGER, + plugin_version VARCHAR(32), + PRIMARY KEY (id) +); +CREATE TABLE categories ( + id INTEGER NOT NULL, + recipe_id INTEGER, + category TEXT, + PRIMARY KEY (id), + FOREIGN KEY(recipe_id) REFERENCES recipe (id) +); +CREATE TABLE ingredients ( + id INTEGER NOT NULL, + recipe_id INTEGER, + refid INTEGER, + unit TEXT, + amount FLOAT, + rangeamount FLOAT, + item TEXT, + ingkey TEXT, + optional BOOLEAN, + shopoptional INTEGER, + inggroup TEXT, + position INTEGER, + deleted BOOLEAN, + PRIMARY KEY (id), + CHECK (deleted IN (0, 1)), + FOREIGN KEY(recipe_id) REFERENCES recipe (id), + FOREIGN KEY(refid) REFERENCES recipe (id), + CHECK (optional IN (0, 1)) +); +CREATE TABLE IF NOT EXISTS "pantry" ( +id INTEGER NOT NULL, +ingkey TEXT(32), +pantry BOOLEAN, +PRIMARY KEY (id), +CHECK (pantry IN (0, 1)) +); +CREATE TABLE unitdict ( +id INTEGER NOT NULL, +ukey VARCHAR(150), +value VARCHAR(150), +PRIMARY KEY (id) +); +CREATE TABLE crossunitdict ( +id INTEGER NOT NULL, +cukey VARCHAR(150), +value VARCHAR(150), +PRIMARY KEY (id) +); +CREATE TABLE density ( +id INTEGER NOT NULL, +dkey VARCHAR(150), +value VARCHAR(150), +PRIMARY KEY (id) +); +CREATE TABLE shopcatsorder ( +id INTEGER NOT NULL, +shopcategory TEXT(32), +position INTEGER, +PRIMARY KEY (id) +); +CREATE TABLE convtable ( +id INTEGER NOT NULL, +ckey VARCHAR(150), +value VARCHAR(150), +PRIMARY KEY (id) +); +CREATE TABLE IF NOT EXISTS "shopcats" ( + id INTEGER NOT NULL, + ingkey TEXT(32), + shopcategory TEXT, + position INTEGER, + PRIMARY KEY (id) +); +CREATE TABLE IF NOT EXISTS "recipe_ingredients" ("Recipe_id" integer not null, "ingredientHash_id" integer not null, unique ("ingredientHash_id")); diff --git a/src/main/java/com/mousetech/gourmetj/AdminMainBean.java b/src/main/java/com/mousetech/gourmetj/AdminMainBean.java index ec2fe5f..31a6311 100644 --- a/src/main/java/com/mousetech/gourmetj/AdminMainBean.java +++ b/src/main/java/com/mousetech/gourmetj/AdminMainBean.java @@ -101,17 +101,15 @@ public class AdminMainBean implements Serializable { public void resetSuggestions() { suggestionList = null; } - + public List searchSuggestionList(String query) { if (suggestionList == null) { switch (this.userSession.getSearchType()) { case rst_BY_CATEGORY: - suggestionList = - recipeService.findCategories(); + suggestionList = recipeService.findCategories(); break; case rst_BY_CUISINE: - suggestionList = - recipeService.findCuisines(); + suggestionList = recipeService.findCuisines(); break; default: suggestionList = new ArrayList(1); @@ -119,7 +117,7 @@ public class AdminMainBean implements Serializable { } return suggestionList; } - + /**/ transient DataModel searchResults; @@ -220,6 +218,14 @@ public class AdminMainBean implements Serializable { return "detailEdit?faces-redirect=true"; } + /** + * Navigate to "More features" page (shopping list and + * maint.) + */ + public String doMore() { + return "shoppingList.jsf"; + } + /** * Show selected recipe * diff --git a/src/main/java/com/mousetech/gourmetj/RecipeDetailBean.java b/src/main/java/com/mousetech/gourmetj/RecipeDetailBean.java index 9b6d326..5f95e1c 100644 --- a/src/main/java/com/mousetech/gourmetj/RecipeDetailBean.java +++ b/src/main/java/com/mousetech/gourmetj/RecipeDetailBean.java @@ -243,6 +243,9 @@ public class RecipeDetailBean implements Serializable { buildIngredientFacade(recipe.getIngredientHash())); stringifyCategories(recipe); + this.shop = this.getUserSession().getShoppingList() + .contains(recipe); + log.info("Set recipe: " + this.recipe); } @@ -262,8 +265,8 @@ public class RecipeDetailBean implements Serializable { } /** - * Categories are a Set attached to recipe. Build - * a displayable comma-separated list of them. + * Categories are a Set attached to recipe. Build a + * displayable comma-separated list of them. * * @param recipe Recipe to get categories from. * @@ -667,6 +670,26 @@ public class RecipeDetailBean implements Serializable { .getWrappedData(); } + private boolean shop = false; + + public boolean isShop() { + return shop; + } + + /** + * Add/remove recipe to shopping list (toggle) + */ + public void doShop() { + shop = !shop; + List shoppingList = + userSession.getShoppingList(); + if (shop) { + shoppingList.add(recipe); + } else { + shoppingList.remove(recipe); + } + } + /** * Save the recipe. * diff --git a/src/main/java/com/mousetech/gourmetj/ShopIngredient.java b/src/main/java/com/mousetech/gourmetj/ShopIngredient.java new file mode 100644 index 0000000..8d8321a --- /dev/null +++ b/src/main/java/com/mousetech/gourmetj/ShopIngredient.java @@ -0,0 +1,165 @@ +package com.mousetech.gourmetj; + +import com.mousetech.gourmetj.utils.IngredientDigester; +import com.mousetech.gourmetj.utils.IngredientDigester.IngredientAmountFormat; + +public class ShopIngredient implements Comparable { + /** + * Constructor. + * + * @param shopCat + * @param ingkey + */ + public ShopIngredient( String shopCat, String item, + String ingkey) { + this.shopCat = shopCat; + this.ingkey = ingkey; + } + + /** + * Constructor. + * + * @param amount + * @param unit + * @param item + * @param ingkey + * @param shopCat + */ + public ShopIngredient( Double amount, String unit, + String item, String ingkey, + String shopCat) { + this.amount = amount; + this.unit = unit; + this.item = item; + this.ingkey = ingkey; + this.shopCat = shopCat; + } + + private String shopCat; + + /** + * @return the shopCat + */ + public String getShopCat() { + return shopCat; + } + + /** + * @param shopCat the shopCat to set + */ + public void setShopCat(String shopCat) { + this.shopCat = shopCat; + } + + /** + * @return the ingkey + */ + public String getIngkey() { + return ingkey; + } + + /** + * @param ingkey the ingkey to set + */ + public void setIngkey(String ingkey) { + this.ingkey = ingkey; + } + + /** + * @return the amount + */ + public Double getAmount() { + return amount; + } + + /** + * @param amount the amount to set + */ + public void setAmount(Double amount) { + this.amount = amount; + } + + /** + * @return the displayAmount + */ + public String getDisplayAmount() { + Double amt = this.getAmount(); + if ( amt == null) { + return ""; + } + return IngredientDigester.displayAmount( + IngredientAmountFormat.IA_TEXT, amt, + null); + } + + /** + * @return the unit + */ + public String getUnit() { + return unit; + } + + /** + * @param unit the unit to set + */ + public void setUnit(String unit) { + this.unit = unit; + } + + private String item; + + /** + * @return the item + */ + public String getItem() { + return item; + } + + private String ingkey; + private Double amount; + private String displayAmount; + private String unit; + + @Override + public int compareTo(Object o) { + if ((o == null) || !(o instanceof ShopIngredient)) { + throw new RuntimeException( + "Invalid shipIngredient comparison"); + } + ShopIngredient o1 = (ShopIngredient) o; + int i = relate(this.getItem(), o1.getItem()); + if (i != 0) { + return i; + } + i = relate(this.getShopCat(), o1.getShopCat()); + if (i != 0) { + return i; + } + // TODO: normalize case, singular/plural/abbreviations + i = relate(this.getUnit(), o1.getUnit()); + if (i != 0) { + return i; + } + return i; // ZERO + } + + private int relate(String item2, String item3) { + if ((item2 == null) && (item3 == null)) { + return 0; + } + if (item2 == null) { + return -1; + } + if (item3 == null) { + return 1; + } + return item2.compareTo(item3); + } + + @Override + public String toString() { + return this.getAmount() + " " + this.getUnit() + " " + + this.getItem() + " " + this.getIngkey() + " " + + this.getShopCat(); + } +} diff --git a/src/main/java/com/mousetech/gourmetj/ShoppingListBean.java b/src/main/java/com/mousetech/gourmetj/ShoppingListBean.java new file mode 100644 index 0000000..6dbe31a --- /dev/null +++ b/src/main/java/com/mousetech/gourmetj/ShoppingListBean.java @@ -0,0 +1,397 @@ +package com.mousetech.gourmetj; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import javax.annotation.PostConstruct; +import javax.faces.view.ViewScoped; +import javax.inject.Inject; +import javax.inject.Named; + +import org.apache.commons.lang3.StringUtils; +import org.primefaces.model.ByteArrayContent; +import org.primefaces.model.DefaultStreamedContent; +import org.primefaces.model.StreamedContent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mousetech.gourmetj.persistence.dao.ShopcatRepository; +import com.mousetech.gourmetj.persistence.model.Ingredient; +import com.mousetech.gourmetj.persistence.model.Recipe; +import com.mousetech.gourmetj.persistence.model.Shopcat; +import com.mousetech.gourmetj.utils.YamlShoppingList; + +@Named +@ViewScoped +public class ShoppingListBean implements Serializable { + + public class RecipeReference { + + private int count; + private Recipe recipe; + + /** + * Constructor Constructor. + * + * @param r Recipe to reference (from Shopping List) + */ + public RecipeReference(Recipe r) { + count = 1; + recipe = r; + } + + /** + * @return the count + */ + public int getCount() { + return count; + } + + /** + * @param count the count to set + */ + public void setCount(int count) { + this.count = count; + } + + /** + * @return the recipe + */ + public Recipe getRecipe() { + return recipe; + } + + /** + * @param recipe the recipe to set + */ + public void setRecipe(Recipe recipe) { + this.recipe = recipe; + } + + } + + /** + * Serial version for session save/restore + */ + private static final long serialVersionUID = + 7449440266704831598L; + + /* Logger */ + + @SuppressWarnings("unused") + private static final Logger log = + LoggerFactory.getLogger(ShoppingListBean.class); + + @Inject + private UserSession userSession; + + private List siList; + + private List recipeList; + + @PostConstruct + public void init() { + // Load up details on recipes + this.siList = new ArrayList(30); + buildMaps(); + } + + public List getRecipeList() { + if (this.recipeList == null) { + this.recipeList = loadRecipeList(); + } + return this.recipeList; + } + + private List loadRecipeList() { + List list = + userSession.getShoppingList().stream() + .map(r -> new RecipeReference(r)) + .collect(Collectors.toList()); + return list; + } + + public List getIngredientList() { + return this.siList; + } + + private void buildMaps() { + this.siList = new ArrayList(30); + for (RecipeReference r : this.getRecipeList()) { + buildMapsFor(r); + } + // Now consolidate amounts and sort by + // shopcat/item/ingkey + optimizeIngredients(this.siList); + + this.siList.sort(new ShopclassComparator()); + } + + /** + * Run the ingredient list for the selected recipe and add + * them to siList + * + * @param r + * + * @see #buildMaps() + */ + private void buildMapsFor(RecipeReference r) { + final int multiplier = r.getCount(); + if (multiplier == 0) { + return; + } + for (Ingredient ing : r.getRecipe() + .getIngredientHash()) { + String ingkey = ing.getIngkey(); + if (StringUtils.isBlank(ingkey)) { + continue; + } + String shopCatName = ing.getShopCat() != null + ? ing.getShopCat().getShopcategory() + : null; + ShopIngredient sing; + try { + Double amt = ing.getAmount(); + if (multiplier > 1 && (amt != null)) { + amt *= multiplier; + } + sing = new ShopIngredient(amt, ing.getUnit(), + ing.getItem(), ing.getIngkey(), + shopCatName); + siList.add(sing); + } catch (Exception e) { + log.error("Unable to create ShopIngredient for " + + r.getRecipe() + " Ingredient " + ing); + e.printStackTrace(); + } + } + } + + /** + * Sort ShopIngredient list, then optimize it by + * consolidating amounts where possible. + * + * @param victim List to optimize + * + * #TestedBy @see ShoppingListBeanTest + */ + static void optimizeIngredients( + List victim) { + victim.sort(null); + for (int i = 0; i < (victim.size() - 1); i++) { + ShopIngredient si = victim.get(i); + ShopIngredient si2 = victim.get(i + 1); + if (si.compareTo(si2) == 0) { + si.setAmount(si.getAmount() + si2.getAmount()); + victim.remove(si2); // reduces size() + } + } + } + + class ShopclassComparator + implements Comparator { + + @Override + public int compare(ShopIngredient ing1, + ShopIngredient ing2) { + int i = 0; + i = relate(ing1.getShopCat(), ing2.getShopCat()); + if (i != 0) { + return i; + } + i = relate(ing1.getItem(), ing2.getItem()); + if (i != 0) { + return i; + } + i = relate(ing1.getIngkey(), ing2.getIngkey()); + if (i != 0) { + return i; + } + return 0; + } + + private int relate(String item2, String item3) { + if ((item2 == null) && (item3 == null)) { + return 0; + } + if (item2 == null) { + return -1; + } + if (item3 == null) { + return 1; + } + return item2.compareTo(item3); + } + } + + public StreamedContent getDlIngredientList() { + return YamlShoppingList + .createDownload(getIngredientList()); + } + + // ============================================= + private List shopcatList; + + public List getShopcatList() { + if (shopcatList == null) { + shopcatList = loadShopcatList(); + } + return shopcatList; + } + + @Inject + ShopcatRepository shopcatRepository; + + private List loadShopcatList() { + return shopcatRepository.findDistinctCategoryNative(); +// .findAllByOrderByShopcategoryAsc(); + } + + private Shopcat xeditShopcat = new Shopcat(); + + private String oldShopcategoryName; + + /** + * @return the editShopcat + */ + public Shopcat getEditShopcat() { + return xeditShopcat; + } + + public void doEditShopcat(int scId) { +// xeditShopcat = null; +// final List scl = getShopcatList(); +// for (Shopcat sc : scl) { +// if (sc.getId() == scId) { +// xeditShopcat = sc; +// this.oldShopcategoryName = sc.getShopcategory(); +// if (sc.getPosition() == null) { +// sc.setPosition(0); +// } +// return; +// } +// } +// log.error("SHOPCAT " + scId + " NOT FOUND"); + } + + public void ajaxOnClickShopcatIngkey() { + // Saves 1 shopcat/ingkey + this.shopcatRepository.save(xeditShopcat); + this.shopcatList = null; + } + + /** + * Updates all ingredient keys for shopcat name-change. Note + * that once done, this cannot be undone! + */ + public void ajaxOnClickShopcat() { + this.shopcatRepository.UpdateShopcats( + this.oldShopcategoryName, + xeditShopcat.getShopcategory()); + this.shopcatList = null; + } + + /** + * Primefaces AJAX listener for changes to amount values of + * recipes the recipe list. Forces re-computation of + * ingredient requirements. + */ + public void pfAmountChange() { + buildMaps(); + } + + // === + private String selectedShopcat; + + /** + * @return the selectedShopcat + */ + public String getSelectedShopcat() { + return selectedShopcat; + } + + /** + * @param selectedShopcat the selectedShopcat to set + */ + public void setSelectedShopcat(String selectedShopcat) { + this.selectedShopcat = selectedShopcat; + this.ingkeyList = null; + } + + private List selectedIngkey; + + /** + * @return the selectedIngkey + */ + public List getSelectedIngkey() { + return selectedIngkey; + } + + /** + * @param selectedIngkey the selectedIngkey to set + */ + public void setSelectedIngkey(List selectedIngkey) { + this.selectedIngkey = selectedIngkey; + } + + private List ingkeyList; + + /** + * @return the ingkeyList + */ + public List getIngkeyList() { + if (ingkeyList == null) { + ingkeyList = loadIngkeyListFor(selectedShopcat); + } + return ingkeyList; + } + + private List loadIngkeyListFor( + String selectedShopcat2) { + List list = this.shopcatRepository + .findByIngkeySorted(selectedShopcat2); + return list; + } + + private String newShopcat; + + /** + * @return the newShopcat + */ + public String getNewShopcat() { + return newShopcat; + } + + /** + * @param newShopcat the newShopcat to set + */ + public void setNewShopcat(String newShopcat) { + this.newShopcat = newShopcat; + } + + public List suggestShopcat(String query) { + return this.shopcatList; + } + + public void doChangeShopcat() { + String oldCat = this.getSelectedShopcat(); + String newCat = this.getNewShopcat(); + if (oldCat.equals(newCat)) { + return; // effective NO-OP + } + newCat = newCat.trim(); + if (StringUtils.isBlank(newCat)) { + this.shopcatRepository + .deleteShopcatFor(this.getSelectedIngkey()); + } else { + this.shopcatRepository.updateShopcatFor(newCat, + this.getSelectedIngkey()); + } + } + +} diff --git a/src/main/java/com/mousetech/gourmetj/UserSession.java b/src/main/java/com/mousetech/gourmetj/UserSession.java index ec8acb4..5d8e0c6 100644 --- a/src/main/java/com/mousetech/gourmetj/UserSession.java +++ b/src/main/java/com/mousetech/gourmetj/UserSession.java @@ -182,6 +182,13 @@ public class UserSession implements Serializable { // Session timeout, ms (25 minutes) long sessionTimeoutInterval = 25 * 60_000L; + /** + * When you click the "shop" button on a recipe, it + * gets added to this list. Note that it's the fully-expanded + * recipe! + */ + private List shoppingList = new ArrayList(); + /** * @return the sessionTimeoutInterval */ @@ -199,4 +206,8 @@ public class UserSession implements Serializable { log.warn("Session Idle listener logout"); return "/main.jsf"; } + + public List getShoppingList() { + return this.shoppingList ; + } } diff --git a/src/main/java/com/mousetech/gourmetj/persistence/dao/ShopcatRepository.java b/src/main/java/com/mousetech/gourmetj/persistence/dao/ShopcatRepository.java index 164fa39..576a784 100644 --- a/src/main/java/com/mousetech/gourmetj/persistence/dao/ShopcatRepository.java +++ b/src/main/java/com/mousetech/gourmetj/persistence/dao/ShopcatRepository.java @@ -2,15 +2,18 @@ package com.mousetech.gourmetj.persistence.dao; import java.util.List; +import javax.transaction.Transactional; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import com.mousetech.gourmetj.persistence.model.Shopcat; /** - * JpaRepository for Shopping Categories, which relate ManyToOne to Ingredient. - * Service method is @see RecipeService + * JpaRepository for Shopping Categories, which relate ManyToOne + * to Ingredient. Service method is @see RecipeService * * @author timh * @since Dec 28, 2021 @@ -28,7 +31,27 @@ public interface ShopcatRepository @Query(value = SQL_FIND_CATEGORIES, nativeQuery = true) public List findDistinctCategoryNative(); + @Transactional + @Query(value = "UPDATE Shopcat set shopcategory = :newCatname WHERE shopcategory = :oldCatname") + @Modifying + public int UpdateShopcats(String oldCatname, + String newCatname); + public Shopcat findShopcatByIngkey(String ingkey); public void deleteByIngkey(String key); + + public List findAllByOrderByShopcategoryAsc(); + + @Query(value = "SELECT s.ingkey FROM Shopcat s WHERE s.shopcategory = :shopcat ORDER BY s.ingkey") + public List findByIngkeySorted(String shopcat); + + @Transactional + @Query(value = "UPDATE Shopcat set shopcategory = :newCat WHERE ingkey IN :selectedIngkey") + @Modifying + public void updateShopcatFor(String newCat, List selectedIngkey); + + @Transactional + @Query(value = "DELETE Shopcat WHERE ingkey IN :selectedIngkey") + @Modifying public void deleteShopcatFor(List selectedIngkey); } diff --git a/src/main/java/com/mousetech/gourmetj/utils/YamlShoppingList.java b/src/main/java/com/mousetech/gourmetj/utils/YamlShoppingList.java new file mode 100644 index 0000000..61c23e9 --- /dev/null +++ b/src/main/java/com/mousetech/gourmetj/utils/YamlShoppingList.java @@ -0,0 +1,71 @@ +package com.mousetech.gourmetj.utils; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.primefaces.model.ByteArrayContent; +import org.primefaces.model.StreamedContent; + +import com.mousetech.gourmetj.ShopIngredient; + +/** + * Construct a Primefaces file output content for an ingredient + * list in YAML format. + * + * @author timh + * @since Jan 15, 2022 + */ + +public class YamlShoppingList { + + public static StreamedContent createDownload( + List ingredientList) { + ByteArrayOutputStream ary = new ByteArrayOutputStream(); + PrintWriter wtr = new PrintWriter(ary); + wtr.println("---"); + formatContent(wtr, ingredientList); + wtr.close(); + byte[] bas = ary.toByteArray(); + + StreamedContent dlList = new ByteArrayContent(bas, + "text/text", "shopping_list.yml"); + return dlList; + } + + /** + * Output line items in the ingredient list with topics for + * each Shopping Category. + * + * @param wtr Output Writer + * @param ingredientList Ingredient list to output. + */ + private static void formatContent(PrintWriter wtr, + List ingredientList) { + String oldShopcat = null; + for (ShopIngredient ing : ingredientList) { + String newShopcat = ing.getShopCat(); + if (StringUtils.isBlank(newShopcat)) { + newShopcat = "Unassigned"; + } + if (!StringUtils.equals(newShopcat, oldShopcat)) { + wtr.println(newShopcat + ":"); + oldShopcat = newShopcat; + } + wtr.print(" - "); + String displa = ing.getDisplayAmount(); + wtr.print(displa); + if (!displa.isBlank()) { + wtr.print(' '); + } + String unit = ing.getUnit(); + if (StringUtils.isNotBlank(unit)) { + wtr.print(unit); + wtr.print(' '); + } + wtr.println(ing.getItem()); + } + } + +} diff --git a/src/main/resources/META-INF/resources/WEB-INF/layout/layout.xhtml b/src/main/resources/META-INF/resources/WEB-INF/layout/layout.xhtml index 5f66996..800c7d5 100644 --- a/src/main/resources/META-INF/resources/WEB-INF/layout/layout.xhtml +++ b/src/main/resources/META-INF/resources/WEB-INF/layout/layout.xhtml @@ -21,18 +21,23 @@

Gourmet Recipe Manager (web version)

+ - - (C) 2021 Tim Holloway, Licensed under the Apache License, Version 2.0. -

Based on Gourmet Recipe Manager by T. Hinkle

- + + + + @@ -60,7 +65,7 @@ severity="alert" widgetVar="opError" > diff --git a/src/main/resources/META-INF/resources/WEB-INF/layout/misctabs/ingshopkey.xhtml b/src/main/resources/META-INF/resources/WEB-INF/layout/misctabs/ingshopkey.xhtml new file mode 100644 index 0000000..7296161 --- /dev/null +++ b/src/main/resources/META-INF/resources/WEB-INF/layout/misctabs/ingshopkey.xhtml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/css/style.css b/src/main/resources/META-INF/resources/css/style.css index 2c80602..fc7c387 100644 --- a/src/main/resources/META-INF/resources/css/style.css +++ b/src/main/resources/META-INF/resources/css/style.css @@ -13,7 +13,16 @@ font-weight: bold; } +.greenButton { + background-color: green !important; +} + textarea { font-family: 'latoregular', 'Trebuchet MS,Arial,Helvetica,sans-serif'; font-size: 1em +} + +#footer { + bottom: 90px; + width: 100%; } \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/editShopcatList.xhtml b/src/main/resources/META-INF/resources/editShopcatList.xhtml new file mode 100644 index 0000000..1d42743 --- /dev/null +++ b/src/main/resources/META-INF/resources/editShopcatList.xhtml @@ -0,0 +1,68 @@ + + + + Shopping Category + + + + + + + + + + + + + +
Ingredient key: + #{editShopcatBean.ingkey}
+ + + + + + + + + + + + +
+
+
+
+ \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/main.xhtml b/src/main/resources/META-INF/resources/main.xhtml index e3263c1..3106eb5 100644 --- a/src/main/resources/META-INF/resources/main.xhtml +++ b/src/main/resources/META-INF/resources/main.xhtml @@ -33,7 +33,9 @@ - + + + + diff --git a/src/main/resources/META-INF/resources/recipeDetails.xhtml b/src/main/resources/META-INF/resources/recipeDetails.xhtml index d3a8077..aaa6a54 100644 --- a/src/main/resources/META-INF/resources/recipeDetails.xhtml +++ b/src/main/resources/META-INF/resources/recipeDetails.xhtml @@ -57,7 +57,16 @@ styleClass="ui-button-print" immediate="true" /> - + + + + Gourmet Recipe Manager - Shopping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/com/mousetech/gourmetj/ShoppingListBeanTest.java b/src/test/java/com/mousetech/gourmetj/ShoppingListBeanTest.java new file mode 100644 index 0000000..a5eb3a7 --- /dev/null +++ b/src/test/java/com/mousetech/gourmetj/ShoppingListBeanTest.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2022, Tim Holloway + * + * Date written: Jan 12, 2022 + * Author: Tim Holloway + */ +package com.mousetech.gourmetj; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.mousetech.gourmetj.ShopIngredient; +import com.mousetech.gourmetj.persistence.model.Shopcat; + +/** + * @author timh + * @since Jan 12, 2022 + */ +class ShoppingListBeanTest { + + static List testList; + + /** + * @throws java.lang.Exception + */ + @BeforeAll + static void setUpBeforeClass() throws Exception { + testList = new ArrayList(); + testList.add(new ShopIngredient(2.0d, "cup", + "sugar", "sugar", "baking")); + testList.add(new ShopIngredient(1.5, "tsp", + "salt", "salt", "condiments")); + testList.add(new ShopIngredient(0.5, "tsp", + "pepper", "pepper", "condiments")); + testList.add(new ShopIngredient(2.0d, "cup", + "milk", "milk", "dairy")); + testList.add(new ShopIngredient(0.75d, "cup", + "sugar", "sugar", "baking")); + } + + @Test + void test() { + ShoppingListBean.optimizeIngredients(testList); + assertEquals(4, testList.size()); + } + +}