summaryrefslogtreecommitdiffstats
path: root/app/templates/shopping_list.html
diff options
context:
space:
mode:
Diffstat (limited to 'app/templates/shopping_list.html')
-rw-r--r--app/templates/shopping_list.html167
1 files changed, 167 insertions, 0 deletions
diff --git a/app/templates/shopping_list.html b/app/templates/shopping_list.html
new file mode 100644
index 0000000..e6cfaaf
--- /dev/null
+++ b/app/templates/shopping_list.html
@@ -0,0 +1,167 @@
+{% extends "base.html" %}
+{% block title %}Shopping List — Menu Planner{% endblock %}
+
+{% block content %}
+<div class="d-flex align-items-center justify-content-between mb-4 no-print">
+ <div>
+ <h1 class="h3 fw-bold mb-0">Shopping List</h1>
+ <p class="text-muted mb-0">
+ Week of {{ week_start.strftime('%B %d, %Y') }}
+ {% if total %}&nbsp;·&nbsp; {{ checked }}/{{ total }} checked{% endif %}
+ </p>
+ </div>
+ <div class="d-flex gap-2 align-items-center">
+ <a href="/shopping-list?week={{ prev_week }}" class="btn btn-outline-secondary btn-sm"><i class="bi bi-chevron-left"></i></a>
+ <a href="/shopping-list" class="btn btn-outline-secondary btn-sm">This Week</a>
+ <a href="/shopping-list?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 ms-2">
+ <i class="bi bi-printer me-1"></i>Print
+ </button>
+ {% if current_user.is_authenticated %}
+ <button id="regenerateBtn" class="btn btn-success btn-sm">
+ <i class="bi bi-arrow-clockwise me-1"></i>Regenerate
+ </button>
+ {% if total %}
+ <button id="clearBtn" class="btn btn-outline-danger btn-sm">
+ <i class="bi bi-trash me-1"></i>Clear
+ </button>
+ {% endif %}
+ {% endif %}{# end is_authenticated #}
+ </div>
+</div>
+
+{% if categories %}
+
+<!-- Progress bar -->
+{% if total %}
+<div class="mb-4 no-print">
+ <div class="d-flex justify-content-between small text-muted mb-1">
+ <span>{{ checked }} of {{ total }} items collected</span>
+ <span>{{ (checked / total * 100)|int }}%</span>
+ </div>
+ <div class="progress" style="height:8px">
+ <div class="progress-bar bg-success" style="width: {{ (checked / total * 100)|int }}%"></div>
+ </div>
+</div>
+{% endif %}
+
+<div class="row g-4">
+ {% for cat_name, items in categories %}
+ <div class="col-md-6 col-xl-4">
+ <div class="card shadow-sm h-100">
+ <div class="card-header fw-semibold d-flex align-items-center gap-2">
+ {% if cat_name == 'Meat & Poultry' %}<i class="bi bi-egg-fried text-warning"></i>
+ {% elif cat_name == 'Seafood' %}<i class="bi bi-water text-info"></i>
+ {% elif cat_name == 'Dairy & Eggs' %}<i class="bi bi-cup-straw text-warning"></i>
+ {% elif cat_name == 'Produce' %}<i class="bi bi-flower1 text-success"></i>
+ {% elif cat_name == 'Pantry' %}<i class="bi bi-archive text-secondary"></i>
+ {% elif cat_name == 'Spices & Herbs' %}<i class="bi bi-asterisk text-danger"></i>
+ {% else %}<i class="bi bi-box text-muted"></i>{% endif %}
+ {{ cat_name }}
+ <span class="badge bg-secondary ms-auto">{{ items|length }}</span>
+ </div>
+ <ul class="list-group list-group-flush">
+ {% for item in items %}
+ <li class="list-group-item shopping-item {% if item.checked %}checked{% endif %}" data-id="{{ item.id }}">
+ <div class="d-flex align-items-center gap-2">
+ <input type="checkbox" class="form-check-input flex-shrink-0 shop-check" id="item{{ item.id }}"
+ {% if item.checked %}checked{% endif %}>
+ <label for="item{{ item.id }}" class="mb-0 flex-grow-1 check-label">
+ <span class="ing-qty fw-semibold me-1">
+ {% if item.quantity == item.quantity|int %}{{ item.quantity|int }}{% else %}{{ item.quantity }}{% endif %}
+ {{ item.unit }}
+ </span>
+ {{ item.ingredient_name }}
+ </label>
+ {% if item.recipe_sources %}
+ <span class="text-muted small d-none d-sm-inline source-hint" title="{{ item.recipe_sources }}">
+ <i class="bi bi-book"></i>
+ </span>
+ {% endif %}
+ </div>
+ {% if item.recipe_sources %}
+ <div class="small text-muted mt-1 ps-4 source-text">{{ item.recipe_sources }}</div>
+ {% endif %}
+ </li>
+ {% endfor %}
+ </ul>
+ </div>
+ </div>
+ {% endfor %}
+</div>
+
+{% else %}
+<div class="text-center py-5">
+ <i class="bi bi-cart-x fs-1 text-muted"></i>
+ <p class="text-muted mt-3">No shopping list yet for this week.</p>
+ <p class="text-muted small">Add meals to your <a href="/meal-plan">meal plan</a> first, then click Regenerate.</p>
+ <button id="regenerateBtn2" class="btn btn-success mt-2">
+ <i class="bi bi-arrow-clockwise me-1"></i>Generate from Meal Plan
+ </button>
+</div>
+{% endif %}
+
+<!-- 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>
+{% endblock %}
+
+{% block scripts %}
+<script>
+const weekStart = '{{ week_start.isoformat() }}';
+const toast = new bootstrap.Toast(document.getElementById('toast'), {delay: 2000});
+
+// Checkbox toggle
+document.querySelectorAll('.shop-check').forEach(cb => {
+ cb.addEventListener('change', async function() {
+ const item = this.closest('.shopping-item');
+ const res = await api(`/shopping-list/${item.dataset.id}/check`, {
+ method: 'POST', body: JSON.stringify({checked: this.checked}),
+ });
+ if (res) {
+ item.classList.toggle('checked', this.checked);
+ updateProgress();
+ }
+ });
+});
+
+function updateProgress() {
+ const all = document.querySelectorAll('.shop-check').length;
+ const done = document.querySelectorAll('.shop-check:checked').length;
+ const bar = document.querySelector('.progress-bar');
+ if (bar) bar.style.width = all ? `${Math.round(done/all*100)}%` : '0%';
+}
+
+async function regenerate() {
+ const res = await api('/shopping-list/generate', {
+ method: 'POST', body: JSON.stringify({week: weekStart}),
+ });
+ if (!res) return;
+ const data = await res.json();
+ if (data.success) {
+ location.reload();
+ } else {
+ document.getElementById('toastBody').textContent = data.error || 'No meals planned for this week.';
+ document.getElementById('toast').className = 'toast align-items-center text-bg-danger border-0';
+ toast.show();
+ }
+}
+
+document.getElementById('regenerateBtn')?.addEventListener('click', regenerate);
+document.getElementById('regenerateBtn2')?.addEventListener('click', regenerate);
+
+document.getElementById('clearBtn')?.addEventListener('click', async () => {
+ if (!confirm('Clear the shopping list for this week?')) return;
+ const res = await api('/shopping-list/clear', {
+ method: 'POST', body: JSON.stringify({week: weekStart}),
+ });
+ if (res) location.reload();
+});
+</script>
+{% endblock %}