commit
c80b8598d4
16 changed files with 1232 additions and 24 deletions
@ -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")); |
@ -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(); |
||||
} |
||||
} |
@ -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()); |
||||
} |
||||
} |
||||
|
||||
} |
@ -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()); |
||||
} |
||||
} |
||||
|
||||
} |
@ -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> |
@ -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> |
@ -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> |
@ -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()); |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue