Merge branch 'shoppinglist'

Adds Shopping List feature to app.
This commit is contained in:
Tim Holloway 2022-01-16 07:21:57 -05:00
commit c80b8598d4
16 changed files with 1232 additions and 24 deletions

121
recipes.sql Normal file
View File

@ -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"));

View File

@ -106,12 +106,10 @@ public class AdminMainBean implements Serializable {
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<String>(1);
@ -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
*

View File

@ -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<Recipe> shoppingList =
userSession.getShoppingList();
if (shop) {
shoppingList.add(recipe);
} else {
shoppingList.remove(recipe);
}
}
/**
* Save the recipe.
*

View File

@ -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<Object> {
/**
* 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();
}
}

View File

@ -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<ShopIngredient> siList;
private List<RecipeReference> recipeList;
@PostConstruct
public void init() {
// Load up details on recipes
this.siList = new ArrayList<ShopIngredient>(30);
buildMaps();
}
public List<RecipeReference> getRecipeList() {
if (this.recipeList == null) {
this.recipeList = loadRecipeList();
}
return this.recipeList;
}
private List<RecipeReference> loadRecipeList() {
List<RecipeReference> list =
userSession.getShoppingList().stream()
.map(r -> new RecipeReference(r))
.collect(Collectors.toList());
return list;
}
public List<ShopIngredient> getIngredientList() {
return this.siList;
}
private void buildMaps() {
this.siList = new ArrayList<ShopIngredient>(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<ShopIngredient> 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<ShopIngredient> {
@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<String> shopcatList;
public List<String> getShopcatList() {
if (shopcatList == null) {
shopcatList = loadShopcatList();
}
return shopcatList;
}
@Inject
ShopcatRepository shopcatRepository;
private List<String> 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<Shopcat> 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<String> selectedIngkey;
/**
* @return the selectedIngkey
*/
public List<String> getSelectedIngkey() {
return selectedIngkey;
}
/**
* @param selectedIngkey the selectedIngkey to set
*/
public void setSelectedIngkey(List<String> selectedIngkey) {
this.selectedIngkey = selectedIngkey;
}
private List<String> ingkeyList;
/**
* @return the ingkeyList
*/
public List<String> getIngkeyList() {
if (ingkeyList == null) {
ingkeyList = loadIngkeyListFor(selectedShopcat);
}
return ingkeyList;
}
private List<String> loadIngkeyListFor(
String selectedShopcat2) {
List<String> 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<String> 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());
}
}
}

View File

@ -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<Recipe> shoppingList = new ArrayList<Recipe>();
/**
* @return the sessionTimeoutInterval
*/
@ -199,4 +206,8 @@ public class UserSession implements Serializable {
log.warn("Session Idle listener logout");
return "/main.jsf";
}
public List<Recipe> getShoppingList() {
return this.shoppingList ;
}
}

View File

@ -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<String> 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<Shopcat> findAllByOrderByShopcategoryAsc();
@Query(value = "SELECT s.ingkey FROM Shopcat s WHERE s.shopcategory = :shopcat ORDER BY s.ingkey")
public List<String> findByIngkeySorted(String shopcat);
@Transactional
@Query(value = "UPDATE Shopcat set shopcategory = :newCat WHERE ingkey IN :selectedIngkey")
@Modifying
public void updateShopcatFor(String newCat, List<String> selectedIngkey);
@Transactional
@Query(value = "DELETE Shopcat WHERE ingkey IN :selectedIngkey")
@Modifying public void deleteShopcatFor(List<String> selectedIngkey);
}

View File

@ -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<ShopIngredient> 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<ShopIngredient> 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());
}
}
}

View File

@ -21,18 +21,23 @@
<h1>
<ui:insert name="title">Gourmet Recipe Manager (web version)</ui:insert>
</h1>
<p:ajaxStatus onerror="PF('opError').show()"/>
<ui:insert name="content">
<ui:include src="content.xhtml" />
</ui:insert>
(C) 2021 Tim Holloway, Licensed under the <a
href="http://www.apache.org/licenses/LICENSE-2.0"
>Apache License, Version 2.0</a>.
<p>Based on Gourmet Recipe Manager by T. Hinkle</p>
<h:form id="ftmTimeout">
<!-- -->
<div id="footer">
(C) 2021 Tim Holloway, Licensed under the <a
href="http://www.apache.org/licenses/LICENSE-2.0"
>Apache License, Version 2.0</a>.
<p>Based on Gourmet Recipe Manager by T.
Hinkle</p>
</div>
<!-- -->
<h:form id="frmTimeout">
<p:idleMonitor
timeout="#{userSession.sessionTimeoutInterval}"
onidle="PF('sessionExpiredConfirmation').show()"
onidle="PF('dlgSessionExpired').show()"
>
<p:ajax event="idle"
listener="#{userSession.sessionIdleListener}"
@ -43,12 +48,12 @@
message="Your session has expired."
header="#{msgs['confirmDialog.initiatingDestroyProcess.label']}"
severity="alert"
widgetVar="sessionExpiredConfirmation"
widgetVar="dlgSessionExpired"
style="z-index: 25000"
>
<p:commandButton id="cmdExpiredOK"
value="OK" action="/main.jsf"
oncomplete="PF('sessionExpiredConfirmation').hide()"
oncomplete="PF('dlgSessionExpired').hide()"
/>
</p:confirmDialog>
</h:form>
@ -60,7 +65,7 @@
severity="alert" widgetVar="opError"
>
<p:commandButton value="OK"
oncomplete="PF('cd').hide()"
oncomplete="PF('opError').hide()"
/>
</p:confirmDialog>
</h:form>

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jstl"
>
<!-- === Edit ingkey/shopcat === -->
<h:form id="frmIsk">
<p:panelGrid>
<p:row>
<p:column>
<p:outputLabel for="ctlScSel"
value="Shopping Category"
/>
</p:column>
<p:column>
<p:outputLabel for="ctlIngkeySel"
value="Ingredient Key"
/>
</p:column>
</p:row>
<p:row style="vertical-align: top">
<p:column>
<p:selectOneListbox id="ctlScSel"
style="width: 240px"
value="#{shoppingListBean.selectedShopcat}"
>
<f:selectItems
value="#{shoppingListBean.shopcatList}"
/>
<p:ajax update="ctlIngkeySel" event="change" />
</p:selectOneListbox>
</p:column>
<p:column>
<h:selectManyListbox id="ctlIngkeySel"
style="width: 240px"
value="#{shoppingListBean.selectedIngkey}"
label="Ingcat"
>
<f:selectItems
value="#{shoppingListBean.ingkeyList}"
/>
<p:ajax event="change" update="ctlChangeCat"/>
</h:selectManyListbox>
</p:column>
</p:row>
<p:row>
<p:column>
<p:outputLabel
value="Change shopping category to:"
/>
</p:column>
<p:column>
<p:autoComplete
value="#{shoppingListBean.newShopcat}"
autoSelection="false" forceSelection="false"
maxResults="12"
completeMethod="#{shoppingListBean.suggestShopcat}"
/>
</p:column>
</p:row>
<p:row>
<p:column>
<p:commandButton id="ctlChangeCat" value="Change..."
disabled="#{empty shoppingListBean.selectedIngkey}"
onclick="PF('dlgOkRecat').show()"
/>
</p:column>
<p:column>
<h:outputText value="" />
</p:column>
</p:row>
</p:panelGrid>
</h:form>
<!-- -->
<h:form id="frmDelete">
<p:confirmDialog closable="false" id="dlgOkRecat"
header="Confirm Change - CANNOT UNDO"
message="OK to CHANGE Shopping Category for these Ingredient Keys?"
severity="alert" widgetVar="dlgOkRecat"
style="z-index: 25000"
>
<p:commandButton id="dlgOK" value="OK"
oncomplete="PF('dlgOkRecat').hide()"
action="#{shoppingListBean.doChangeShopcat}"
update="@form:@parent:frmIsk:ctlScSel @form:@parent:frmIsk:ctlIngkeySel"
immediate="true"
/>
<p:commandButton id="dlgCancel" value="Cancel"
onclick="PF('dlgOkRecat').hide()"
/>
</p:confirmDialog>
</h:form>
</html>

View File

@ -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%;
}

View File

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
>
<h:head>
<title>Shopping Category</title>
<style type="text/css">
html {
font-size: 14px;
}
</style>
</h:head>
<h:body>
<ui:component>
<h:form id="frmShopcat">
<p:panelGrid columns="1">
<p:dataTable id="tblShopcats"
style="width: 600px"
value="#{shoppingListBean.shopcatList}"
sortBy="#{item.shopCat}" var="item"
>
<p:headerRow>
<p:column colspan="4">
<h:outputText value="#{item.shopCat}" />
</p:column>
</p:headerRow>
</p:dataTable>
<div>Ingredient key:
#{editShopcatBean.ingkey}</div>
<p:outputLabel for="@next"
value="Category Name"
/>
<p:inputText id="ctlShopcat"
value="#{editShopcatBean.shopcatName}"
>
</p:inputText>
<h:outputText value="suggestion:" />
<p:selectOneMenu id="ctlShopcatMenu"
value="#{editShopcatBean.shopcatSuggestion}"
>
<f:selectItems
value="#{editShopcatBean.shopcatSuggestionList}"
/>
<p:ajax event="change"
listener="#{editShopcatBean.ajaxShopcatSuggest}"
update="ctlShopcat"
/>
</p:selectOneMenu>
<p:panelGrid columns="2" style="width: 100%">
<p:commandButton id="scDlgOK" value="OK"
style="width: 6em"
action="#{recipeDetailBean.doUpdateShopcat}"
update="form1:tabGroupClient:ingredientTable"
oncomplete="PF('editShopcatDlg').hide()"
/>
<p:commandButton id="scDlgCan"
value="Cancel" style="width: 6em"
onclick="PF('editShopcatDlg').hide()"
/>
</p:panelGrid>
</p:panelGrid>
</h:form>
</ui:component>
</h:body>
</html>

View File

@ -33,7 +33,9 @@
<f:selectItems
value="#{userSession.searchTypeList}"
/>
<p:ajax listener="#{adminMainBean.resetSuggestions}"/>
<p:ajax
listener="#{adminMainBean.resetSuggestions}"
/>
</p:selectOneMenu>
<p:commandButton id="ctlClear" value="Clear"
icon="ui-icon-close"
@ -43,6 +45,16 @@
<p:commandButton value="New Recipe"
action="#{adminMainBean.doNewRecipe}"
/>
<p:commandButton value="More..."
action="#{adminMainBean.doMore}"
/>
<h:outputText id="slistSSize"
style="margin-left: 2em"
value="#{userSession.shoppingList.size()}"
/>
<h:outputLabel for="slistSize"
value=" Recipes in Shopping List"
/>
</div>
</h:form>
<h:form id="form2">

View File

@ -57,7 +57,16 @@
styleClass="ui-button-print"
immediate="true"
/>
<p:outputLabel for="@next"
<p:commandButton id="ctlShop"
icon="ui-icon-cart"
value="Shop"
immediate="true"
styleClass="#{recipeDetailBean.shop ? 'greenButton' : null}"
action="#{recipeDetailBean.doShop}"
update="ctlShop"
/>
<h:outputText value=""/>
<p:outputLabel for="@next"
value="Categories:"
/>
<h:outputText
@ -197,8 +206,7 @@
<p:confirmDialog closable="false" id="okDeleteDlg"
header="Confirm Deletion"
message="OK to delete this recipe?"
severity="alert"
widgetVar="okDeleteDlg"
severity="alert" widgetVar="okDeleteDlg"
style="z-index: 25000"
>
<p:commandButton id="dlgOK" value="OK"

View File

@ -0,0 +1,141 @@
<?xml version="1.0"?>
<ui:composition template="/WEB-INF/layout/layout.xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jstl"
>
<ui:define name="title">Gourmet Recipe Manager - Shopping</ui:define>
<ui:define name="content">
<style>
.recipeTitle {
font-size: larger;
font-weight: bold;
}
.subtitle {
font-size: large;
font-weight: bold;
}
.ui-panelgrid-cell {
border-width: 0;
border-style: none;
}
.noRecipe {
text-decoration: line-through;
color: gray;
}
.plusRecipe {
}
</style>
<h:messages />
<p:tabView id="tabGroupClient" orientation="left"
dynamic="true"
>
<p:tab id="overviewTab" title="Shopping List">
<h:form id="form1">
<p:dataTable id="tblRecipes"
style="width: 600px"
value="#{shoppingListBean.recipeList}"
var="item"
>
<f:facet name="header">
<h:outputText value="Recipes" />
</f:facet>
<p:column style="width: 4em">
<p:spinner required="true" min="0"
value="#{item.count}" size="1"
>
<p:ajax
listener="#{shoppingListBean.pfAmountChange}"
update="@form:tblShopIngredients rname"
/>
</p:spinner>
</p:column>
<p:column>
<h:outputText id="rname"
styleClass="#{(item.count eq 0) ? 'noRecipe' :'plusRecipe' }"
value="#{item.recipe.title}"
/>
</p:column>
</p:dataTable>
<!-- ====== Ingredients To Buy ======================= -->
<p:column id="dlIng">
<p:commandButton value="Download List" ajax="false">
<p:fileDownload
value="#{shoppingListBean.dlIngredientList}"
/>
</p:commandButton>
</p:column>
<p:column id="ingredientsc"
style="width: 25%; vertical-align: top;"
>
<p:dataTable id="tblShopIngredients"
style="width: 600px; margin-top: 10px"
value="#{shoppingListBean.ingredientList}"
sortBy="#{item.shopCat}" var="item"
>
<f:facet name="header">
<h:outputText
styleClass="subtitle"
value="Ingredients"
/>
</f:facet>
<p:headerRow>
<p:column colspan="4">
<h:outputText
value="#{item.shopCat}"
/>
</p:column>
</p:headerRow>
<p:column label="Amt"
style="width: 3em; text-align: right"
>
<h:outputText
value="#{item.displayAmount}"
/>
</p:column>
<p:column label="Units"
style="width: 5em"
>
<h:outputText
value="#{item.unit}"
/>
</p:column>
<p:column label="Item"
style="width: 20em"
>
<h:outputText
value="#{item.item}"
/>
</p:column>
</p:dataTable>
</p:column>
</h:form>
</p:tab>
<!-- -->
<p:tab id="ingshopcatEditTab"
title="Edit Shopping Categories"
>
<ui:include
src="/WEB-INF/layout/misctabs/ingshopkey.xhtml"
/>
</p:tab>
<!-- -->
<p:tab id="tabImportExport" title="Import/Export">
<h:outputText value="For future implementation" />
</p:tab>
</p:tabView>
<h:form id="frmHome">
<p:commandButton id="doHome" value="Home"
icon="ui-icon-home" ajax="false" immediate="true"
action="main.jsf"
/>
</h:form>
</ui:define>
</ui:composition>

View File

@ -0,0 +1,52 @@
/**
* Copyright (C) 2022, Tim Holloway
*
* Date written: Jan 12, 2022
* Author: Tim Holloway <timh@mousetech.com>
*/
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<ShopIngredient> testList;
/**
* @throws java.lang.Exception
*/
@BeforeAll
static void setUpBeforeClass() throws Exception {
testList = new ArrayList<ShopIngredient>();
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());
}
}