mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 02:03:35 -04:00 
			
		
		
		
	ui: rework meal-planner (#2203)
* remove unused TS Ignores * refactor planner into multiple pages also includes some minor UI adjustments and some feature work to improve the date selector * use mobile cards for meal-planner * remove component
This commit is contained in:
		
							
								
								
									
										1
									
								
								frontend/.nuxtignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/.nuxtignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | pages/**/*.ts | ||||||
| @@ -34,7 +34,6 @@ export const useMealplans = function (range: Ref<DateRange>) { | |||||||
|           start_date: format(range.value.start, "yyyy-MM-dd"), |           start_date: format(range.value.start, "yyyy-MM-dd"), | ||||||
|           end_date: format(range.value.end, "yyyy-MM-dd"), |           end_date: format(range.value.end, "yyyy-MM-dd"), | ||||||
|         }; |         }; | ||||||
|         // @ts-ignore TODO Modify typing to allow for string start+limit for mealplans |  | ||||||
|         const { data } = await api.mealplans.getAll(1, -1, { start_date: query.start_date, end_date: query.end_date }); |         const { data } = await api.mealplans.getAll(1, -1, { start_date: query.start_date, end_date: query.end_date }); | ||||||
|  |  | ||||||
|         if (data) { |         if (data) { | ||||||
| @@ -53,7 +52,6 @@ export const useMealplans = function (range: Ref<DateRange>) { | |||||||
|         start_date: format(range.value.start, "yyyy-MM-dd"), |         start_date: format(range.value.start, "yyyy-MM-dd"), | ||||||
|         end_date: format(range.value.end, "yyyy-MM-dd"), |         end_date: format(range.value.end, "yyyy-MM-dd"), | ||||||
|       }; |       }; | ||||||
|       // @ts-ignore TODO Modify typing to allow for string start+limit for mealplans |  | ||||||
|       const { data } = await api.mealplans.getAll(1, -1, { start_date: query.start_date, end_date: query.end_date }); |       const { data } = await api.mealplans.getAll(1, -1, { start_date: query.start_date, end_date: query.end_date }); | ||||||
|  |  | ||||||
|       if (data && data.items) { |       if (data && data.items) { | ||||||
|   | |||||||
| @@ -1,19 +1,7 @@ | |||||||
| <template> | <template> | ||||||
|   <v-container> |   <v-container> | ||||||
|     <!-- Create Meal Dialog --> |  | ||||||
|     <BaseDialog |  | ||||||
|       v-model="createMealDialog" |  | ||||||
|       :title="$tc('meal-plan.create-a-new-meal-plan')" |  | ||||||
|       color="primary" |  | ||||||
|       :icon="$globals.icons.foods" |  | ||||||
|       @submit=" |  | ||||||
|         actions.createOne(newMeal); |  | ||||||
|         resetDialog(); |  | ||||||
|       " |  | ||||||
|     > |  | ||||||
|       <v-card-text> |  | ||||||
|     <v-menu |     <v-menu | ||||||
|           v-model="pickerMenu" |       v-model="state.picker" | ||||||
|       :close-on-content-click="false" |       :close-on-content-click="false" | ||||||
|       transition="scale-transition" |       transition="scale-transition" | ||||||
|       offset-y |       offset-y | ||||||
| @@ -21,349 +9,89 @@ | |||||||
|       min-width="auto" |       min-width="auto" | ||||||
|     > |     > | ||||||
|       <template #activator="{ on, attrs }"> |       <template #activator="{ on, attrs }"> | ||||||
|             <v-text-field |         <v-btn color="primary" class="mb-2" v-bind="attrs" v-on="on"> | ||||||
|               v-model="newMeal.date" |           <v-icon left> | ||||||
|               :label="$t('general.date')" |             {{ $globals.icons.calendar }} | ||||||
|               :hint="$t('recipe.date-format-hint-yyyy-mm-dd')" |           </v-icon> | ||||||
|               persistent-hint |           {{ $d(weekRange.start, "short") }} - {{ $d(weekRange.end, "short") }} | ||||||
|               :prepend-icon="$globals.icons.calendar" |         </v-btn> | ||||||
|               v-bind="attrs" |  | ||||||
|               readonly |  | ||||||
|               v-on="on" |  | ||||||
|             ></v-text-field> |  | ||||||
|       </template> |       </template> | ||||||
|           <v-date-picker |       <v-date-picker v-model="state.range" no-title range> | ||||||
|             v-model="newMeal.date" |         <v-spacer></v-spacer> | ||||||
|             :first-day-of-week="firstDayOfWeek" |         <v-btn text color="primary" @click="state.picker = false"> | ||||||
|             no-title |           {{ $t("general.ok") }} | ||||||
|             @input="pickerMenu = false" |         </v-btn> | ||||||
|           ></v-date-picker> |       </v-date-picker> | ||||||
|     </v-menu> |     </v-menu> | ||||||
|         <v-card-text> |  | ||||||
|           <v-select |  | ||||||
|             v-model="newMeal.entryType" |  | ||||||
|             :return-object="false" |  | ||||||
|             :items="planTypeOptions" |  | ||||||
|             :label="$t('recipe.entry-type')" |  | ||||||
|           > |  | ||||||
|           </v-select> |  | ||||||
|  |  | ||||||
|           <v-autocomplete |     <div class="d-flex align-center justify-space-between mb-2"> | ||||||
|             v-if="!dialog.note" |       <v-tabs> | ||||||
|             v-model="newMeal.recipeId" |         <v-tab to="/group/mealplan/planner/view">Meal Planner</v-tab> | ||||||
|             :label="$t('meal-plan.meal-recipe')" |         <v-tab to="/group/mealplan/planner/edit">Edit</v-tab> | ||||||
|             :items="recipeResults" |       </v-tabs> | ||||||
|             :loading="loadingRecipes" |  | ||||||
|             :search-input.sync="recipeSearchTerm" |  | ||||||
|             cache-items |  | ||||||
|             item-text="name" |  | ||||||
|             item-value="id" |  | ||||||
|             :return-object="false" |  | ||||||
|           ></v-autocomplete> |  | ||||||
|           <template v-else> |  | ||||||
|             <v-text-field v-model="newMeal.title" :label="$t('meal-plan.meal-title')"> </v-text-field> |  | ||||||
|             <v-textarea v-model="newMeal.text" rows="2" :label="$t('meal-plan.meal-note')"> </v-textarea> |  | ||||||
|           </template> |  | ||||||
|         </v-card-text> |  | ||||||
|         <v-card-actions class="my-0 py-0"> |  | ||||||
|           <v-switch v-model="dialog.note" class="mt-n3" :label="$t('meal-plan.note-only')"></v-switch> |  | ||||||
|         </v-card-actions> |  | ||||||
|       </v-card-text> |  | ||||||
|     </BaseDialog> |  | ||||||
|  |  | ||||||
|     <!-- Date Forward / Back --> |  | ||||||
|     <div class="d-flex justify-center flex-column"> |  | ||||||
|       <h3 class="text-h6 mt-2 text-center">{{ $d(weekRange.start, "short") }} - {{ $d(weekRange.end, "short") }}</h3> |  | ||||||
|       <div class="d-flex justify-center my-2 align-center" style="gap: 10px"> |  | ||||||
|         <v-btn icon color="info" outlined @click="backOneWeek"> |  | ||||||
|           <v-icon>{{ $globals.icons.back }} </v-icon> |  | ||||||
|         </v-btn> |  | ||||||
|         <v-btn icon color="info" outlined @click="forwardOneWeek"> |  | ||||||
|           <v-icon>{{ $globals.icons.forward }} </v-icon> |  | ||||||
|         </v-btn> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|     <div class="d-flex align-center justify-space-between"> |  | ||||||
|       <v-switch v-model="edit" :label="$t('meal-plan.editor')"></v-switch> |  | ||||||
|       <ButtonLink :icon="$globals.icons.calendar" to="/group/mealplan/settings" :text="$tc('general.settings')" /> |       <ButtonLink :icon="$globals.icons.calendar" to="/group/mealplan/settings" :text="$tc('general.settings')" /> | ||||||
|     </div> |     </div> | ||||||
|     <v-row class=""> |  | ||||||
|       <v-col |  | ||||||
|         v-for="(plan, index) in mealsByDate" |  | ||||||
|         :key="index" |  | ||||||
|         cols="12" |  | ||||||
|         sm="12" |  | ||||||
|         md="4" |  | ||||||
|         lg="3" |  | ||||||
|         xl="2" |  | ||||||
|         class="col-borders my-1 d-flex flex-column" |  | ||||||
|       > |  | ||||||
|         <v-sheet class="mb-2 bottom-color-border"> |  | ||||||
|           <p class="headline text-center mb-1"> |  | ||||||
|             {{ $d(plan.date, "short") }} |  | ||||||
|           </p> |  | ||||||
|         </v-sheet> |  | ||||||
|  |  | ||||||
|         <!-- Day Column Recipes --> |     <div> | ||||||
|         <template v-if="edit"> |       <NuxtChild :mealplans="mealsByDate" :actions="actions" /> | ||||||
|           <draggable |  | ||||||
|             tag="div" |  | ||||||
|             handle=".handle" |  | ||||||
|             :value="plan.meals" |  | ||||||
|             group="meals" |  | ||||||
|             :data-index="index" |  | ||||||
|             :data-box="plan.date" |  | ||||||
|             style="min-height: 150px" |  | ||||||
|             @end="onMoveCallback" |  | ||||||
|           > |  | ||||||
|             <v-card |  | ||||||
|               v-for="mealplan in plan.meals" |  | ||||||
|               :key="mealplan.id" |  | ||||||
|               v-model="hover[mealplan.id]" |  | ||||||
|               class="my-1" |  | ||||||
|               :class="{ handle: $vuetify.breakpoint.smAndUp }" |  | ||||||
|             > |  | ||||||
|               <v-list-item :to="edit || !mealplan.recipe ? null : `/recipe/${mealplan.recipe.slug}`"> |  | ||||||
|                 <v-list-item-avatar :rounded="false"> |  | ||||||
|                   <RecipeCardImage |  | ||||||
|                     v-if="mealplan.recipe" |  | ||||||
|                     :recipe-id="mealplan.recipe.id" |  | ||||||
|                     tiny |  | ||||||
|                     icon-size="25" |  | ||||||
|                     :slug="mealplan.recipe ? mealplan.recipe.slug : ''" |  | ||||||
|                   > |  | ||||||
|                   </RecipeCardImage> |  | ||||||
|                   <v-icon v-else> |  | ||||||
|                     {{ $globals.icons.primary }} |  | ||||||
|                   </v-icon> |  | ||||||
|                 </v-list-item-avatar> |  | ||||||
|                 <v-list-item-content> |  | ||||||
|                   <v-list-item-title class="mb-1"> |  | ||||||
|                     {{ mealplan.recipe ? mealplan.recipe.name : mealplan.title }} |  | ||||||
|                   </v-list-item-title> |  | ||||||
|                   <v-list-item-subtitle style="min-height: 16px"> |  | ||||||
|                     {{ mealplan.recipe ? mealplan.recipe.description + " " : mealplan.text }} |  | ||||||
|                   </v-list-item-subtitle> |  | ||||||
|                 </v-list-item-content> |  | ||||||
|               </v-list-item> |  | ||||||
|               <v-divider class="mx-2"></v-divider> |  | ||||||
|               <div class="py-2 px-2 d-flex" style="align-items: center"> |  | ||||||
|                 <v-btn small icon :class="{ handle: !$vuetify.breakpoint.smAndUp }"> |  | ||||||
|                   <v-icon> |  | ||||||
|                     {{ $globals.icons.arrowUpDown }} |  | ||||||
|                   </v-icon> |  | ||||||
|                 </v-btn> |  | ||||||
|  |  | ||||||
|                 <v-menu offset-y> |  | ||||||
|                   <template #activator="{ on, attrs }"> |  | ||||||
|                     <v-chip v-bind="attrs" label small color="accent" v-on="on" @click.prevent> |  | ||||||
|                       <v-icon left> |  | ||||||
|                         {{ $globals.icons.tags }} |  | ||||||
|                       </v-icon> |  | ||||||
|                       {{ mealplan.entryType }} |  | ||||||
|                     </v-chip> |  | ||||||
|                   </template> |  | ||||||
|                   <v-list> |  | ||||||
|                     <v-list-item |  | ||||||
|                       v-for="mealType in planTypeOptions" |  | ||||||
|                       :key="mealType.value" |  | ||||||
|                       @click="actions.setType(mealplan, mealType.value)" |  | ||||||
|                     > |  | ||||||
|                       <v-list-item-title> {{ mealType.text }} </v-list-item-title> |  | ||||||
|                     </v-list-item> |  | ||||||
|                   </v-list> |  | ||||||
|                 </v-menu> |  | ||||||
|  |  | ||||||
|                 <v-btn class="ml-auto" small icon @click="actions.deleteOne(mealplan.id)"> |  | ||||||
|                   <v-icon>{{ $globals.icons.delete }}</v-icon> |  | ||||||
|                 </v-btn> |  | ||||||
|     </div> |     </div> | ||||||
|             </v-card> |  | ||||||
|           </draggable> |  | ||||||
|  |  | ||||||
|           <!-- Day Column Actions --> |     <v-row> </v-row> | ||||||
|           <div class="d-flex justify-end"> |  | ||||||
|             <BaseButtonGroup |  | ||||||
|               :buttons="[ |  | ||||||
|                 { |  | ||||||
|                   icon: $globals.icons.diceMultiple, |  | ||||||
|                   text: $tc('meal-plan.random-meal'), |  | ||||||
|                   event: 'random', |  | ||||||
|                   children: [ |  | ||||||
|                     { |  | ||||||
|                       icon: $globals.icons.diceMultiple, |  | ||||||
|                       text: 'Breakfast', |  | ||||||
|                       event: 'randomBreakfast', |  | ||||||
|                     }, |  | ||||||
|  |  | ||||||
|                     { |  | ||||||
|                       icon: $globals.icons.diceMultiple, |  | ||||||
|                       text: $tc('meal-plan.lunch'), |  | ||||||
|                       event: 'randomLunch', |  | ||||||
|                     }, |  | ||||||
|                   ], |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                   icon: $globals.icons.potSteam, |  | ||||||
|                   text: $tc('meal-plan.random-dinner'), |  | ||||||
|                   event: 'randomDinner', |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                   icon: $globals.icons.bowlMixOutline, |  | ||||||
|                   text: $tc('meal-plan.random-side'), |  | ||||||
|                   event: 'randomSide', |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                   icon: $globals.icons.createAlt, |  | ||||||
|                   text: $tc('general.new'), |  | ||||||
|                   event: 'create', |  | ||||||
|                 }, |  | ||||||
|               ]" |  | ||||||
|               @create="openDialog(plan.date)" |  | ||||||
|               @randomBreakfast="randomMeal(plan.date, 'breakfast')" |  | ||||||
|               @randomLunch="randomMeal(plan.date, 'lunch')" |  | ||||||
|               @randomDinner="randomMeal(plan.date, 'dinner')" |  | ||||||
|               @randomSide="randomMeal(plan.date, 'side')" |  | ||||||
|             /> |  | ||||||
|           </div> |  | ||||||
|         </template> |  | ||||||
|         <template v-else-if="plan.meals"> |  | ||||||
|           <RecipeCard |  | ||||||
|             v-for="mealplan in plan.meals" |  | ||||||
|             :key="mealplan.id" |  | ||||||
|             :recipe-id="mealplan.recipe ? mealplan.recipe.id : ''" |  | ||||||
|             :image-height="125" |  | ||||||
|             class="mb-2" |  | ||||||
|             :route="mealplan.recipe ? true : false" |  | ||||||
|             :slug="mealplan.recipe ? mealplan.recipe.slug : mealplan.title" |  | ||||||
|             :description="mealplan.recipe ? mealplan.recipe.description : mealplan.text" |  | ||||||
|             :name="mealplan.recipe ? mealplan.recipe.name : mealplan.title" |  | ||||||
|           > |  | ||||||
|             <template #actions> |  | ||||||
|               <v-divider class="mb-0 mt-2 mx-2"></v-divider> |  | ||||||
|               <v-card-actions class="justify-end mt-1"> |  | ||||||
|                 <v-chip label small color="accent"> |  | ||||||
|                   <v-icon left> |  | ||||||
|                     {{ $globals.icons.tags }} |  | ||||||
|                   </v-icon> |  | ||||||
|                   {{ mealplan.entryType }} |  | ||||||
|                 </v-chip> |  | ||||||
|                 <RecipeContextMenu |  | ||||||
|                   :name="mealplan.recipe ? mealplan.recipe.name : mealplan.title" |  | ||||||
|                   :recipe-id="mealplan.recipe ? mealplan.recipe.id : ''" |  | ||||||
|                   :slug="mealplan.recipe ? mealplan.recipe.slug : ''" |  | ||||||
|                   :use-items="{ |  | ||||||
|                     delete: false, |  | ||||||
|                     edit: false, |  | ||||||
|                     download: true, |  | ||||||
|                     duplicate: false, |  | ||||||
|                     mealplanner: false, |  | ||||||
|                     print: true, |  | ||||||
|                     printPreferences: false, |  | ||||||
|                     share: false, |  | ||||||
|                     shoppingList: true, |  | ||||||
|                     publicUrl: false, |  | ||||||
|                   }" |  | ||||||
|                 /> |  | ||||||
|               </v-card-actions> |  | ||||||
|             </template> |  | ||||||
|           </RecipeCard> |  | ||||||
|         </template> |  | ||||||
|  |  | ||||||
|         <v-skeleton-loader v-else elevation="2" type="image, list-item-two-line"></v-skeleton-loader> |  | ||||||
|       </v-col> |  | ||||||
|     </v-row> |  | ||||||
|   </v-container> |   </v-container> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { computed, defineComponent, reactive, ref, toRefs, watch } from "@nuxtjs/composition-api"; | import { computed, defineComponent, ref, useRoute, useRouter } from "@nuxtjs/composition-api"; | ||||||
| import { isSameDay, addDays, subDays, parseISO, format } from "date-fns"; | import { isSameDay, addDays, parseISO } from "date-fns"; | ||||||
| import { SortableEvent } from "sortablejs"; | import { useMealplans } from "~/composables/use-group-mealplan"; | ||||||
| import draggable from "vuedraggable"; |  | ||||||
| import { watchDebounced } from "@vueuse/core"; |  | ||||||
| import { useMealplans, planTypeOptions } from "~/composables/use-group-mealplan"; |  | ||||||
| import RecipeCardImage from "~/components/Domain/Recipe/RecipeCardImage.vue"; |  | ||||||
| import RecipeCard from "~/components/Domain/Recipe/RecipeCard.vue"; |  | ||||||
| import RecipeContextMenu from "~/components/Domain/Recipe/RecipeContextMenu.vue"; |  | ||||||
| import { PlanEntryType } from "~/lib/api/types/meal-plan"; |  | ||||||
| import { useUserApi } from "~/composables/api"; |  | ||||||
| import { useGroupSelf } from "~/composables/use-groups"; |  | ||||||
| import { RecipeSummary } from "~/lib/api/types/recipe"; |  | ||||||
|  |  | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|   components: { |  | ||||||
|     draggable, |  | ||||||
|     RecipeCardImage, |  | ||||||
|     RecipeCard, |  | ||||||
|     RecipeContextMenu, |  | ||||||
|   }, |  | ||||||
|   setup() { |   setup() { | ||||||
|     const state = reactive({ |     const route = useRoute(); | ||||||
|       createMealDialog: false, |     const router = useRouter(); | ||||||
|       edit: false, |  | ||||||
|       hover: {} as Record<string, boolean>, |     // Force to /view if current route is /planner | ||||||
|       pickerMenu: null, |     if (route.value.path === "/group/mealplan/planner") { | ||||||
|       today: new Date(), |       router.push("/group/mealplan/planner/view"); | ||||||
|       recipeResults: [] as RecipeSummary[], |     } | ||||||
|       loadingRecipes: false, |  | ||||||
|  |     function fmtYYYYMMDD(date: Date) { | ||||||
|  |       return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function parseYYYYMMDD(date: string) { | ||||||
|  |       const [year, month, day] = date.split("-"); | ||||||
|  |       return new Date(parseInt(year), parseInt(month) - 1, parseInt(day)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const state = ref({ | ||||||
|  |       range: [fmtYYYYMMDD(new Date()), fmtYYYYMMDD(addDays(new Date(), 6))] as [string, string], | ||||||
|  |       start: new Date(), | ||||||
|  |       picker: false, | ||||||
|  |       end: addDays(new Date(), 6), | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     const recipeSearchTerm = ref(""); |     const recipeSearchTerm = ref(""); | ||||||
|  |  | ||||||
|     const weekRange = computed(() => { |     const weekRange = computed(() => { | ||||||
|  |       const sorted = state.value.range.sort((a, b) => { | ||||||
|  |         return parseYYYYMMDD(a).getTime() - parseYYYYMMDD(b).getTime(); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (sorted.length === 2) { | ||||||
|  |         console.log(parseYYYYMMDD(sorted[0])); | ||||||
|         return { |         return { | ||||||
|         start: subDays(state.today as Date, 1), |           start: parseYYYYMMDD(sorted[0]), | ||||||
|         end: addDays(state.today as Date, 6), |           end: parseYYYYMMDD(sorted[1]), | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |       return { | ||||||
|  |         start: new Date(), | ||||||
|  |         end: addDays(new Date(), 6), | ||||||
|       }; |       }; | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     const api = useUserApi(); |     const { mealplans, actions } = useMealplans(weekRange); | ||||||
|  |  | ||||||
|     const { mealplans, actions, loading } = useMealplans(weekRange); |  | ||||||
|  |  | ||||||
|     async function searchRecipes(term: string) { |  | ||||||
|       state.loadingRecipes = true; |  | ||||||
|       const { data, error } = await api.recipes.search({ |  | ||||||
|         search: term, |  | ||||||
|         page: 1, |  | ||||||
|         orderBy: "name", |  | ||||||
|         orderDirection: "asc", |  | ||||||
|         perPage: 20, |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       if (error) { |  | ||||||
|         console.error(error); |  | ||||||
|         state.loadingRecipes = false; |  | ||||||
|         state.recipeResults = []; |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (data) { |  | ||||||
|         state.recipeResults = data.items; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       state.loadingRecipes = false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     watchDebounced( |  | ||||||
|       recipeSearchTerm, |  | ||||||
|       async (term: string) => { |  | ||||||
|         await searchRecipes(term); |  | ||||||
|       }, |  | ||||||
|       { debounce: 500 } |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     const { group } = useGroupSelf(); |  | ||||||
|  |  | ||||||
|     const firstDayOfWeek = computed(() => { |  | ||||||
|       const pref = group.value?.preferences?.firstDayOfWeek; |  | ||||||
|  |  | ||||||
|       if (pref) { |  | ||||||
|         return pref; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return 0; |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     function filterMealByDate(date: Date) { |     function filterMealByDate(date: Date) { | ||||||
|       if (!mealplans.value) return []; |       if (!mealplans.value) return []; | ||||||
| @@ -373,40 +101,17 @@ export default defineComponent({ | |||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function forwardOneWeek() { |     const days = computed(() => { | ||||||
|       if (!state.today) return; |       const numDays = | ||||||
|       state.today = addDays(state.today as Date, +5); |         Math.floor((weekRange.value.end.getTime() - weekRange.value.start.getTime()) / (1000 * 60 * 60 * 24)) + 1; | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function backOneWeek() { |       // Calculate aboslute value | ||||||
|       if (!state.today) return; |       if (numDays < 0) return []; | ||||||
|       state.today = addDays(state.today as Date, -5); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function onMoveCallback(evt: SortableEvent) { |       return Array.from(Array(numDays).keys()).map( | ||||||
|       const supportedEvents = ["drop", "touchend"]; |         (i) => new Date(weekRange.value.start.getTime() + i * 24 * 60 * 60 * 1000) | ||||||
|  |       ); | ||||||
|       // Adapted From https://github.com/SortableJS/Vue.Draggable/issues/1029 |     }); | ||||||
|       const ogEvent: DragEvent = (evt as any).originalEvent; |  | ||||||
|  |  | ||||||
|       if (ogEvent && ogEvent.type in supportedEvents) { |  | ||||||
|         // The drop was cancelled, unsure if anything needs to be done? |  | ||||||
|         console.log("Cancel Move Event"); |  | ||||||
|       } else { |  | ||||||
|         // A Meal was moved, set the new date value and make an update request and refresh the meals |  | ||||||
|         const fromMealsByIndex = parseInt(evt.from.getAttribute("data-index") ?? ""); |  | ||||||
|         const toMealsByIndex = parseInt(evt.to.getAttribute("data-index") ?? ""); |  | ||||||
|  |  | ||||||
|         if (!isNaN(fromMealsByIndex) && !isNaN(toMealsByIndex)) { |  | ||||||
|           const mealData = mealsByDate.value[fromMealsByIndex].meals[evt.oldIndex as number]; |  | ||||||
|           const destDate = mealsByDate.value[toMealsByIndex].date; |  | ||||||
|  |  | ||||||
|           mealData.date = format(destDate, "yyyy-MM-dd"); |  | ||||||
|  |  | ||||||
|           actions.updateOne(mealData); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const mealsByDate = computed(() => { |     const mealsByDate = computed(() => { | ||||||
|       return days.value.map((day) => { |       return days.value.map((day) => { | ||||||
| @@ -414,79 +119,11 @@ export default defineComponent({ | |||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     const days = computed(() => { |  | ||||||
|       return Array.from(Array(8).keys()).map( |  | ||||||
|         (i) => new Date(weekRange.value.start.getTime() + i * 24 * 60 * 60 * 1000) |  | ||||||
|       ); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     // ===================================================== |  | ||||||
|     // New Meal Dialog |  | ||||||
|  |  | ||||||
|     const dialog = reactive({ |  | ||||||
|       loading: false, |  | ||||||
|       error: false, |  | ||||||
|       note: false, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     watch(dialog, () => { |  | ||||||
|       if (dialog.note) { |  | ||||||
|         newMeal.recipeId = undefined; |  | ||||||
|       } |  | ||||||
|       newMeal.title = ""; |  | ||||||
|       newMeal.text = ""; |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     const newMeal = reactive({ |  | ||||||
|       date: "", |  | ||||||
|       title: "", |  | ||||||
|       text: "", |  | ||||||
|       recipeId: undefined as string | undefined, |  | ||||||
|       entryType: "dinner" as PlanEntryType, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     function openDialog(date: Date) { |  | ||||||
|       newMeal.date = format(date, "yyyy-MM-dd"); |  | ||||||
|       state.createMealDialog = true; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function resetDialog() { |  | ||||||
|       newMeal.date = ""; |  | ||||||
|       newMeal.title = ""; |  | ||||||
|       newMeal.text = ""; |  | ||||||
|       newMeal.entryType = "dinner"; |  | ||||||
|       newMeal.recipeId = undefined; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function randomMeal(date: Date, type: PlanEntryType) { |  | ||||||
|       const { data } = await api.mealplans.setRandom({ |  | ||||||
|         date: format(date, "yyyy-MM-dd"), |  | ||||||
|         entryType: type, |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       if (data) { |  | ||||||
|         actions.refreshAll(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|       ...toRefs(state), |       state, | ||||||
|       actions, |       actions, | ||||||
|       backOneWeek, |  | ||||||
|       days, |  | ||||||
|       dialog, |  | ||||||
|       forwardOneWeek, |  | ||||||
|       loading, |  | ||||||
|       mealplans, |  | ||||||
|       mealsByDate, |       mealsByDate, | ||||||
|       newMeal, |  | ||||||
|       onMoveCallback, |  | ||||||
|       openDialog, |  | ||||||
|       planTypeOptions, |  | ||||||
|       randomMeal, |  | ||||||
|       resetDialog, |  | ||||||
|       weekRange, |       weekRange, | ||||||
|       firstDayOfWeek, |  | ||||||
|       recipeSearchTerm, |       recipeSearchTerm, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   | |||||||
							
								
								
									
										390
									
								
								frontend/pages/group/mealplan/planner/edit.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										390
									
								
								frontend/pages/group/mealplan/planner/edit.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,390 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <!-- Create Meal Dialog --> | ||||||
|  |     <BaseDialog | ||||||
|  |       v-model="state.dialog" | ||||||
|  |       :title="$tc('meal-plan.create-a-new-meal-plan')" | ||||||
|  |       color="primary" | ||||||
|  |       :icon="$globals.icons.foods" | ||||||
|  |       @submit=" | ||||||
|  |         actions.createOne(newMeal); | ||||||
|  |         resetDialog(); | ||||||
|  |       " | ||||||
|  |     > | ||||||
|  |       <v-card-text> | ||||||
|  |         <v-menu | ||||||
|  |           v-model="state.pickerMenu" | ||||||
|  |           :close-on-content-click="false" | ||||||
|  |           transition="scale-transition" | ||||||
|  |           offset-y | ||||||
|  |           max-width="290px" | ||||||
|  |           min-width="auto" | ||||||
|  |         > | ||||||
|  |           <template #activator="{ on, attrs }"> | ||||||
|  |             <v-text-field | ||||||
|  |               v-model="newMeal.date" | ||||||
|  |               :label="$t('general.date')" | ||||||
|  |               :hint="$t('recipe.date-format-hint-yyyy-mm-dd')" | ||||||
|  |               persistent-hint | ||||||
|  |               :prepend-icon="$globals.icons.calendar" | ||||||
|  |               v-bind="attrs" | ||||||
|  |               readonly | ||||||
|  |               v-on="on" | ||||||
|  |             /> | ||||||
|  |           </template> | ||||||
|  |           <v-date-picker | ||||||
|  |             v-model="newMeal.date" | ||||||
|  |             :first-day-of-week="firstDayOfWeek" | ||||||
|  |             no-title | ||||||
|  |             @input="state.pickerMenu = false" | ||||||
|  |           /> | ||||||
|  |         </v-menu> | ||||||
|  |         <v-card-text> | ||||||
|  |           <v-select | ||||||
|  |             v-model="newMeal.entryType" | ||||||
|  |             :return-object="false" | ||||||
|  |             :items="planTypeOptions" | ||||||
|  |             :label="$t('recipe.entry-type')" | ||||||
|  |           /> | ||||||
|  |           <v-autocomplete | ||||||
|  |             v-if="!dialog.note" | ||||||
|  |             v-model="newMeal.recipeId" | ||||||
|  |             :label="$t('meal-plan.meal-recipe')" | ||||||
|  |             :items="recipes.data" | ||||||
|  |             :loading="recipes.loading" | ||||||
|  |             :search-input.sync="recipes.search" | ||||||
|  |             cache-items | ||||||
|  |             item-text="name" | ||||||
|  |             item-value="id" | ||||||
|  |             :return-object="false" | ||||||
|  |           /> | ||||||
|  |           <template v-else> | ||||||
|  |             <v-text-field v-model="newMeal.title" :label="$t('meal-plan.meal-title')" /> | ||||||
|  |             <v-textarea v-model="newMeal.text" rows="2" :label="$t('meal-plan.meal-note')" /> | ||||||
|  |           </template> | ||||||
|  |         </v-card-text> | ||||||
|  |         <v-card-actions class="my-0 py-0"> | ||||||
|  |           <v-switch v-model="dialog.note" class="mt-n3" :label="$t('meal-plan.note-only')" /> | ||||||
|  |         </v-card-actions> | ||||||
|  |       </v-card-text> | ||||||
|  |     </BaseDialog> | ||||||
|  |  | ||||||
|  |     <v-row> | ||||||
|  |       <v-col | ||||||
|  |         v-for="(plan, index) in mealplans" | ||||||
|  |         :key="index" | ||||||
|  |         cols="12" | ||||||
|  |         sm="12" | ||||||
|  |         md="3" | ||||||
|  |         lg="3" | ||||||
|  |         xl="2" | ||||||
|  |         class="col-borders my-1 d-flex flex-column" | ||||||
|  |       > | ||||||
|  |         <v-card class="mb-2 border-left-primary rounded-sm pa-2"> | ||||||
|  |           <p class="pl-2 mb-1"> | ||||||
|  |             {{ $d(plan.date, "short") }} | ||||||
|  |           </p> | ||||||
|  |         </v-card> | ||||||
|  |         <draggable | ||||||
|  |           tag="div" | ||||||
|  |           handle=".handle" | ||||||
|  |           :value="plan.meals" | ||||||
|  |           group="meals" | ||||||
|  |           :data-index="index" | ||||||
|  |           :data-box="plan.date" | ||||||
|  |           style="min-height: 150px" | ||||||
|  |           @end="onMoveCallback" | ||||||
|  |         > | ||||||
|  |           <v-card | ||||||
|  |             v-for="mealplan in plan.meals" | ||||||
|  |             :key="mealplan.id" | ||||||
|  |             class="my-1" | ||||||
|  |             :class="{ handle: $vuetify.breakpoint.smAndUp }" | ||||||
|  |           > | ||||||
|  |             <v-list-item> | ||||||
|  |               <v-list-item-avatar :rounded="false"> | ||||||
|  |                 <RecipeCardImage | ||||||
|  |                   v-if="mealplan.recipe" | ||||||
|  |                   :recipe-id="mealplan.recipe.id" | ||||||
|  |                   tiny | ||||||
|  |                   icon-size="25" | ||||||
|  |                   :slug="mealplan.recipe ? mealplan.recipe.slug : ''" | ||||||
|  |                 > | ||||||
|  |                 </RecipeCardImage> | ||||||
|  |                 <v-icon v-else> | ||||||
|  |                   {{ $globals.icons.primary }} | ||||||
|  |                 </v-icon> | ||||||
|  |               </v-list-item-avatar> | ||||||
|  |               <v-list-item-content> | ||||||
|  |                 <v-list-item-title class="mb-1"> | ||||||
|  |                   {{ mealplan.recipe ? mealplan.recipe.name : mealplan.title }} | ||||||
|  |                 </v-list-item-title> | ||||||
|  |                 <v-list-item-subtitle style="min-height: 16px"> | ||||||
|  |                   {{ mealplan.recipe ? mealplan.recipe.description + " " : mealplan.text }} | ||||||
|  |                 </v-list-item-subtitle> | ||||||
|  |               </v-list-item-content> | ||||||
|  |             </v-list-item> | ||||||
|  |             <v-divider class="mx-2"></v-divider> | ||||||
|  |             <div class="py-2 px-2 d-flex" style="align-items: center"> | ||||||
|  |               <v-btn small icon :class="{ handle: !$vuetify.breakpoint.smAndUp }"> | ||||||
|  |                 <v-icon> | ||||||
|  |                   {{ $globals.icons.arrowUpDown }} | ||||||
|  |                 </v-icon> | ||||||
|  |               </v-btn> | ||||||
|  |               <v-menu offset-y> | ||||||
|  |                 <template #activator="{ on, attrs }"> | ||||||
|  |                   <v-chip v-bind="attrs" label small color="accent" v-on="on" @click.prevent> | ||||||
|  |                     <v-icon left> | ||||||
|  |                       {{ $globals.icons.tags }} | ||||||
|  |                     </v-icon> | ||||||
|  |                     {{ mealplan.entryType }} | ||||||
|  |                   </v-chip> | ||||||
|  |                 </template> | ||||||
|  |                 <v-list> | ||||||
|  |                   <v-list-item | ||||||
|  |                     v-for="mealType in planTypeOptions" | ||||||
|  |                     :key="mealType.value" | ||||||
|  |                     @click="actions.setType(mealplan, mealType.value)" | ||||||
|  |                   > | ||||||
|  |                     <v-list-item-title> {{ mealType.text }} </v-list-item-title> | ||||||
|  |                   </v-list-item> | ||||||
|  |                 </v-list> | ||||||
|  |               </v-menu> | ||||||
|  |               <v-btn class="ml-auto" small icon @click="actions.deleteOne(mealplan.id)"> | ||||||
|  |                 <v-icon>{{ $globals.icons.delete }}</v-icon> | ||||||
|  |               </v-btn> | ||||||
|  |             </div> | ||||||
|  |           </v-card> | ||||||
|  |         </draggable> | ||||||
|  |         <!-- Day Column Actions --> | ||||||
|  |         <div class="d-flex justify-end mt-auto"> | ||||||
|  |           <BaseButtonGroup | ||||||
|  |             :buttons="[ | ||||||
|  |               { | ||||||
|  |                 icon: $globals.icons.diceMultiple, | ||||||
|  |                 text: $tc('meal-plan.random-meal'), | ||||||
|  |                 event: 'random', | ||||||
|  |                 children: [ | ||||||
|  |                   { | ||||||
|  |                     icon: $globals.icons.diceMultiple, | ||||||
|  |                     text: 'Breakfast', | ||||||
|  |                     event: 'randomBreakfast', | ||||||
|  |                   }, | ||||||
|  |                   { | ||||||
|  |                     icon: $globals.icons.diceMultiple, | ||||||
|  |                     text: $tc('meal-plan.lunch'), | ||||||
|  |                     event: 'randomLunch', | ||||||
|  |                   }, | ||||||
|  |                 ], | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 icon: $globals.icons.potSteam, | ||||||
|  |                 text: $tc('meal-plan.random-dinner'), | ||||||
|  |                 event: 'randomDinner', | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 icon: $globals.icons.bowlMixOutline, | ||||||
|  |                 text: $tc('meal-plan.random-side'), | ||||||
|  |                 event: 'randomSide', | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 icon: $globals.icons.createAlt, | ||||||
|  |                 text: $tc('general.new'), | ||||||
|  |                 event: 'create', | ||||||
|  |               }, | ||||||
|  |             ]" | ||||||
|  |             @create="openDialog(plan.date)" | ||||||
|  |             @randomBreakfast="randomMeal(plan.date, 'breakfast')" | ||||||
|  |             @randomLunch="randomMeal(plan.date, 'lunch')" | ||||||
|  |             @randomDinner="randomMeal(plan.date, 'dinner')" | ||||||
|  |             @randomSide="randomMeal(plan.date, 'side')" | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |       </v-col> | ||||||
|  |     </v-row> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import { defineComponent, computed, reactive, ref, watch } from "@nuxtjs/composition-api"; | ||||||
|  | import { format } from "date-fns"; | ||||||
|  | import { SortableEvent } from "sortablejs"; | ||||||
|  | import draggable from "vuedraggable"; | ||||||
|  | import { watchDebounced } from "@vueuse/core"; | ||||||
|  | import { MealsByDate } from "./types"; | ||||||
|  | import { useMealplans, planTypeOptions } from "~/composables/use-group-mealplan"; | ||||||
|  | import RecipeCardImage from "~/components/Domain/Recipe/RecipeCardImage.vue"; | ||||||
|  | import { PlanEntryType } from "~/lib/api/types/meal-plan"; | ||||||
|  | import { useUserApi } from "~/composables/api"; | ||||||
|  | import { useGroupSelf } from "~/composables/use-groups"; | ||||||
|  | import { RecipeSummary } from "~/lib/api/types/recipe"; | ||||||
|  |  | ||||||
|  | export default defineComponent({ | ||||||
|  |   components: { | ||||||
|  |     draggable, | ||||||
|  |     RecipeCardImage, | ||||||
|  |   }, | ||||||
|  |   props: { | ||||||
|  |     mealplans: { | ||||||
|  |       type: Array as () => MealsByDate[], | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     actions: { | ||||||
|  |       type: Object as () => ReturnType<typeof useMealplans>["actions"], | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   setup(props) { | ||||||
|  |     const api = useUserApi(); | ||||||
|  |     const { group } = useGroupSelf(); | ||||||
|  |  | ||||||
|  |     const state = ref({ | ||||||
|  |       dialog: false, | ||||||
|  |       pickerMenu: null as null | boolean, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const firstDayOfWeek = computed(() => { | ||||||
|  |       const pref = group.value?.preferences?.firstDayOfWeek; | ||||||
|  |  | ||||||
|  |       if (pref) { | ||||||
|  |         return pref; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return 0; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     function onMoveCallback(evt: SortableEvent) { | ||||||
|  |       const supportedEvents = ["drop", "touchend"]; | ||||||
|  |  | ||||||
|  |       // Adapted From https://github.com/SortableJS/Vue.Draggable/issues/1029 | ||||||
|  |       const ogEvent: DragEvent = (evt as any).originalEvent; | ||||||
|  |  | ||||||
|  |       if (ogEvent && ogEvent.type in supportedEvents) { | ||||||
|  |         // The drop was cancelled, unsure if anything needs to be done? | ||||||
|  |         console.log("Cancel Move Event"); | ||||||
|  |       } else { | ||||||
|  |         // A Meal was moved, set the new date value and make an update request and refresh the meals | ||||||
|  |         const fromMealsByIndex = parseInt(evt.from.getAttribute("data-index") ?? ""); | ||||||
|  |         const toMealsByIndex = parseInt(evt.to.getAttribute("data-index") ?? ""); | ||||||
|  |  | ||||||
|  |         if (!isNaN(fromMealsByIndex) && !isNaN(toMealsByIndex)) { | ||||||
|  |           const mealData = props.mealplans[fromMealsByIndex].meals[evt.oldIndex as number]; | ||||||
|  |           const destDate = props.mealplans[toMealsByIndex].date; | ||||||
|  |  | ||||||
|  |           mealData.date = format(destDate, "yyyy-MM-dd"); | ||||||
|  |  | ||||||
|  |           props.actions.updateOne(mealData); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // ===================================================== | ||||||
|  |     // New Meal Dialog | ||||||
|  |  | ||||||
|  |     const dialog = reactive({ | ||||||
|  |       loading: false, | ||||||
|  |       error: false, | ||||||
|  |       note: false, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     watch(dialog, () => { | ||||||
|  |       if (dialog.note) { | ||||||
|  |         newMeal.recipeId = undefined; | ||||||
|  |       } | ||||||
|  |       newMeal.title = ""; | ||||||
|  |       newMeal.text = ""; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const newMeal = reactive({ | ||||||
|  |       date: "", | ||||||
|  |       title: "", | ||||||
|  |       text: "", | ||||||
|  |       recipeId: undefined as string | undefined, | ||||||
|  |       entryType: "dinner" as PlanEntryType, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     function openDialog(date: Date) { | ||||||
|  |       newMeal.date = format(date, "yyyy-MM-dd"); | ||||||
|  |       state.value.dialog = true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function resetDialog() { | ||||||
|  |       newMeal.date = ""; | ||||||
|  |       newMeal.title = ""; | ||||||
|  |       newMeal.text = ""; | ||||||
|  |       newMeal.entryType = "dinner"; | ||||||
|  |       newMeal.recipeId = undefined; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function randomMeal(date: Date, type: PlanEntryType) { | ||||||
|  |       const { data } = await api.mealplans.setRandom({ | ||||||
|  |         date: format(date, "yyyy-MM-dd"), | ||||||
|  |         entryType: type, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (data) { | ||||||
|  |         props.actions.refreshAll(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // ===================================================== | ||||||
|  |     // Search | ||||||
|  |  | ||||||
|  |     const recipes = ref({ | ||||||
|  |       search: "", | ||||||
|  |       loading: false, | ||||||
|  |       error: false, | ||||||
|  |       data: [] as RecipeSummary[], | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     async function searchRecipes(term: string) { | ||||||
|  |       recipes.value.loading = true; | ||||||
|  |       const { data, error } = await api.recipes.search({ | ||||||
|  |         search: term, | ||||||
|  |         page: 1, | ||||||
|  |         orderBy: "name", | ||||||
|  |         orderDirection: "asc", | ||||||
|  |         perPage: 20, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (error) { | ||||||
|  |         console.error(error); | ||||||
|  |         recipes.value.loading = false; | ||||||
|  |         recipes.value.data = []; | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (data) { | ||||||
|  |         recipes.value.data = data.items; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       recipes.value.loading = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     watchDebounced( | ||||||
|  |       () => recipes.value.search, | ||||||
|  |       async (term: string) => { | ||||||
|  |         await searchRecipes(term); | ||||||
|  |       }, | ||||||
|  |       { debounce: 500 } | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       state, | ||||||
|  |       onMoveCallback, | ||||||
|  |       planTypeOptions, | ||||||
|  |  | ||||||
|  |       // Dialog | ||||||
|  |       dialog, | ||||||
|  |       newMeal, | ||||||
|  |       openDialog, | ||||||
|  |       resetDialog, | ||||||
|  |       randomMeal, | ||||||
|  |  | ||||||
|  |       // Search | ||||||
|  |       recipes, | ||||||
|  |       firstDayOfWeek, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
							
								
								
									
										6
									
								
								frontend/pages/group/mealplan/planner/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/pages/group/mealplan/planner/types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | import { ReadPlanEntry } from "~/lib/api/types/meal-plan"; | ||||||
|  |  | ||||||
|  | export type MealsByDate = { | ||||||
|  |   date: Date; | ||||||
|  |   meals: ReadPlanEntry[] | ||||||
|  | } | ||||||
							
								
								
									
										106
									
								
								frontend/pages/group/mealplan/planner/view.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								frontend/pages/group/mealplan/planner/view.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | |||||||
|  | <template> | ||||||
|  |   <v-row> | ||||||
|  |     <v-col | ||||||
|  |       v-for="(day, index) in plan" | ||||||
|  |       :key="index" | ||||||
|  |       cols="12" | ||||||
|  |       sm="12" | ||||||
|  |       md="4" | ||||||
|  |       lg="4" | ||||||
|  |       xl="2" | ||||||
|  |       class="col-borders my-1 d-flex flex-column" | ||||||
|  |     > | ||||||
|  |       <v-card class="mb-2 border-left-primary rounded-sm pa-2"> | ||||||
|  |         <p class="pl-2 mb-1"> | ||||||
|  |           {{ $d(day.date, "short") }} | ||||||
|  |         </p> | ||||||
|  |       </v-card> | ||||||
|  |       <div v-for="section in day.sections" :key="section.title"> | ||||||
|  |         <div class="py-2 d-flex flex-column"> | ||||||
|  |           <div class="primary" style="width: 50px; height: 2.5px"></div> | ||||||
|  |           <p class="text-overline my-0"> | ||||||
|  |             {{ section.title }} | ||||||
|  |           </p> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <RecipeCardMobile | ||||||
|  |           v-for="mealplan in section.meals" | ||||||
|  |           :key="mealplan.id" | ||||||
|  |           :recipe-id="mealplan.recipe ? mealplan.recipe.id : ''" | ||||||
|  |           class="mb-2" | ||||||
|  |           :route="mealplan.recipe ? true : false" | ||||||
|  |           :slug="mealplan.recipe ? mealplan.recipe.slug : mealplan.title" | ||||||
|  |           :description="mealplan.recipe ? mealplan.recipe.description : mealplan.text" | ||||||
|  |           :name="mealplan.recipe ? mealplan.recipe.name : mealplan.title" | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |     </v-col> | ||||||
|  |   </v-row> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import { computed, defineComponent } from "@nuxtjs/composition-api"; | ||||||
|  | import { MealsByDate } from "./types"; | ||||||
|  | import { ReadPlanEntry } from "~/lib/api/types/meal-plan"; | ||||||
|  | import RecipeCardMobile from "~/components/Domain/Recipe/RecipeCardMobile.vue"; | ||||||
|  |  | ||||||
|  | export default defineComponent({ | ||||||
|  |   components: { | ||||||
|  |     RecipeCardMobile, | ||||||
|  |   }, | ||||||
|  |   props: { | ||||||
|  |     mealplans: { | ||||||
|  |       type: Array as () => MealsByDate[], | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   setup(props) { | ||||||
|  |     type DaySection = { | ||||||
|  |       title: string; | ||||||
|  |       meals: ReadPlanEntry[]; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     type Days = { | ||||||
|  |       date: Date; | ||||||
|  |       sections: DaySection[]; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const plan = computed<Days[]>(() => { | ||||||
|  |       return props.mealplans.reduce((acc, day) => { | ||||||
|  |         const out: Days = { | ||||||
|  |           date: day.date, | ||||||
|  |           sections: [ | ||||||
|  |             { title: "Breakfast", meals: [] }, | ||||||
|  |             { title: "Lunch", meals: [] }, | ||||||
|  |             { title: "Dinner", meals: [] }, | ||||||
|  |             { title: "Side", meals: [] }, | ||||||
|  |           ], | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         for (const meal of day.meals) { | ||||||
|  |           if (meal.entryType === "breakfast") { | ||||||
|  |             out.sections[0].meals.push(meal); | ||||||
|  |           } else if (meal.entryType === "lunch") { | ||||||
|  |             out.sections[1].meals.push(meal); | ||||||
|  |           } else if (meal.entryType === "dinner") { | ||||||
|  |             out.sections[2].meals.push(meal); | ||||||
|  |           } else if (meal.entryType === "side") { | ||||||
|  |             out.sections[3].meals.push(meal); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Drop empty sections | ||||||
|  |         out.sections = out.sections.filter((section) => section.meals.length > 0); | ||||||
|  |  | ||||||
|  |         acc.push(out); | ||||||
|  |  | ||||||
|  |         return acc; | ||||||
|  |       }, [] as Days[]); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       plan, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
| @@ -1,21 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div>This Week</div> |  | ||||||
| </template> |  | ||||||
|    |  | ||||||
|   <script lang="ts"> |  | ||||||
| import { defineComponent } from "@nuxtjs/composition-api"; |  | ||||||
|  |  | ||||||
| export default defineComponent({ |  | ||||||
|   setup() { |  | ||||||
|     return {}; |  | ||||||
|   }, |  | ||||||
|   head() { |  | ||||||
|     return { |  | ||||||
|       title: this.$t("meal-plan.dinner-this-week") as string, |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
|    |  | ||||||
|   <style scoped> |  | ||||||
| </style> |  | ||||||
		Reference in New Issue
	
	Block a user