summaryrefslogtreecommitdiffstats
path: root/app/templates/meal_plan.html
diff options
context:
space:
mode:
Diffstat (limited to 'app/templates/meal_plan.html')
-rw-r--r--app/templates/meal_plan.html265
1 files changed, 265 insertions, 0 deletions
diff --git a/app/templates/meal_plan.html b/app/templates/meal_plan.html
new file mode 100644
index 0000000..3c3a932
--- /dev/null
+++ b/app/templates/meal_plan.html
@@ -0,0 +1,265 @@
+{% extends "base.html" %}
+{% block title %}Meal Plan — Menu Planner{% endblock %}
+
+{% block content %}
+<div class="d-flex align-items-center justify-content-between mb-4">
+ <div>
+ <h1 class="h3 fw-bold mb-0">Meal Plan</h1>
+ <p class="text-muted mb-0">{{ week_start.strftime('%B %d') }} – {{ dates[-1].strftime('%B %d, %Y') }}</p>
+ </div>
+ <div class="d-flex align-items-center gap-2">
+ <a href="/meal-plan?week={{ prev_week }}" class="btn btn-outline-secondary btn-sm"><i class="bi bi-chevron-left"></i></a>
+ <a href="/meal-plan" class="btn btn-outline-secondary btn-sm">This Week</a>
+ <a href="/meal-plan?week={{ next_week }}" class="btn btn-outline-secondary btn-sm"><i class="bi bi-chevron-right"></i></a>
+ <button onclick="window.print()" class="btn btn-outline-secondary btn-sm">
+ <i class="bi bi-printer me-1"></i>Print
+ </button>
+ {% if current_user.is_authenticated %}
+ <button id="generateShoppingBtn" class="btn btn-success btn-sm">
+ <i class="bi bi-cart-plus me-1"></i>Generate Shopping List
+ </button>
+ {% endif %}
+ </div>
+</div>
+
+{% if not current_user.is_authenticated %}
+<div class="alert alert-info d-flex align-items-center gap-2 mb-3">
+ <i class="bi bi-info-circle-fill"></i>
+ <span><a href="/login">Log in</a> to add or remove meals from the plan.</span>
+</div>
+{% endif %}
+
+<!-- Plan grid -->
+<div class="card shadow-sm mb-4">
+ <div class="card-body p-0">
+ <div class="table-responsive">
+ <table class="table table-bordered mb-0 plan-table">
+ <thead class="table-dark">
+ <tr>
+ <th style="width:110px"></th>
+ {% for d in dates %}
+ <th class="text-center {% if d.isoformat() == today_str %}today-col{% endif %}">
+ <div class="fw-semibold">{{ d.strftime('%A') }}</div>
+ <div class="small text-muted">{{ d.strftime('%b %d') }}</div>
+ </th>
+ {% endfor %}
+ </tr>
+ </thead>
+ <tbody>
+ {% for mt in meal_types %}
+ <tr>
+ <td class="meal-type-label text-capitalize fw-semibold align-middle">{{ mt }}</td>
+ {% for d in dates %}
+ {% set key = d.isoformat() + '_' + mt %}
+ <td class="plan-cell" data-date="{{ d.isoformat() }}" data-meal="{{ mt }}">
+ {% if key in plan %}
+ {% set entry = plan[key] %}
+ <div class="planned-meal" data-meal-id="{{ entry.id }}">
+ <div class="d-flex align-items-start justify-content-between gap-1">
+ <div class="flex-grow-1 overflow-hidden">
+ <div class="d-flex align-items-center gap-1">
+ <span class="cuisine-badge">{{ cuisine_emoji.get(entry.cuisine, '') }}</span>
+ <span class="meal-recipe-name">{{ entry.recipe_name }}</span>
+ </div>
+ <div class="meal-macros">
+ <span>{{ entry.calories_per_serving|int }} cal</span>
+ <span class="mx-1">·</span>
+ <span>{{ entry.carbs_per_serving|int }}g carbs</span>
+ <span class="mx-1">·</span>
+ <span class="fw-semibold">{{ entry.servings }} ppl</span>
+ </div>
+ </div>
+ {% if current_user.is_authenticated %}
+ <button class="btn btn-sm text-muted border-0 remove-meal p-0 flex-shrink-0" title="Remove">
+ <i class="bi bi-x-lg" style="font-size:0.7rem"></i>
+ </button>
+ {% endif %}
+ </div>
+ </div>
+ {% else %}
+ {% if current_user.is_authenticated %}
+ <button class="btn btn-sm add-meal-btn w-100" title="Add meal">
+ <i class="bi bi-plus text-muted"></i>
+ </button>
+ {% else %}
+ <span class="empty-slot">—</span>
+ {% endif %}
+ {% endif %}
+ </td>
+ {% endfor %}
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+ </div>
+</div>
+
+<!-- Toast -->
+<div class="position-fixed bottom-0 end-0 p-3 no-print" style="z-index:1100">
+ <div id="toast" class="toast align-items-center text-bg-success border-0" role="alert">
+ <div class="d-flex">
+ <div class="toast-body" id="toastBody">Done!</div>
+ <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
+ </div>
+ </div>
+</div>
+
+<!-- Recipe picker modal -->
+<div class="modal fade" id="recipeModal" tabindex="-1">
+ <div class="modal-dialog modal-lg modal-dialog-scrollable">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title">Choose a Recipe</h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+ </div>
+ <div class="modal-body">
+
+ <!-- People / servings picker -->
+ <div class="d-flex align-items-center gap-3 mb-3 p-3 bg-light rounded">
+ <label class="fw-semibold mb-0 text-nowrap">
+ <i class="bi bi-people me-1"></i>People eating:
+ </label>
+ <div class="d-flex align-items-center gap-2">
+ <button type="button" id="servingsMinus" class="btn btn-outline-secondary btn-sm px-2">−</button>
+ <span id="servingsDisplay" class="fw-bold fs-5 px-2">2</span>
+ <button type="button" id="servingsPlus" class="btn btn-outline-secondary btn-sm px-2">+</button>
+ </div>
+ <span class="text-muted small">Shopping list quantities will scale accordingly</span>
+ </div>
+
+ <!-- Cuisine quick-filter -->
+ <div id="cuisineFilter" class="d-flex gap-1 flex-wrap mb-3">
+ <button class="btn btn-sm btn-dark cf-btn" data-cuisine="all">All</button>
+ {% for c in cuisines %}
+ <button class="btn btn-sm btn-outline-secondary cf-btn" data-cuisine="{{ c }}">{{ cuisine_emoji.get(c) }} {{ c }}</button>
+ {% endfor %}
+ </div>
+
+ <!-- Search -->
+ <input type="text" id="recipeSearch" class="form-control mb-3" placeholder="Search recipes…">
+
+ <!-- Recipe list -->
+ <div id="recipePickerList" class="row g-2"></div>
+ </div>
+ </div>
+ </div>
+</div>
+{% endblock %}
+
+{% block scripts %}
+<script>
+const allRecipes = {{ recipes_json | safe }};
+let activeDate = null, activeMeal = null, currentCuisine = 'all', currentServings = 2;
+
+const modal = new bootstrap.Modal(document.getElementById('recipeModal'));
+const toast = new bootstrap.Toast(document.getElementById('toast'), {delay: 2500});
+
+function renderPickerList(filter='', cuisine='all') {
+ const list = document.getElementById('recipePickerList');
+ const filtered = allRecipes.filter(r =>
+ (cuisine === 'all' || r.cuisine === cuisine) &&
+ r.name.toLowerCase().includes(filter.toLowerCase())
+ );
+ if (!filtered.length) {
+ list.innerHTML = '<div class="col-12 text-center text-muted py-3">No recipes found</div>';
+ return;
+ }
+ list.innerHTML = filtered.map(r => `
+ <div class="col-12 col-md-6">
+ <button class="btn btn-outline-secondary text-start w-100 recipe-pick-btn p-2" data-id="${r.id}">
+ <div class="fw-semibold">${r.name}</div>
+ <div class="small text-muted">${r.cuisine} &nbsp;·&nbsp; ${Math.round(r.calories_per_serving || 0)} cal/serving</div>
+ </button>
+ </div>
+ `).join('');
+ list.querySelectorAll('.recipe-pick-btn').forEach(btn =>
+ btn.addEventListener('click', () => selectRecipe(parseInt(btn.dataset.id)))
+ );
+}
+
+async function selectRecipe(recipeId) {
+ const res = await api('/meal-plan/add', {
+ method: 'POST',
+ body: JSON.stringify({date: activeDate, meal_type: activeMeal, recipe_id: recipeId, servings: currentServings}),
+ });
+ if (!res) return;
+ const data = await res.json();
+ if (data.success) { modal.hide(); location.reload(); }
+}
+
+// Open modal
+document.querySelectorAll('.add-meal-btn').forEach(btn => {
+ btn.addEventListener('click', function() {
+ const cell = this.closest('.plan-cell');
+ activeDate = cell.dataset.date;
+ activeMeal = cell.dataset.meal;
+ currentCuisine = 'all';
+ document.getElementById('recipeSearch').value = '';
+ document.querySelectorAll('.cf-btn').forEach(b => {
+ b.classList.toggle('btn-dark', b.dataset.cuisine === 'all');
+ b.classList.toggle('btn-outline-secondary', b.dataset.cuisine !== 'all');
+ });
+ renderPickerList('', 'all');
+ modal.show();
+ });
+});
+
+// Remove meal
+document.querySelectorAll('.remove-meal').forEach(btn => {
+ btn.addEventListener('click', async function(e) {
+ e.stopPropagation();
+ const cell = this.closest('.plan-cell');
+ const res = await api('/meal-plan/remove', {
+ method: 'POST',
+ body: JSON.stringify({date: cell.dataset.date, meal_type: cell.dataset.meal}),
+ });
+ if (res && (await res.json()).success) location.reload();
+ });
+});
+
+// Servings +/−
+const servingsDisplay = document.getElementById('servingsDisplay');
+document.getElementById('servingsMinus').addEventListener('click', () => {
+ if (currentServings > 1) { currentServings--; servingsDisplay.textContent = currentServings; }
+});
+document.getElementById('servingsPlus').addEventListener('click', () => {
+ if (currentServings < 20) { currentServings++; servingsDisplay.textContent = currentServings; }
+});
+
+// Cuisine filter
+document.querySelectorAll('.cf-btn').forEach(btn => {
+ btn.addEventListener('click', function() {
+ document.querySelectorAll('.cf-btn').forEach(b => {
+ b.classList.remove('btn-dark'); b.classList.add('btn-outline-secondary');
+ });
+ this.classList.add('btn-dark'); this.classList.remove('btn-outline-secondary');
+ currentCuisine = this.dataset.cuisine;
+ renderPickerList(document.getElementById('recipeSearch').value, currentCuisine);
+ });
+});
+
+document.getElementById('recipeSearch').addEventListener('input', function() {
+ renderPickerList(this.value, currentCuisine);
+});
+
+// Generate shopping list
+document.getElementById('generateShoppingBtn')?.addEventListener('click', async () => {
+ const week = '{{ week_start.isoformat() }}';
+ const res = await api('/shopping-list/generate', {
+ method: 'POST', body: JSON.stringify({week}),
+ });
+ if (!res) return;
+ const data = await res.json();
+ if (data.success) {
+ document.getElementById('toastBody').textContent = 'Shopping list generated!';
+ toast.show();
+ setTimeout(() => window.location.href = `/shopping-list?week=${week}`, 1500);
+ } else {
+ document.getElementById('toast').className = 'toast align-items-center text-bg-danger border-0';
+ document.getElementById('toastBody').textContent = data.error || 'Error';
+ toast.show();
+ }
+});
+</script>
+{% endblock %}