download shopping list PDF
This commit is contained in:
14497
vue/package-lock.json
generated
Normal file
14497
vue/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,7 @@
|
|||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"bootstrap-vue": "^2.21.2",
|
"bootstrap-vue": "^2.21.2",
|
||||||
"core-js": "^3.19.0",
|
"core-js": "^3.19.0",
|
||||||
|
"html2pdf.js": "^0.10.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"prismjs": "^1.25.0",
|
"prismjs": "^1.25.0",
|
||||||
|
@ -6,8 +6,15 @@
|
|||||||
<b-button variant="link" class="px-0">
|
<b-button variant="link" class="px-0">
|
||||||
<i class="btn fas fa-plus-circle fa-lg px-0" @click="entrymode = !entrymode" :class="entrymode ? 'text-success' : 'text-muted'" />
|
<i class="btn fas fa-plus-circle fa-lg px-0" @click="entrymode = !entrymode" :class="entrymode ? 'text-success' : 'text-muted'" />
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button variant="link" class="px-0 text-muted">
|
<b-button variant="link" class="px-0">
|
||||||
<i class="btn fas fa-download fa-lg px-0" @click="entrymode = !entrymode" />
|
<i class="fas fa-download fa-lg nav-link dropdown-toggle text-muted px-0" id="downloadShoppingLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></i>
|
||||||
|
|
||||||
|
<div class="dropdown-menu dropdown-menu-center" aria-labelledby="downloadShoppingLink">
|
||||||
|
<a class="dropdown-item" @click="download('CSV')"><i class="fas fa-file-import"></i> {{ $t("Download csv") }}</a>
|
||||||
|
<DownloadPDF dom="#shoppinglist" name="shopping.pdf" :label="$t('download_pdf')" icon="far fa-file-pdf" />
|
||||||
|
<a class="dropdown-item" @click="download('clipboard')"><i class="fas fa-plus"></i> {{ $t("copy to clipboard") }}</a>
|
||||||
|
<a class="dropdown-item" @click="download('markdown')"><i class="fas fa-plus"></i> {{ $t("copy as markdown") }}</a>
|
||||||
|
</div>
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button variant="link" id="id_filters_button" class="px-0">
|
<b-button variant="link" id="id_filters_button" class="px-0">
|
||||||
<i class="btn fas fa-filter text-decoration-none fa-lg px-0" :class="filterApplied ? 'text-danger' : 'text-muted'" />
|
<i class="btn fas fa-filter text-decoration-none fa-lg px-0" :class="filterApplied ? 'text-danger' : 'text-muted'" />
|
||||||
@ -19,7 +26,7 @@
|
|||||||
<!-- shopping list tab -->
|
<!-- shopping list tab -->
|
||||||
<b-tab active>
|
<b-tab active>
|
||||||
<template #title> <b-spinner v-if="loading" type="border" small></b-spinner> {{ $t("Shopping_list") }} </template>
|
<template #title> <b-spinner v-if="loading" type="border" small></b-spinner> {{ $t("Shopping_list") }} </template>
|
||||||
<div class="container">
|
<div class="container" id="shoppinglist">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
<div role="tablist" v-if="items && items.length > 0">
|
<div role="tablist" v-if="items && items.length > 0">
|
||||||
@ -352,6 +359,7 @@
|
|||||||
<b-form-group v-bind:label="$t('Supermarket')" label-for="popover-input-2" label-cols="6" class="mb-1">
|
<b-form-group v-bind:label="$t('Supermarket')" label-for="popover-input-2" label-cols="6" class="mb-1">
|
||||||
<b-form-select v-model="selected_supermarket" :options="supermarkets" text-field="name" value-field="id" size="sm"></b-form-select>
|
<b-form-select v-model="selected_supermarket" :options="supermarkets" text-field="name" value-field="id" size="sm"></b-form-select>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
|
<!-- TODO: shade filters red when they are actually filtering content -->
|
||||||
<b-form-group v-bind:label="$t('ShowDelayed')" label-for="popover-input-3" content-cols="1" class="mb-1">
|
<b-form-group v-bind:label="$t('ShowDelayed')" label-for="popover-input-3" content-cols="1" class="mb-1">
|
||||||
<b-form-checkbox v-model="show_delay"></b-form-checkbox>
|
<b-form-checkbox v-model="show_delay"></b-form-checkbox>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
@ -442,6 +450,7 @@ import "bootstrap-vue/dist/bootstrap-vue.css"
|
|||||||
import ContextMenu from "@/components/ContextMenu/ContextMenu"
|
import ContextMenu from "@/components/ContextMenu/ContextMenu"
|
||||||
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
|
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
|
||||||
import ShoppingLineItem from "@/components/ShoppingLineItem"
|
import ShoppingLineItem from "@/components/ShoppingLineItem"
|
||||||
|
import DownloadPDF from "@/components/Buttons/DownloadPDF"
|
||||||
import GenericMultiselect from "@/components/GenericMultiselect"
|
import GenericMultiselect from "@/components/GenericMultiselect"
|
||||||
import GenericPill from "@/components/GenericPill"
|
import GenericPill from "@/components/GenericPill"
|
||||||
import LookupInput from "@/components/Modals/LookupInput"
|
import LookupInput from "@/components/Modals/LookupInput"
|
||||||
@ -456,7 +465,7 @@ Vue.use(BootstrapVue)
|
|||||||
export default {
|
export default {
|
||||||
name: "ShoppingListView",
|
name: "ShoppingListView",
|
||||||
mixins: [ApiMixin],
|
mixins: [ApiMixin],
|
||||||
components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect, GenericPill, draggable, LookupInput },
|
components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect, GenericPill, draggable, LookupInput, DownloadPDF },
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -644,9 +653,8 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// this.genericAPI inherited from ApiMixin
|
// this.genericAPI inherited from ApiMixin
|
||||||
test(e) {
|
download(type) {
|
||||||
this.new_item.unit = e
|
console.log("you just downloaded", type)
|
||||||
console.log(e, this.new_item, this.formUnit)
|
|
||||||
},
|
},
|
||||||
addItem() {
|
addItem() {
|
||||||
let api = new ApiApiFactory()
|
let api = new ApiApiFactory()
|
||||||
|
32
vue/src/components/Buttons/DownloadPDF.vue
Normal file
32
vue/src/components/Buttons/DownloadPDF.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a v-if="!button" class="dropdown-item" @click="downloadFile"><i :class="icon"></i> {{ label }}</a>
|
||||||
|
<b-button v-if="button" @click="downloadFile">{{ label }}</b-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import html2pdf from "html2pdf.js"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DownloadPDF",
|
||||||
|
|
||||||
|
props: {
|
||||||
|
dom: { type: String },
|
||||||
|
name: { type: String },
|
||||||
|
icon: { type: String },
|
||||||
|
label: { type: String },
|
||||||
|
button: { type: Boolean, default: false },
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
downloadFile() {
|
||||||
|
const doc = document.querySelector(this.dom)
|
||||||
|
var options = {
|
||||||
|
margin: 1,
|
||||||
|
filename: this.name,
|
||||||
|
}
|
||||||
|
html2pdf().from(doc).set(options).save()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
@ -6,8 +6,8 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-1">
|
<div class="col col-md-1">
|
||||||
<div style="position: static;" class=" btn-group">
|
<div style="position: static" class="btn-group">
|
||||||
<div class="dropdown b-dropdown position-static inline-block">
|
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true">
|
||||||
<button
|
<button
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
@ -29,7 +29,7 @@
|
|||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{{ formatFood }} <span class="small text-muted">{{ formatHint }}</span>
|
{{ formatFood }} <span class="small text-muted">{{ formatHint }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-1">
|
<div class="col col-md-1" data-html2canvas-ignore="true">
|
||||||
<b-button size="sm" @click="showDetails = !showDetails" class="mr-2" variant="link">
|
<b-button size="sm" @click="showDetails = !showDetails" class="mr-2" variant="link">
|
||||||
<div class="text-nowrap">{{ showDetails ? "Hide" : "Show" }} Details</div>
|
<div class="text-nowrap">{{ showDetails ? "Hide" : "Show" }} Details</div>
|
||||||
</b-button>
|
</b-button>
|
||||||
@ -44,7 +44,7 @@
|
|||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-link btn-sm m-0 p-0"
|
class="btn btn-link btn-sm m-0 p-0"
|
||||||
style="text-overflow: ellipsis;"
|
style="text-overflow: ellipsis"
|
||||||
@click.stop="openRecipeCard($event, e)"
|
@click.stop="openRecipeCard($event, e)"
|
||||||
@mouseover="openRecipeCard($event, e)"
|
@mouseover="openRecipeCard($event, e)"
|
||||||
>
|
>
|
||||||
@ -59,8 +59,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row ml-2 light">
|
<div class="row ml-2 light">
|
||||||
<div class="col-sm-1 text-nowrap">
|
<div class="col-sm-1 text-nowrap">
|
||||||
<div style="position: static;" class=" btn-group ">
|
<div style="position: static" class="btn-group">
|
||||||
<div class="dropdown b-dropdown position-static inline-block">
|
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true">
|
||||||
<button
|
<button
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
@ -89,7 +89,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr class="m-1" />
|
<hr class="m-1" />
|
||||||
</div>
|
</div>
|
||||||
<ContextMenu ref="recipe_card" triggers="click, hover" :title="$t('Filters')" style="max-width:300">
|
<ContextMenu ref="recipe_card" triggers="click, hover" :title="$t('Filters')" style="max-width: 300">
|
||||||
<template #menu="{ contextData }" v-if="recipe">
|
<template #menu="{ contextData }" v-if="recipe">
|
||||||
<ContextMenuItem><RecipeCard :recipe="contextData" :detail="false"></RecipeCard></ContextMenuItem>
|
<ContextMenuItem><RecipeCard :recipe="contextData" :detail="false"></RecipeCard></ContextMenuItem>
|
||||||
<ContextMenuItem @click="$refs.menu.close()">
|
<ContextMenuItem @click="$refs.menu.close()">
|
||||||
@ -138,7 +138,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
formatAmount: function() {
|
formatAmount: function () {
|
||||||
let amount = {}
|
let amount = {}
|
||||||
this.entries.forEach((entry) => {
|
this.entries.forEach((entry) => {
|
||||||
let unit = entry?.unit?.name ?? "----"
|
let unit = entry?.unit?.name ?? "----"
|
||||||
@ -152,26 +152,26 @@ export default {
|
|||||||
})
|
})
|
||||||
return amount
|
return amount
|
||||||
},
|
},
|
||||||
formatCategory: function() {
|
formatCategory: function () {
|
||||||
return this.formatOneCategory(this.entries[0]) || this.$t("Undefined")
|
return this.formatOneCategory(this.entries[0]) || this.$t("Undefined")
|
||||||
},
|
},
|
||||||
formatChecked: function() {
|
formatChecked: function () {
|
||||||
return this.entries.map((x) => x.checked).every((x) => x === true)
|
return this.entries.map((x) => x.checked).every((x) => x === true)
|
||||||
},
|
},
|
||||||
formatHint: function() {
|
formatHint: function () {
|
||||||
if (this.groupby == "recipe") {
|
if (this.groupby == "recipe") {
|
||||||
return this.formatCategory
|
return this.formatCategory
|
||||||
} else {
|
} else {
|
||||||
return this.formatRecipe
|
return this.formatRecipe
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
formatFood: function() {
|
formatFood: function () {
|
||||||
return this.formatOneFood(this.entries[0])
|
return this.formatOneFood(this.entries[0])
|
||||||
},
|
},
|
||||||
formatUnit: function() {
|
formatUnit: function () {
|
||||||
return this.formatOneUnit(this.entries[0])
|
return this.formatOneUnit(this.entries[0])
|
||||||
},
|
},
|
||||||
formatRecipe: function() {
|
formatRecipe: function () {
|
||||||
if (this.entries?.length == 1) {
|
if (this.entries?.length == 1) {
|
||||||
return this.formatOneMealPlan(this.entries[0]) || ""
|
return this.formatOneMealPlan(this.entries[0]) || ""
|
||||||
} else {
|
} else {
|
||||||
@ -179,7 +179,7 @@ export default {
|
|||||||
return [this.formatOneMealPlan(mealplan_name?.[0]), this.$t("CountMore", { count: this.entries?.length - 1 })].join(" ")
|
return [this.formatOneMealPlan(mealplan_name?.[0]), this.$t("CountMore", { count: this.entries?.length - 1 })].join(" ")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
formatNotes: function() {
|
formatNotes: function () {
|
||||||
if (this.entries?.length == 1) {
|
if (this.entries?.length == 1) {
|
||||||
return this.formatOneNote(this.entries[0]) || ""
|
return this.formatOneNote(this.entries[0]) || ""
|
||||||
}
|
}
|
||||||
@ -193,49 +193,49 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
// this.genericAPI inherited from ApiMixin
|
// this.genericAPI inherited from ApiMixin
|
||||||
|
|
||||||
formatDate: function(datetime) {
|
formatDate: function (datetime) {
|
||||||
if (!datetime) {
|
if (!datetime) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return Intl.DateTimeFormat(window.navigator.language, { dateStyle: "short", timeStyle: "short" }).format(Date.parse(datetime))
|
return Intl.DateTimeFormat(window.navigator.language, { dateStyle: "short", timeStyle: "short" }).format(Date.parse(datetime))
|
||||||
},
|
},
|
||||||
formatOneAmount: function(item) {
|
formatOneAmount: function (item) {
|
||||||
return item?.amount ?? 1
|
return item?.amount ?? 1
|
||||||
},
|
},
|
||||||
formatOneUnit: function(item) {
|
formatOneUnit: function (item) {
|
||||||
return item?.unit?.name ?? ""
|
return item?.unit?.name ?? ""
|
||||||
},
|
},
|
||||||
formatOneCategory: function(item) {
|
formatOneCategory: function (item) {
|
||||||
return item?.food?.supermarket_category?.name
|
return item?.food?.supermarket_category?.name
|
||||||
},
|
},
|
||||||
formatOneCompletedAt: function(item) {
|
formatOneCompletedAt: function (item) {
|
||||||
if (!item.completed_at) {
|
if (!item.completed_at) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return [this.$t("Completed"), "@", this.formatDate(item.completed_at)].join(" ")
|
return [this.$t("Completed"), "@", this.formatDate(item.completed_at)].join(" ")
|
||||||
},
|
},
|
||||||
formatOneFood: function(item) {
|
formatOneFood: function (item) {
|
||||||
return item.food.name
|
return item.food.name
|
||||||
},
|
},
|
||||||
formatOneChecked: function(item) {
|
formatOneChecked: function (item) {
|
||||||
return item.checked
|
return item.checked
|
||||||
},
|
},
|
||||||
formatOneMealPlan: function(item) {
|
formatOneMealPlan: function (item) {
|
||||||
return item?.recipe_mealplan?.name
|
return item?.recipe_mealplan?.name
|
||||||
},
|
},
|
||||||
formatOneRecipe: function(item) {
|
formatOneRecipe: function (item) {
|
||||||
return item?.recipe_mealplan?.recipe_name
|
return item?.recipe_mealplan?.recipe_name
|
||||||
},
|
},
|
||||||
formatOneNote: function(item) {
|
formatOneNote: function (item) {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
item = this.entries[0]
|
item = this.entries[0]
|
||||||
}
|
}
|
||||||
return [item?.recipe_mealplan?.mealplan_note, item?.ingredient_note].filter(String)
|
return [item?.recipe_mealplan?.mealplan_note, item?.ingredient_note].filter(String)
|
||||||
},
|
},
|
||||||
formatOneCreatedBy: function(item) {
|
formatOneCreatedBy: function (item) {
|
||||||
return [item?.created_by.username, "@", this.formatDate(item.created_at)].join(" ")
|
return [item?.created_by.username, "@", this.formatDate(item.created_at)].join(" ")
|
||||||
},
|
},
|
||||||
openRecipeCard: function(e, item) {
|
openRecipeCard: function (e, item) {
|
||||||
this.genericAPI(this.Models.RECIPE, this.Actions.FETCH, { id: item.recipe_mealplan.recipe }).then((result) => {
|
this.genericAPI(this.Models.RECIPE, this.Actions.FETCH, { id: item.recipe_mealplan.recipe }).then((result) => {
|
||||||
let recipe = result.data
|
let recipe = result.data
|
||||||
recipe.steps = undefined
|
recipe.steps = undefined
|
||||||
@ -243,7 +243,7 @@ export default {
|
|||||||
this.$refs.recipe_card.open(e, recipe)
|
this.$refs.recipe_card.open(e, recipe)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateChecked: function(e, item) {
|
updateChecked: function (e, item) {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
let update = { entries: this.entries.map((x) => x.id), checked: !this.formatChecked }
|
let update = { entries: this.entries.map((x) => x.id), checked: !this.formatChecked }
|
||||||
this.$emit("update-checkbox", update)
|
this.$emit("update-checkbox", update)
|
||||||
|
@ -270,5 +270,6 @@
|
|||||||
"New_Cookbook": "New cookbook",
|
"New_Cookbook": "New cookbook",
|
||||||
"Hide_Keyword": "Hide keywords",
|
"Hide_Keyword": "Hide keywords",
|
||||||
"Clear": "Clear",
|
"Clear": "Clear",
|
||||||
"create_shopping_new": "NEW: Add to Shopping List"
|
"create_shopping_new": "NEW: Add to Shopping List",
|
||||||
|
"download_pdf": "Download PDF"
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user