add food substitutions

This commit is contained in:
Chris Scoggins
2022-02-03 15:04:46 -06:00
parent 5e3f94fcf7
commit 6ef25b604b
19 changed files with 322 additions and 49 deletions

View File

@ -417,7 +417,6 @@ export default {
// TODO: make this generic
let params = { pageSize: 50, random: true }
params[this.this_recipe_param] = item.id
console.log("RECIPE PARAM", this.this_recipe_param, params, item.id)
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params)
.then((result) => {
parent = this.findCard(item.id, this["items_" + col])

View File

@ -915,7 +915,6 @@ export default {
},
},
mounted() {
console.log(screen.height)
this.getShoppingList()
this.getSupermarkets()
this.getShoppingCategories()
@ -1406,7 +1405,6 @@ export default {
window.removeEventListener("offline", this.updateOnlineStatus)
},
addRecipeToShopping() {
console.log(this.new_recipe)
this.$bvModal.show(`shopping_${this.new_recipe.id}`)
},
finishShopping() {

View File

@ -100,7 +100,7 @@ export default {
this.loadInitial()
},
methods: {
loadInitial: function() {
loadInitial: function () {
let apiClient = new ApiApiFactory()
apiClient.listSupermarkets().then((results) => {
this.supermarkets = results.data
@ -110,7 +110,7 @@ export default {
this.selectable_categories = this.categories
})
},
selectedCategoriesChanged: function(data) {
selectedCategoriesChanged: function (data) {
let apiClient = new ApiApiFactory()
if ("removed" in data) {
@ -133,23 +133,22 @@ export default {
if ("moved" in data || "added" in data) {
this.supermarket_categories.forEach((element, index) => {
let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === element.id)[0]
console.log(relation)
apiClient.partialUpdateSupermarketCategoryRelation(relation.id, { order: index })
})
}
},
selectedSupermarketChanged: function(supermarket, id) {
selectedSupermarketChanged: function (supermarket, id) {
this.supermarket_categories = []
this.selectable_categories = this.categories
for (let i of supermarket.category_to_supermarket) {
this.supermarket_categories.push(i.category)
this.selectable_categories = this.selectable_categories.filter(function(el) {
this.selectable_categories = this.selectable_categories.filter(function (el) {
return el.id !== i.category.id
})
}
},
supermarketModalOk: function() {
supermarketModalOk: function () {
let apiClient = new ApiApiFactory()
if (this.selected_supermarket.new) {
apiClient.createSupermarket({ name: this.selected_supermarket.name }).then((results) => {
@ -160,7 +159,7 @@ export default {
apiClient.partialUpdateSupermarket(this.selected_supermarket.id, { name: this.selected_supermarket.name })
}
},
categoryModalOk: function() {
categoryModalOk: function () {
let apiClient = new ApiApiFactory()
if (this.selected_category.new) {
apiClient.createSupermarketCategory({ name: this.selected_category.name }).then((results) => {

View File

@ -1,13 +1,6 @@
<template>
<span>
<b-button
class="btn text-decoration-none fas px-1 py-0 border-0"
variant="link"
v-b-popover.hover.html
:title="[onhand ? $t('FoodOnHand', { food: item.name }) : $t('FoodNotOnHand', { food: item.name })]"
:class="[onhand ? 'text-success fa-clipboard-check' : 'text-muted fa-clipboard']"
@click="toggleOnHand"
/>
<b-button v-if="!item.ignore_shopping" class="btn text-decoration-none fas px-1 py-0 border-0" variant="link" v-b-popover.hover.html :title="Title" :class="IconClass" @click="toggleOnHand" />
</span>
</template>
@ -25,6 +18,26 @@ export default {
onhand: false,
}
},
computed: {
Title: function () {
if (this.onhand) {
return this.$t("FoodOnHand", { food: this.item.name })
} else if (this.item.substitute_onhand) {
return this.$t("SubstituteOnHand")
} else {
return this.$t("FoodNotOnHand", { food: this.item.name })
}
},
IconClass: function () {
if (this.onhand) {
return "text-success fa-clipboard-check"
} else if (this.item.substitute_onhand) {
return "text-warning fa-clipboard-check"
} else {
return "text-muted fa-clipboard"
}
},
},
mounted() {
this.onhand = this.item.food_onhand
},

View File

@ -33,6 +33,7 @@
</div>
</td>
<td v-else-if="show_shopping" class="text-right text-nowrap">
<shopping-badge v-if="ingredient.food.ignore_shopping" :item="shoppingBadgeFood" />
<b-button
v-if="!ingredient.food.ignore_shopping"
class="btn text-decoration-none fas fa-shopping-cart px-2 user-select-none"
@ -56,10 +57,11 @@
<script>
import { calculateAmount, ResolveUrlMixin, ApiMixin } from "@/utils/utils"
import OnHandBadge from "@/components/Badges/OnHand"
import ShoppingBadge from "@/components/Badges/Shopping"
export default {
name: "IngredientComponent",
components: { OnHandBadge },
components: { OnHandBadge, ShoppingBadge },
props: {
ingredient: Object,
ingredient_factor: { type: Number, default: 1 },
@ -87,6 +89,11 @@ export default {
this.shop = this.ingredient?.shop
},
computed: {
shoppingBadgeFood() {
// shopping badge is hidden when ignore_shopping=true.
// force true in this context to allow adding to shopping list from recipe view
return { ...this.ingredient.food, ignore_shopping: false }
},
ShoppingPopover() {
if (this.ingredient?.shopping_status == false) {
return this.$t("NotInShopping", { food: this.ingredient.food.name })

View File

@ -56,7 +56,6 @@ import "bootstrap-vue/dist/bootstrap-vue.css"
import IngredientComponent from "@/components/IngredientComponent"
import { ApiMixin, StandardToasts } from "@/utils/utils"
import ShoppingListViewVue from "../apps/ShoppingListView/ShoppingListView.vue"
Vue.use(BootstrapVue)

View File

@ -16,8 +16,11 @@
<small-text v-if="visibleCondition(f, 'smalltext')" :value="f.value" />
</div>
<template v-slot:modal-footer>
<div class="row w-100 justify-content-end">
<div class="col-auto">
<div class="row w-100">
<div class="col-6 align-self-end">
<b-form-checkbox v-if="advancedForm" sm switch v-model="show_advanced">{{ $t("Advanced") }}</b-form-checkbox>
</div>
<div class="col-auto justify-content-end">
<b-button class="mx-1" variant="secondary" v-on:click="cancelAction">{{ $t("Cancel") }}</b-button>
<b-button class="mx-1" variant="primary" v-on:click="doAction">{{ form.ok_label }}</b-button>
</div>
@ -78,7 +81,8 @@ export default {
form: {},
dirty: false,
special_handling: false,
show_help: true,
show_help: false,
show_advanced: false,
}
},
mounted() {
@ -86,6 +90,13 @@ export default {
this.$root.$on("change", this.storeValue) // bootstrap modal placed at document so have to listen at root of component
},
computed: {
advancedForm() {
return this.form.fields
.map((x) => {
return x?.advanced ?? false
})
.includes(true)
},
buttonLabel() {
return this.buttons[this.action].label
},
@ -268,6 +279,11 @@ export default {
visibleCondition(field, field_type) {
let type_match = field?.type == field_type
let checks = true
let show_advanced = true
if (field?.advanced) {
show_advanced = this.show_advanced
}
if (type_match && field?.condition) {
const value = this.item1[field?.condition?.field]
const preference = getUserPreference(field?.condition?.field)
@ -294,7 +310,7 @@ export default {
}
}
}
return type_match && checks
return type_match && checks && show_advanced
},
},
}

View File

@ -55,7 +55,7 @@
</b-input-group-prepend>
<b-form-spinbutton min="1" v-model="recipe_servings" inline style="height: 3em"></b-form-spinbutton>
<CustomInputSpinButton v-model.number="recipe_servings" style="height: 3em" />
<!-- <CustomInputSpinButton v-model.number="recipe_servings" style="height: 3em" /> -->
<b-input-group-append>
<b-button variant="secondary" @click="$bvModal.hide(`shopping_${modal_id}`)">{{ $t("Cancel") }} </b-button>
@ -76,11 +76,11 @@ const { ApiApiFactory } = require("@/utils/openapi/api")
import { StandardToasts } from "@/utils/utils"
import IngredientsCard from "@/components/IngredientsCard"
import LoadingSpinner from "@/components/LoadingSpinner"
import CustomInputSpinButton from "@/components/CustomInputSpinButton"
// import CustomInputSpinButton from "@/components/CustomInputSpinButton"
export default {
name: "ShoppingModal",
components: { IngredientsCard, LoadingSpinner, CustomInputSpinButton },
components: { IngredientsCard, LoadingSpinner },
mixins: [],
props: {
recipe: { required: true, type: Object },

View File

@ -289,13 +289,11 @@
"remember_search": "Remember Search",
"remember_hours": "Hours to Remember",
"tree_select": "Use Tree Selection",
"OnHand_help": "Food is in inventory and will not be automatically added to a shopping list.",
"OnHand_help": "Food is in inventory and will not be automatically added to a shopping list. Onhand status is shared with shopping users.",
"ignore_shopping_help": "Never add food to the shopping list (e.g. water)",
"shopping_category_help": "Supermarkets can be ordered and filtered by Shopping Category according to the layout of the aisles.",
"food_recipe_help": "Linking a recipe here will include the linked recipe in any other recipe that use this food",
"Foods": "Foods",
"review_shopping": "Review shopping entries before saving",
"view_recipe": "View Recipe",
"enable_expert": "Enable Expert Mode",
"expert_mode": "Expert Mode",
"simple_mode": "Simple Mode",
@ -326,6 +324,15 @@
"make_now": "Make Now",
"recipe_filter": "Recipe Filter",
"book_filter_help": "Include recipes from recipe filter instead of assigning each recipe",
"review_shopping": "Review shopping entries before saving",
"view_recipe": "View Recipe",
"filter": "Filter",
"reset_children": "Reset Child Inheritance",
"reset_children_help": "Overwrite all children with values from inherited fields."
"reset_children_help": "Overwrite all children with values from inherited fields.",
"substitute_help": "Substitutes are considered when searching for recipes that can be made with onhand ingredients.",
"substitute_siblings_help": "All food that share a parent of this food are considered substitutes.",
"substitute_children_help": "All food that are children of this food are considered substitutes.",
"substitute_siblings": "Substitute Siblings",
"substitute_children": "Substitute Children",
"SubstituteOnHand": "You have a substitute on hand."
}

View File

@ -76,7 +76,22 @@ export class Models {
// REQUIRED: unordered array of fields that can be set during create
create: {
// if not defined partialUpdate will use the same parameters, prepending 'id'
params: [["name", "description", "recipe", "food_onhand", "supermarket_category", "inherit", "inherit_fields", "ignore_shopping", "reset_inherit"]],
params: [
[
"name",
"description",
"recipe",
"food_onhand",
"supermarket_category",
"inherit",
"inherit_fields",
"ignore_shopping",
"substitute",
"substitute_siblings",
"substitute_children",
"reset_inherit",
],
],
form: {
show_help: true,
@ -126,8 +141,38 @@ export class Models {
allow_create: true,
help_text: i18n.t("shopping_category_help"),
},
substitute: {
form_field: true,
advanced: true,
type: "lookup",
multiple: true,
field: "substitute",
list: "FOOD",
label: i18n.t("Substitutes"),
allow_create: false,
help_text: i18n.t("substitute_help"),
},
substitute_siblings: {
form_field: true,
advanced: true,
type: "checkbox",
field: "substitute_siblings",
label: i18n.t("substitute_siblings"),
help_text: i18n.t("substitute_siblings_help"),
condition: { field: "parent", value: true, condition: "field_exists" },
},
substitute_children: {
form_field: true,
advanced: true,
type: "checkbox",
field: "substitute_children",
label: i18n.t("substitute_children"),
help_text: i18n.t("substitute_children_help"),
condition: { field: "numchild", value: 0, condition: "gt" },
},
inherit_fields: {
form_field: true,
advanced: true,
type: "lookup",
multiple: true,
field: "inherit_fields",
@ -137,6 +182,7 @@ export class Models {
},
reset_inherit: {
form_field: true,
advanced: true,
type: "checkbox",
field: "reset_inherit",
label: i18n.t("reset_children"),

View File

@ -307,6 +307,30 @@ export interface Food {
* @memberof Food
*/
ignore_shopping?: boolean;
/**
*
* @type {Array<FoodSubstitute>}
* @memberof Food
*/
substitute?: Array<FoodSubstitute> | null;
/**
*
* @type {boolean}
* @memberof Food
*/
substitute_siblings?: boolean;
/**
*
* @type {boolean}
* @memberof Food
*/
substitute_children?: boolean;
/**
*
* @type {string}
* @memberof Food
*/
substitute_onhand?: string;
}
/**
*
@ -423,6 +447,25 @@ export enum FoodShoppingUpdateDeleteEnum {
True = 'true'
}
/**
*
* @export
* @interface FoodSubstitute
*/
export interface FoodSubstitute {
/**
*
* @type {number}
* @memberof FoodSubstitute
*/
id?: number;
/**
*
* @type {string}
* @memberof FoodSubstitute
*/
name?: string;
}
/**
*
* @export
@ -709,6 +752,30 @@ export interface IngredientFood {
* @memberof IngredientFood
*/
ignore_shopping?: boolean;
/**
*
* @type {Array<FoodSubstitute>}
* @memberof IngredientFood
*/
substitute?: Array<FoodSubstitute> | null;
/**
*
* @type {boolean}
* @memberof IngredientFood
*/
substitute_siblings?: boolean;
/**
*
* @type {boolean}
* @memberof IngredientFood
*/
substitute_children?: boolean;
/**
*
* @type {string}
* @memberof IngredientFood
*/
substitute_onhand?: string;
}
/**
*
@ -5482,13 +5549,13 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
* @param {string} [_new] Returns new results first in search results. [true/&lt;b&gt;false&lt;/b&gt;]
* @param {number} [timescooked] Filter recipes cooked X times or more. Negative values returns cooked less than X times
* @param {string} [lastcooked] Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on or before date.
* @param {number} [makenow] Filter recipes that can be made with OnHand food. [true/&lt;b&gt;false&lt;/b&gt;]
* @param {string} [makenow] Filter recipes that can be made with OnHand food. [true/&lt;b&gt;false&lt;/b&gt;]
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listRecipes: async (query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, lastcooked?: string, makenow?: number, page?: number, pageSize?: number, options: any = {}): Promise<RequestArgs> => {
listRecipes: async (query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, lastcooked?: string, makenow?: string, page?: number, pageSize?: number, options: any = {}): Promise<RequestArgs> => {
const localVarPath = `/api/recipe/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -10068,13 +10135,13 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {string} [_new] Returns new results first in search results. [true/&lt;b&gt;false&lt;/b&gt;]
* @param {number} [timescooked] Filter recipes cooked X times or more. Negative values returns cooked less than X times
* @param {string} [lastcooked] Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on or before date.
* @param {number} [makenow] Filter recipes that can be made with OnHand food. [true/&lt;b&gt;false&lt;/b&gt;]
* @param {string} [makenow] Filter recipes that can be made with OnHand food. [true/&lt;b&gt;false&lt;/b&gt;]
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, lastcooked?: string, makenow?: number, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2004>> {
async listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, lastcooked?: string, makenow?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2004>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listRecipes(query, keywords, keywordsOr, keywordsAnd, keywordsOrNot, keywordsAndNot, foods, foodsOr, foodsAnd, foodsOrNot, foodsAndNot, units, rating, books, booksOr, booksAnd, booksOrNot, booksAndNot, internal, random, _new, timescooked, lastcooked, makenow, page, pageSize, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
@ -11823,13 +11890,13 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {string} [_new] Returns new results first in search results. [true/&lt;b&gt;false&lt;/b&gt;]
* @param {number} [timescooked] Filter recipes cooked X times or more. Negative values returns cooked less than X times
* @param {string} [lastcooked] Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on or before date.
* @param {number} [makenow] Filter recipes that can be made with OnHand food. [true/&lt;b&gt;false&lt;/b&gt;]
* @param {string} [makenow] Filter recipes that can be made with OnHand food. [true/&lt;b&gt;false&lt;/b&gt;]
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, lastcooked?: string, makenow?: number, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2004> {
listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, lastcooked?: string, makenow?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2004> {
return localVarFp.listRecipes(query, keywords, keywordsOr, keywordsAnd, keywordsOrNot, keywordsAndNot, foods, foodsOr, foodsAnd, foodsOrNot, foodsAndNot, units, rating, books, booksOr, booksAnd, booksOrNot, booksAndNot, internal, random, _new, timescooked, lastcooked, makenow, page, pageSize, options).then((request) => request(axios, basePath));
},
/**
@ -13605,14 +13672,14 @@ export class ApiApi extends BaseAPI {
* @param {string} [_new] Returns new results first in search results. [true/&lt;b&gt;false&lt;/b&gt;]
* @param {number} [timescooked] Filter recipes cooked X times or more. Negative values returns cooked less than X times
* @param {string} [lastcooked] Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on or before date.
* @param {number} [makenow] Filter recipes that can be made with OnHand food. [true/&lt;b&gt;false&lt;/b&gt;]
* @param {string} [makenow] Filter recipes that can be made with OnHand food. [true/&lt;b&gt;false&lt;/b&gt;]
* @param {number} [page] A page number within the paginated result set.
* @param {number} [pageSize] Number of results to return per page.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, lastcooked?: string, makenow?: number, page?: number, pageSize?: number, options?: any) {
public listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, lastcooked?: string, makenow?: string, page?: number, pageSize?: number, options?: any) {
return ApiApiFp(this.configuration).listRecipes(query, keywords, keywordsOr, keywordsAnd, keywordsOrNot, keywordsAndNot, foods, foodsOr, foodsAnd, foodsOrNot, foodsAndNot, units, rating, books, booksOr, booksAnd, booksOrNot, booksAndNot, internal, random, _new, timescooked, lastcooked, makenow, page, pageSize, options).then((request) => request(this.axios, this.basePath));
}