Pantry move/expiry edit + smart shopping list with summary

- Move items between fridge↔freezer (freezer→fridge sets +7d or shelf life expiry, fridge→freezer clears expiry)
- Inline expiry date editor on fridge items (📅 button)
- Smart shopping list now shows recipe gaps (missing required slot ingredients)
- Summary card shows breakdown: X staple restocks, Y expiring, Z recipe gaps
This commit is contained in:
Caine
2026-04-02 23:18:01 +01:00
parent 726cb9d153
commit d0de1b44ea
6 changed files with 154 additions and 6 deletions
+98 -2
View File
@@ -249,11 +249,67 @@ def pantry_delete(request, item_id):
return render(request, "kitchen/partials/pantry_table.html", ctx)
@csrf_exempt
@require_POST
def pantry_move(request, item_id):
"""Move item between fridge/freezer."""
item = get_object_or_404(PantryItem, id=item_id)
target = request.POST.get("to", "fridge")
if target == "freezer":
item.location = "freezer"
item.expiry_date = None
elif target == "fridge":
item.location = "fridge"
# Default +7 days when defrosting
if item.ingredient.shelf_life_days:
item.expiry_date = date.today() + timedelta(days=item.ingredient.shelf_life_days)
else:
item.expiry_date = date.today() + timedelta(days=7)
item.save(update_fields=["location", "expiry_date"])
ctx = _pantry_context()
return render(request, "kitchen/partials/pantry_table.html", ctx)
@csrf_exempt
@require_POST
def pantry_edit_expiry(request, item_id):
"""Show inline expiry date editor."""
item = get_object_or_404(PantryItem, id=item_id)
return render(request, "kitchen/partials/pantry_expiry_edit.html", {"item": item})
@csrf_exempt
@require_POST
def pantry_save_expiry(request, item_id):
"""Save edited expiry date."""
item = get_object_or_404(PantryItem, id=item_id)
expiry = request.POST.get("expiry_date")
if expiry:
item.expiry_date = expiry
else:
item.expiry_date = None
item.save(update_fields=["expiry_date"])
ctx = _pantry_context()
return render(request, "kitchen/partials/pantry_table.html", ctx)
def pantry_cancel_edit(request):
"""Cancel expiry edit — just re-render the table."""
ctx = _pantry_context()
return render(request, "kitchen/partials/pantry_table.html", ctx)
@csrf_exempt
@require_POST
def shopping_generate(request):
"""Generate smart shopping list and return updated HTML."""
from .views import _get_section, _find_ingredient as api_find, _is_known_staple
from .views import _get_section
staple_count = 0
expiring_count = 0
recipe_gap_count = 0
suggestions = []
@@ -263,7 +319,9 @@ def shopping_generate(request):
"ingredient": item.ingredient.name,
"reason": "restock staple",
"section": _get_section(item.ingredient),
"type": "staple",
})
staple_count += 1
# Expiring items
cutoff = date.today() + timedelta(days=2)
@@ -274,7 +332,34 @@ def shopping_generate(request):
"ingredient": item.ingredient.name,
"reason": f"expiring {item.expiry_date}",
"section": _get_section(item.ingredient),
"type": "expiring",
})
expiring_count += 1
# Recipe gaps — check required slots with zero available options
for meta in MetaRecipe.objects.prefetch_related(
"slots__options__ingredient", "base_ingredients__ingredient"
).all():
for slot in meta.slots.all():
if not slot.required:
continue
any_available = False
first_option = None
for opt in slot.options.all():
if not first_option:
first_option = opt
available = _get_pantry_total(opt.ingredient_id)
if available >= opt.quantity_per_serving * 2:
any_available = True
break
if not any_available and first_option:
suggestions.append({
"ingredient": first_option.ingredient.name,
"reason": f"for {meta.name} ({slot.name})",
"section": _get_section(first_option.ingredient),
"type": "recipe",
})
recipe_gap_count += 1
# Dedupe and save
seen = set()
@@ -294,7 +379,18 @@ def shopping_generate(request):
items = ShoppingListItem.objects.select_related("ingredient").all()
for item in items:
item.section = _get_section_for_item(item)
return render(request, "kitchen/partials/shopping_list.html", {"items": items})
summary = {
"total": len(set(s["ingredient"] for s in suggestions)),
"staples": staple_count,
"expiring": expiring_count,
"recipe_gaps": recipe_gap_count,
}
return render(request, "kitchen/partials/shopping_list.html", {
"items": items,
"summary": summary,
})
@csrf_exempt