mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-06 14:48:02 -05:00
lots of shopping improvements
This commit is contained in:
@@ -1,141 +1,52 @@
|
||||
<template>
|
||||
<div class="swipe-container" :id="itemContainerId" @touchend="handleSwipe()"
|
||||
v-if="(useUserPreferenceStore().deviceSettings.shopping_show_checked_entries || !isChecked) && (useUserPreferenceStore().deviceSettings.shopping_show_delayed_entries || !isDelayed)"
|
||||
<v-list-item class="swipe-container" :id="itemContainerId" @touchend="handleSwipe()"
|
||||
v-if="(useUserPreferenceStore().deviceSettings.shopping_show_checked_entries || !isChecked) && (useUserPreferenceStore().deviceSettings.shopping_show_delayed_entries || !isDelayed)"
|
||||
@click="detail_modal_visible = true"
|
||||
>
|
||||
<div class="swipe-action" :class="{'bg-success': !isChecked , 'bg-warning': isChecked }">
|
||||
<i class="swipe-icon fa-fw fas" :class="{'fa-check': !isChecked , 'fa-cart-plus': isChecked }"></i>
|
||||
</div>
|
||||
<!-- <div class="swipe-action" :class="{'bg-success': !isChecked , 'bg-warning': isChecked }">-->
|
||||
<!-- <i class="swipe-icon fa-fw fas" :class="{'fa-check': !isChecked , 'fa-cart-plus': isChecked }"></i>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<v-btn-group class="swipe-element">
|
||||
<template #prepend>
|
||||
<v-btn color="primary" v-if="isDelayed">
|
||||
<i class="fa-fw fas fa-hourglass-half"></i>
|
||||
</v-btn>
|
||||
<div class="card flex-grow-1 btn-block p-2" @click="detail_modal_visible = true">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex flex-column pr-2" v-if="Object.keys(amounts).length> 0">
|
||||
<span v-for="a in amounts" v-bind:key="a.id">
|
||||
</template>
|
||||
|
||||
<span><i class="fas fa-check" v-if="a.checked && !isChecked"></i><i class="fas fa-hourglass-half" v-if="a.delayed && !a.checked"></i> <b>{{ a.amount }} {{
|
||||
a.unit
|
||||
}} </b></span>
|
||||
<br/></span>
|
||||
<div class="flex-grow-1 p-2">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex flex-column pr-2">
|
||||
<span v-for="[i, a] in amounts" v-bind:key="a">
|
||||
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-grow-1 align-self-center">
|
||||
{{ food.name }} <br/>
|
||||
<span v-if="info_row"><small class="text-muted">{{ info_row }}</small></span>
|
||||
</div>
|
||||
<span>
|
||||
<i class="fas fa-check" v-if="a.checked && !isChecked"></i>
|
||||
<i class="fas fa-hourglass-half" v-if="a.delayed && !a.checked"></i> <b>
|
||||
{{ a.amount }}
|
||||
{{ a.unit.name }} </b>
|
||||
</span>
|
||||
<br/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-grow-1 align-self-center">
|
||||
{{ food.name }} <br/>
|
||||
<span v-if="info_row"><small class="text-disabled">{{ info_row }}</small></span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<v-btn color="success" @click="useShoppingStore().setEntriesCheckedState(entries, !isChecked, true)"
|
||||
:class="{'btn-success': !isChecked, 'btn-warning': isChecked}">
|
||||
<i class="d-print-none fa-fw fas" :class="{'fa-check': !isChecked , 'fa-cart-plus': isChecked }"></i>
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
<div class="swipe-action bg-primary justify-content-end">
|
||||
<i class="fa-fw fas fa-hourglass-half swipe-icon"></i>
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="detail_modal_visible">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<h5> {{ food_row }}</h5>
|
||||
<small class="text-muted">{{ food.description }}</small>
|
||||
<template #append>
|
||||
<v-btn color="success" @click="useShoppingStore().setEntriesCheckedState(entries, !isChecked, true)"
|
||||
:class="{'btn-success': !isChecked, 'btn-warning': isChecked}" icon="fa-solid fa-check" variant="plain">
|
||||
</v-btn>
|
||||
<!-- <i class="d-print-none fa-fw fas" :class="{'fa-check': !isChecked , 'fa-cart-plus': isChecked }"></i>-->
|
||||
</template>
|
||||
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<!-- <div class="swipe-action bg-primary justify-content-end">-->
|
||||
<!-- <i class="fa-fw fas fa-hourglass-half swipe-icon"></i>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<h5 class="mt-2">{{ $t('Quick actions') }}</h5>
|
||||
{{ $t('Category') }}
|
||||
<v-select
|
||||
class="form-control mb-2"
|
||||
:items="useShoppingStore().supermarketCategories"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
return-object
|
||||
v-model="food.supermarket_category"
|
||||
@input="detail_modal_visible = false; updateFoodCategory(food)"
|
||||
></v-select>
|
||||
</v-list-item>
|
||||
|
||||
<v-btn color="info" block
|
||||
@click="detail_modal_visible = false;useShoppingStore().delayEntries(entries,!isDelayed, true)">
|
||||
{{ $t('Postpone') }}
|
||||
</v-btn>
|
||||
|
||||
|
||||
<h6 class="mt-2">{{ $t('Entries') }}</h6>
|
||||
|
||||
|
||||
<v-row v-for="e in entries" v-bind:key="e.id">
|
||||
<v-col cold="12">
|
||||
|
||||
<v-btn-group class="w-100">
|
||||
<div class="card flex-grow-1 btn-block p-2">
|
||||
<span><i class="fas fa-check" v-if="e.checked"></i><i class="fas fa-hourglass-half" v-if="e.delay_until !== null && !e.checked"></i>
|
||||
<b><span v-if="e.amount > 0">{{ e.amount }}</span> {{ e.unit?.name }}</b> {{ food.name }}</span>
|
||||
<span><small class="text-muted">
|
||||
<span v-if="e.recipe_mealplan && e.recipe_mealplan.recipe_name !== ''">
|
||||
<!-- TOOD used to be a link to view_recipe -->
|
||||
<a> <b> {{
|
||||
e.recipe_mealplan.recipe_name
|
||||
}} </b></a>({{
|
||||
e.recipe_mealplan.servings
|
||||
}} {{ $t('Servings') }})<br/>
|
||||
</span>
|
||||
<span v-if="e.recipe_mealplan && e.recipe_mealplan.mealplan_type !== undefined">
|
||||
{{ e.recipe_mealplan.mealplan_type }}
|
||||
{{ DateTime().fromJSDate(e.recipe_mealplan.mealplan_from_date).toLocaleString(DateTime.DATETIME_SHORT)}}
|
||||
<br/>
|
||||
</span>
|
||||
|
||||
{{ e.created_by.display_name }} {{ DateTime().fromJSDate(e.created_at).toLocaleString(DateTime.DATETIME_SHORT) }}<br/>
|
||||
</small>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
<v-btn color="error"
|
||||
@click="useShoppingStore().deleteObject(e)"><i
|
||||
class="fas fa-trash"></i></v-btn>
|
||||
</v-btn-group>
|
||||
|
||||
<!-- TODO implement -->
|
||||
<!-- <generic-multiselect-->
|
||||
<!-- class="mt-1"-->
|
||||
<!-- v-if="e.recipe_mealplan === null"-->
|
||||
<!-- :initial_single_selection="e.unit"-->
|
||||
<!-- :model="Models.UNIT"-->
|
||||
<!-- :multiple="false"-->
|
||||
<!-- @change="e.unit = $event.val; useShoppingListStore().updateObject(e)"-->
|
||||
<!-- >-->
|
||||
<!-- </generic-multiselect>-->
|
||||
|
||||
<!-- <number-scaler-component :number="e.amount"-->
|
||||
<!-- @change="e.amount = $event; useShoppingListStore().updateObject(e)"-->
|
||||
<!-- v-if="e.recipe_mealplan === null"></number-scaler-component>-->
|
||||
<hr class="m-2"/>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
<v-btn color="success" block @click="useShoppingListStore().createObject({ amount: 0, unit: null, food: food, })"> {{ $t("Add") }}</v-btn>
|
||||
<v-btn color="warning" block @click="detail_modal_visible = false; setFoodIgnoredAndChecked(food)"> {{ $t("Ignore_Shopping") }}</v-btn>
|
||||
<v-btn color="danger" block class="mt-2"
|
||||
@click="detail_modal_visible = false;useShoppingListStore().deleteEntries(entries)">
|
||||
{{ $t('Delete_All') }}
|
||||
</v-btn>
|
||||
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
|
||||
</v-dialog>
|
||||
|
||||
<!-- <generic-modal-form :model="Models.FOOD" :show="editing_food !== null"-->
|
||||
<!-- @hidden="editing_food = null; useShoppingListStore().refreshFromAPI()"></generic-modal-form>-->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -147,6 +58,7 @@ import {useShoppingStore} from "@/stores/ShoppingStore.js";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore.js";
|
||||
import {ApiApi, Food, ShoppingListEntry} from '@/openapi'
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
||||
import {ShoppingLineAmount} from "@/types/Shopping";
|
||||
|
||||
const props = defineProps({
|
||||
entries: {type: [] as PropType<ShoppingListEntry[]>, required: true},
|
||||
@@ -187,14 +99,18 @@ const food = computed(() => {
|
||||
return props.entries[Object.keys(props.entries)[0]]['food']
|
||||
})
|
||||
|
||||
|
||||
const amounts = computed(() => {
|
||||
let unit_amounts = {}
|
||||
/**
|
||||
* calculate the amounts for the given line
|
||||
* can combine 1 to n entries with the same unit
|
||||
* can contain more 0 to n different entries for different units
|
||||
*/
|
||||
const amounts = computed((): Map<number, ShoppingLineAmount> => {
|
||||
let unitAmounts = new Map<number, ShoppingLineAmount>()
|
||||
|
||||
for (let i in props.entries) {
|
||||
let e = props.entries[i]
|
||||
|
||||
if (!e.checked && e.delayUntil === null
|
||||
if (!e.checked && (e.delayUntil == null)
|
||||
|| (e.checked && useUserPreferenceStore().deviceSettings.shopping_show_checked_entries)
|
||||
|| (e.delayUntil !== null && useUserPreferenceStore().deviceSettings.shopping_show_delayed_entries)) {
|
||||
|
||||
@@ -202,25 +118,23 @@ const amounts = computed(() => {
|
||||
if (e.unit !== undefined && e.unit !== null) {
|
||||
unit = e.unit.id!
|
||||
}
|
||||
if (e.amount > 0) {
|
||||
if (unit in unit_amounts) {
|
||||
unit_amounts[unit]['amount'] += e.amount
|
||||
} else {
|
||||
if (unit === -1) {
|
||||
unit_amounts[unit] = {id: -1, unit: "", amount: e.amount, checked: e.checked, delayed: (e.delayUntil !== null)}
|
||||
} else {
|
||||
unit_amounts[unit] = {id: e.unit.id, unit: e.unit.name, amount: e.amount, checked: e.checked, delayed: (e.delayUntil !== null)}
|
||||
}
|
||||
|
||||
if (e.amount > 0) {
|
||||
|
||||
if (unitAmounts.get(unit) != undefined) {
|
||||
unitAmounts.get(unit)!.amount += e.amount
|
||||
} else {
|
||||
unitAmounts.set(unit, {
|
||||
amount: e.amount,
|
||||
unit: e.unit,
|
||||
checked: e.checked,
|
||||
delayed: (e.delayUntil != null)
|
||||
} as ShoppingLineAmount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return unit_amounts
|
||||
})
|
||||
|
||||
const food_row = computed(() => {
|
||||
return food.value.name
|
||||
return unitAmounts
|
||||
})
|
||||
|
||||
const info_row = computed(() => {
|
||||
@@ -237,7 +151,6 @@ const info_row = computed(() => {
|
||||
authors.push(e.createdBy.displayName)
|
||||
}
|
||||
|
||||
|
||||
if (e.recipeMealplan !== null) {
|
||||
let recipe_name = e.recipeMealplan.recipeName
|
||||
if (recipes.indexOf(recipe_name) === -1) {
|
||||
@@ -307,63 +220,20 @@ function setFoodIgnoredAndChecked(food: Food) {
|
||||
* check if min distance is reached and execute desired action
|
||||
*/
|
||||
function handleSwipe() {
|
||||
const minDistance = 80;
|
||||
const container = document.querySelector('#' + itemContainerId.value);
|
||||
// get the distance the user swiped
|
||||
const swipeDistance = container.scrollLeft - container.clientWidth;
|
||||
if (swipeDistance < minDistance * -1) {
|
||||
useShoppingStore().setEntriesCheckedState(props.entries, !isChecked.value, true)
|
||||
} else if (swipeDistance > minDistance) {
|
||||
useShoppingStore().delayEntries(props.entries, !isDelayed.value, true)
|
||||
}
|
||||
//
|
||||
// const minDistance = 80;
|
||||
// const container = document.querySelector('#' + itemContainerId.value);
|
||||
// // get the distance the user swiped
|
||||
// const swipeDistance = container!.scrollLeft - container!.clientWidth;
|
||||
// if (swipeDistance < minDistance * -1) {
|
||||
// useShoppingStore().setEntriesCheckedState(props.entries, !isChecked.value, true)
|
||||
// } else if (swipeDistance > minDistance) {
|
||||
// useShoppingStore().delayEntries(props.entries, !isDelayed.value, true)
|
||||
// }
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style>
|
||||
/* scroll snap takes care of restoring scroll position */
|
||||
.swipe-container {
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
overflow-x: scroll;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
/* scrollbar should be hidden */
|
||||
.swipe-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.swipe-container {
|
||||
scrollbar-width: none; /* For Firefox */
|
||||
}
|
||||
|
||||
/* main element should always snap into view */
|
||||
.swipe-element {
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.swipe-icon {
|
||||
color: white;
|
||||
position: sticky;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
/* swipe-actions and element should be 100% wide */
|
||||
.swipe-action,
|
||||
.swipe-element {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.swipe-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
</style>
|
||||
/* TODO swipe system classes removed because not working (visually, touch detection was working), retrieve from old ShoppingLineItem VCS */
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-tabs v-model="currentTab" grow>
|
||||
<v-tabs v-model="currentTab" density="compact">
|
||||
<v-tab value="shopping"><i class="fas fa-shopping-cart fa-fw"></i> <span class="d-none d-md-block ms-1">{{ $t('Shopping_list') }}</span></v-tab>
|
||||
<v-tab value="recipes"><i class="fas fa-book fa-fw"></i> <span class="d-none d-md-block ms-1">{{ $t('Recipes') }}</span></v-tab>
|
||||
</v-tabs>
|
||||
@@ -9,9 +9,10 @@
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field :label="$t('Shopping_input_placeholder')" @keyup.enter="addIngredient()" v-model="ingredientInput">
|
||||
<v-text-field :label="$t('Shopping_input_placeholder')" density="compact" @keyup.enter="addIngredient()" v-model="ingredientInput" hide-details>
|
||||
<template #append>
|
||||
<v-btn
|
||||
density="comfortable"
|
||||
@click="addIngredient()"
|
||||
:icon="ingredientInputIcon"
|
||||
color="create"
|
||||
@@ -19,14 +20,20 @@
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-list lines="two" density="compact">
|
||||
<v-list class="mt-3" density="compact">
|
||||
<template v-for="category in useShoppingStore().getEntriesByGroup" :key="category.name">
|
||||
<template v-if="(category.stats.countUnchecked > 0 || useUserPreferenceStore().deviceSettings.shopping_show_checked_entries)
|
||||
&& (category.stats.countUnchecked + category.stats.countChecked) > 0
|
||||
&& (category.stats.countUncheckedDelayed < category.stats.countUnchecked || useUserPreferenceStore().deviceSettings.shopping_show_delayed_entries)">
|
||||
|
||||
<template v-for="category in useShoppingStore().getEntriesByGroup">
|
||||
<v-list-subheader>{{ category.name }}</v-list-subheader>
|
||||
<v-divider></v-divider>
|
||||
<v-list-subheader v-if="category.name === useShoppingStore().UNDEFINED_CATEGORY"><i>{{ $t('NoCategory') }}</i></v-list-subheader>
|
||||
<v-list-subheader v-else>{{ category.name }}</v-list-subheader>
|
||||
<v-divider></v-divider>
|
||||
|
||||
<template v-for="[i, value] in category.foods" :key="i">
|
||||
<shopping-line-item :entries="Array.from(value.entries.values())"></shopping-line-item>
|
||||
</template>
|
||||
|
||||
<template v-for="[i, value] in category.foods" :key="i">
|
||||
<shopping-line-item :entries="Array.from(value.entries.values())"></shopping-line-item>
|
||||
</template>
|
||||
</template>
|
||||
</v-list>
|
||||
@@ -48,6 +55,7 @@ import {useShoppingStore} from "@/stores/ShoppingStore";
|
||||
import {ApiApi, Food, IngredientString, ShoppingListEntry, Unit} from "@/openapi";
|
||||
import {ErrorMessageType, useMessageStore} from "@/stores/MessageStore";
|
||||
import ShoppingLineItem from "@/components/display/ShoppingLineItem.vue";
|
||||
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
|
||||
|
||||
const currentTab = ref("shopping")
|
||||
|
||||
@@ -58,6 +66,9 @@ onMounted(() => {
|
||||
useShoppingStore().refreshFromAPI()
|
||||
})
|
||||
|
||||
/**
|
||||
* add new ingredient from ingredient text input
|
||||
*/
|
||||
function addIngredient() {
|
||||
const api = new ApiApi()
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
"New_Unit": "Нова единица",
|
||||
"Next_Day": "Следващия ден",
|
||||
"Next_Period": "Следващ период",
|
||||
"NoCategory": "Няма избрана категория.",
|
||||
"NoCategory": "",
|
||||
"No_ID": "Идентификатора не е намерен, не може да се изтрие.",
|
||||
"No_Results": "Няма резултати",
|
||||
"NotFound": "",
|
||||
|
||||
@@ -240,7 +240,7 @@
|
||||
"New_Unit": "Nová jednotka",
|
||||
"Next_Day": "Následující den",
|
||||
"Next_Period": "Další období",
|
||||
"NoCategory": "Není vybrána žádná kategorie.",
|
||||
"NoCategory": "",
|
||||
"No_ID": "ID nenalezeno, odstranění není možné.",
|
||||
"No_Results": "Žádné výsledky",
|
||||
"NotFound": "",
|
||||
|
||||
@@ -225,7 +225,7 @@
|
||||
"New_Unit": "Ny enhed",
|
||||
"Next_Day": "Næste dag",
|
||||
"Next_Period": "Næste periode",
|
||||
"NoCategory": "Ingen kategori valgt.",
|
||||
"NoCategory": "",
|
||||
"No_ID": "ID findes ikke, kan ikke slette.",
|
||||
"No_Results": "Ingen resultater",
|
||||
"NotFound": "",
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
"New_Unit": "Neue Einheit",
|
||||
"Next_Day": "Tag vor",
|
||||
"Next_Period": "nächster Zeitraum",
|
||||
"NoCategory": "Keine Kategorie ausgewählt.",
|
||||
"NoCategory": "Ohne Kategorie",
|
||||
"NoMoreUndo": "Rückgängig: Keine Änderungen",
|
||||
"No_ID": "ID nicht gefunden und kann nicht gelöscht werden.",
|
||||
"No_Results": "Keine Ergebnisse",
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
"New_Unit": "Νέα μονάδα μέτρησης",
|
||||
"Next_Day": "Επόμενη μέρα",
|
||||
"Next_Period": "Επόμενη περίοδος",
|
||||
"NoCategory": "Δεν έχει επιλεγεί κατηγορία.",
|
||||
"NoCategory": "",
|
||||
"No_ID": "Το ID δεν βρέθηκε, αδύνατη η διαγραφή.",
|
||||
"No_Results": "Δεν υπάρχουν αποτελέσματα",
|
||||
"NotFound": "",
|
||||
|
||||
@@ -242,7 +242,7 @@
|
||||
"New_Unit": "New Unit",
|
||||
"Next_Day": "Next Day",
|
||||
"Next_Period": "Next Period",
|
||||
"NoCategory": "No category selected.",
|
||||
"NoCategory": "No Category",
|
||||
"NoMoreUndo": "No changes to be undone.",
|
||||
"No_ID": "ID not found, cannot delete.",
|
||||
"No_Results": "No Results",
|
||||
|
||||
@@ -242,7 +242,7 @@
|
||||
"New_Unit": "Nueva unidad",
|
||||
"Next_Day": "Siguiente Día",
|
||||
"Next_Period": "Siguiente Período",
|
||||
"NoCategory": "No se ha seleccionado categoría.",
|
||||
"NoCategory": "",
|
||||
"NoMoreUndo": "No hay cambios que deshacer.",
|
||||
"No_ID": "No se ha encontrado el ID, no se puede borrar.",
|
||||
"No_Results": "No hay resutado",
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
"New_Unit": "Uusi Yksikkö",
|
||||
"Next_Day": "Seuraava Päivä",
|
||||
"Next_Period": "Seuraava Jakso",
|
||||
"NoCategory": "",
|
||||
"No_ID": "Poistaminen epäonnistui, ID:tä ei löytynyt.",
|
||||
"No_Results": "Ei Tuloksia",
|
||||
"NotFound": "",
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
"New_Unit": "Nouvelle unité",
|
||||
"Next_Day": "Prochain jour",
|
||||
"Next_Period": "Prochaine période",
|
||||
"NoCategory": "Pas de catégorie sélectionnée.",
|
||||
"NoCategory": "",
|
||||
"NoMoreUndo": "Aucun changement à annuler.",
|
||||
"No_ID": "ID introuvable, impossible de supprimer.",
|
||||
"No_Results": "Aucun résultat",
|
||||
|
||||
@@ -242,7 +242,7 @@
|
||||
"New_Unit": "יחידה חדשה",
|
||||
"Next_Day": "היום הבא",
|
||||
"Next_Period": "התקופה הבאה",
|
||||
"NoCategory": "לא נבחרה קטגוריה.",
|
||||
"NoCategory": "",
|
||||
"NoMoreUndo": "אין עוד שינויים לשחזור.",
|
||||
"No_ID": "מזהה לא נמצא, בלתי ניתן למחיקה.",
|
||||
"No_Results": "אין תוצאות",
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
"New_Unit": "Új mennyiségi egység",
|
||||
"Next_Day": "Következő nap",
|
||||
"Next_Period": "Következő periódus",
|
||||
"NoCategory": "Nincs kategória kiválasztva.",
|
||||
"NoCategory": "",
|
||||
"No_ID": "Azonosító nem található, ezért nem törölhető.",
|
||||
"No_Results": "Nincsenek találatok",
|
||||
"NotFound": "",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"New_Food": "Նոր սննդամթերք",
|
||||
"New_Keyword": "Նոր բանալի բառ",
|
||||
"New_Recipe": "Նոր բաղադրատոմս",
|
||||
"NoCategory": "",
|
||||
"No_Results": "Արդյունքներ չկան",
|
||||
"NotFound": "",
|
||||
"NotFoundHelp": "",
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
"New_Unit": "Nuova unità di misura",
|
||||
"Next_Day": "Giorno successivo",
|
||||
"Next_Period": "Periodo successivo",
|
||||
"NoCategory": "Nessuna categoria selezionata.",
|
||||
"NoCategory": "",
|
||||
"No_ID": "ID non trovato, non è possibile eliminare.",
|
||||
"No_Results": "Nessun risultato",
|
||||
"NotFound": "",
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
"New_Unit": "Ny Enhet",
|
||||
"Next_Day": "Neste dag",
|
||||
"Next_Period": "Neste periode",
|
||||
"NoCategory": "Ingen kategori valgt.",
|
||||
"NoCategory": "",
|
||||
"No_ID": "ID ikke funnet, kan ikke slette.",
|
||||
"No_Results": "Ingen resultat",
|
||||
"NotFound": "",
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
"New_Unit": "Nieuwe Eenheid",
|
||||
"Next_Day": "Volgende dag",
|
||||
"Next_Period": "Volgende periode",
|
||||
"NoCategory": "Geen categorie geselecteerd.",
|
||||
"NoCategory": "",
|
||||
"No_ID": "ID niet gevonden, verwijderen niet mogelijk.",
|
||||
"No_Results": "Geen resultaten",
|
||||
"NotFound": "",
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
"New_Unit": "Nowa jednostka",
|
||||
"Next_Day": "Następny dzień",
|
||||
"Next_Period": "Następny okres",
|
||||
"NoCategory": "Nie wybrano kategorii.",
|
||||
"NoCategory": "",
|
||||
"NoMoreUndo": "Brak zmian do wycofania.",
|
||||
"No_ID": "ID nie znaleziono, nie można usunąć.",
|
||||
"No_Results": "Brak wyników",
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
"New_Unit": "Nova Unidade",
|
||||
"Next_Day": "Dia seguinte",
|
||||
"Next_Period": "Próximo período",
|
||||
"NoCategory": "Nenhuma categoria selecionada.",
|
||||
"NoCategory": "",
|
||||
"No_ID": "identificação não encontrada, impossível eliminar.",
|
||||
"No_Results": "Sem resultados",
|
||||
"NotFound": "",
|
||||
|
||||
@@ -232,7 +232,7 @@
|
||||
"New_Unit": "Nova Unidade",
|
||||
"Next_Day": "Próximo Dia",
|
||||
"Next_Period": "Próximo Período",
|
||||
"NoCategory": "Nenhuma categoria selecionada.",
|
||||
"NoCategory": "",
|
||||
"NoMoreUndo": "Nenhuma alteração para desfazer.",
|
||||
"No_ID": "ID não encontrado, impossível deletar.",
|
||||
"No_Results": "Sem Resultados",
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
"New_Unit": "Unitate nouă",
|
||||
"Next_Day": "Ziua următoare",
|
||||
"Next_Period": "Perioada următoare",
|
||||
"NoCategory": "Nicio categorie selectată.",
|
||||
"NoCategory": "",
|
||||
"No_ID": "ID-ul nu a fost găsit, nu se poate șterge.",
|
||||
"No_Results": "Fără rezultate",
|
||||
"NotFound": "",
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
"New_Unit": "Новая единица",
|
||||
"Next_Day": "Следующий день",
|
||||
"Next_Period": "Следующий период",
|
||||
"NoCategory": "Категория не выбрана.",
|
||||
"NoCategory": "",
|
||||
"No_ID": "ID не найден, удаление не возможно.",
|
||||
"No_Results": "Результаты отсутствуют",
|
||||
"NotFound": "",
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
"New_Unit": "Nova enota",
|
||||
"Next_Day": "Naslednji Dan",
|
||||
"Next_Period": "Naslednje obdobje",
|
||||
"NoCategory": "Nobena kategorija ni izbrana.",
|
||||
"NoCategory": "",
|
||||
"No_ID": "ID ni najden, ne morem izbrisati.",
|
||||
"No_Results": "Ni rezultatov",
|
||||
"NotFound": "",
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
"New_Unit": "Ny enhet",
|
||||
"Next_Day": "Nästa dag",
|
||||
"Next_Period": "Nästa period",
|
||||
"NoCategory": "Ingen kategori vald.",
|
||||
"NoCategory": "",
|
||||
"NoMoreUndo": "Inga ändringar att ångra.",
|
||||
"No_ID": "ID hittades inte, kan inte radera.",
|
||||
"No_Results": "Inget resultat",
|
||||
|
||||
@@ -242,7 +242,7 @@
|
||||
"New_Unit": "Yeni Birim",
|
||||
"Next_Day": "Sonraki Gün",
|
||||
"Next_Period": "Sonraki Dönem",
|
||||
"NoCategory": "Hiçbir kategori seçilmedi.",
|
||||
"NoCategory": "",
|
||||
"NoMoreUndo": "Yapılacak değişiklik yok.",
|
||||
"No_ID": "ID bulunamadı, silinemez.",
|
||||
"No_Results": "Sonuç Yok",
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
"New_Unit": "Нова Одиниця",
|
||||
"Next_Day": "Наступний День",
|
||||
"Next_Period": "Наступний період",
|
||||
"NoCategory": "Жодна категорія не вибрана.",
|
||||
"NoCategory": "",
|
||||
"No_ID": "ID не знайдено, неможливо видалити.",
|
||||
"No_Results": "Немає Результату",
|
||||
"NotFound": "",
|
||||
|
||||
@@ -237,7 +237,7 @@
|
||||
"New_Unit": "新建单位",
|
||||
"Next_Day": "第二天",
|
||||
"Next_Period": "下期",
|
||||
"NoCategory": "未选择分类。",
|
||||
"NoCategory": "",
|
||||
"NoMoreUndo": "没有可撤消的更改。",
|
||||
"No_ID": "未找到标识,不能删除。",
|
||||
"No_Results": "没有结果",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"Monday": "",
|
||||
"New": "",
|
||||
"New_Recipe": "",
|
||||
"NoCategory": "",
|
||||
"NotFound": "",
|
||||
"NotFoundHelp": "",
|
||||
"Nutrition": "",
|
||||
|
||||
@@ -22,17 +22,12 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
|
||||
let supermarketCategories = ref([] as SupermarketCategory[])
|
||||
let supermarkets = ref([] as Supermarket[])
|
||||
|
||||
// TODO move into special type for structure ?
|
||||
let total_unchecked = ref(0)
|
||||
let total_checked = ref(0)
|
||||
let total_unchecked_food = ref(0)
|
||||
let total_checked_food = ref(0)
|
||||
|
||||
let stats = ref({
|
||||
countChecked: 0,
|
||||
countUnchecked: 0,
|
||||
countCheckedFood: 0,
|
||||
countUncheckedFood: 0,
|
||||
countUncheckedDelayed: 0,
|
||||
} as ShoppingListStats)
|
||||
|
||||
// internal
|
||||
@@ -47,7 +42,6 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
|
||||
/**
|
||||
* build a multi-level data structure ready for display from shopping list entries
|
||||
* group by selected grouping key
|
||||
* @return {{}}
|
||||
*/
|
||||
const getEntriesByGroup = computed(() => {
|
||||
let structure = {} as IShoppingList
|
||||
@@ -67,6 +61,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
|
||||
countUnchecked: 0,
|
||||
countCheckedFood: 0,
|
||||
countUncheckedFood: 0,
|
||||
countUncheckedDelayed: 0,
|
||||
} as ShoppingListStats
|
||||
|
||||
category.foods.forEach(food => {
|
||||
@@ -77,6 +72,9 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
|
||||
categoryStats.countChecked++
|
||||
} else {
|
||||
categoryStats.countUnchecked++
|
||||
if(entry.delayUntil != null) {
|
||||
categoryStats.countUncheckedDelayed++
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -93,7 +91,7 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
|
||||
stats.value.countCheckedFood += categoryStats.countCheckedFood
|
||||
stats.value.countUncheckedFood += categoryStats.countUncheckedFood
|
||||
})
|
||||
|
||||
|
||||
// ordering
|
||||
|
||||
if (structure.categories.has(UNDEFINED_CATEGORY)) {
|
||||
@@ -497,7 +495,23 @@ export const useShoppingStore = defineStore(_STORE_ID, () => {
|
||||
}
|
||||
}
|
||||
|
||||
return {entries, supermarkets, supermarketCategories, getEntriesByGroup, getFlatEntries, hasFailedItems, refreshFromAPI, createObject, deleteObject, updateObject, undoChange, setEntriesCheckedState, delayEntries}
|
||||
return {
|
||||
UNDEFINED_CATEGORY,
|
||||
entries,
|
||||
supermarkets,
|
||||
supermarketCategories,
|
||||
getEntriesByGroup,
|
||||
getFlatEntries,
|
||||
hasFailedItems,
|
||||
refreshFromAPI,
|
||||
createObject,
|
||||
deleteObject,
|
||||
updateObject,
|
||||
undoChange,
|
||||
setEntriesCheckedState,
|
||||
delayEntries,
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
// enable hot reload for store
|
||||
|
||||
@@ -1,53 +1,88 @@
|
||||
import {Food, ShoppingListEntry, SupermarketCategory} from "@/openapi";
|
||||
import {b} from "vite/dist/node/types.d-aGj9QkWt";
|
||||
import {ref} from "vue";
|
||||
import {Food, ShoppingListEntry, Unit} from "@/openapi";
|
||||
|
||||
/**
|
||||
* enum of different options a shopping list can be grouped by
|
||||
*/
|
||||
export enum ShoppingGroupingOptions {
|
||||
CATEGORY = 'CATEGORY',
|
||||
CREATED_BY = 'CREATED_BY',
|
||||
RECIPE = 'RECIPE',
|
||||
}
|
||||
|
||||
/**
|
||||
* Top level structure for calculated (grouped categories/foods/entries) shopping list
|
||||
* build by the ShoppingStore for usage in UI
|
||||
*/
|
||||
export interface IShoppingList {
|
||||
categories: Map<string, IShoppingListCategory>
|
||||
}
|
||||
|
||||
/**
|
||||
* category in shopping list with its associated foods
|
||||
*/
|
||||
export interface IShoppingListCategory {
|
||||
name: string,
|
||||
foods: Map<number, IShoppingListFood>,
|
||||
stats: ShoppingListStats,
|
||||
}
|
||||
|
||||
/**
|
||||
* food in shopping list with its associated entries
|
||||
*/
|
||||
export interface IShoppingListFood {
|
||||
food: Food,
|
||||
entries: Map<number, ShoppingListEntry>
|
||||
}
|
||||
|
||||
export interface IGroupingOption {
|
||||
id: string,
|
||||
translationKey: string
|
||||
export type ShoppingLineAmount = {
|
||||
amount: number,
|
||||
unit: Unit,
|
||||
checked: boolean,
|
||||
delayed: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* flat representation of a shopping list entry used for exports
|
||||
*/
|
||||
export interface IShoppingExportEntry {
|
||||
amount: number,
|
||||
unit: string,
|
||||
food: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* data holder for sync queue items containing lists of ShoppingListEntry id's
|
||||
*/
|
||||
export interface IShoppingSyncQueueEntry {
|
||||
ids: number[],
|
||||
checked: boolean,
|
||||
status: 'waiting' | 'syncing' | 'syncing_failed_before' | 'waiting_failed_before',
|
||||
}
|
||||
|
||||
/**
|
||||
* stats object calculated for the main structure and all subcategories
|
||||
*/
|
||||
export type ShoppingListStats = {
|
||||
countChecked: number,
|
||||
countUnchecked: number,
|
||||
countCheckedFood: number,
|
||||
countUncheckedFood: number,
|
||||
countUncheckedDelayed: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* different history entries used by history/undo functions to determine what to do
|
||||
* CREATED: ShoppingListEntry was created
|
||||
* CHECKED: ShoppingListEntry was checked
|
||||
* UNCHECKED: ShoppingListEntry check was removed
|
||||
* DELAY: ShoppingListEntry was postponed/delayed
|
||||
* UNDELAY: ShoppingListEntry delay was removed
|
||||
*/
|
||||
export type ShoppingOperationHistoryType = 'CREATED' | 'CHECKED' | 'UNCHECKED' | 'DELAY' | 'UNDELAY'
|
||||
|
||||
/**
|
||||
* history event consisting of a type and affected entries
|
||||
*/
|
||||
export type ShoppingOperationHistoryEntry = {
|
||||
type: ShoppingOperationHistoryType,
|
||||
entries: ShoppingListEntry[]
|
||||
|
||||
Reference in New Issue
Block a user