feat(shopping): stats achat produit + édition quantité article

Backend :
- Migration 004 : last_purchased_at (DATE) + avg_interval_days (NUMERIC)
  sur shopping.products
- update_item : met à jour les stats au premier cochage d'un article
  lié à un produit (moyenne mobile exp. 70/30)
- ProductResponse expose les deux nouveaux champs

Frontend :
- ItemRow : long press 500ms → onEdit() (mobile) ; crayon + croix (laptop)
- ShoppingPage : modal édition quantité/unité, état editingItem
- api/shopping.ts : Product inclut last_purchased_at + avg_interval_days

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 07:08:14 +02:00
co-authored by Claude Sonnet 4.6
parent 377531d08e
commit dee7037d70
7 changed files with 171 additions and 14 deletions
+22 -1
View File
@@ -1,6 +1,7 @@
# backend/app/api/shopping.py
import uuid
from datetime import datetime, timezone
from datetime import datetime, timezone, date as date_type
from decimal import Decimal
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import Response
from sqlalchemy import select, text, or_
@@ -270,8 +271,28 @@ async def update_item(
item = result.scalar_one_or_none()
if not item:
raise HTTPException(404, "Article introuvable")
was_checked = item.is_checked
for field, value in payload.model_dump(exclude_unset=True).items():
setattr(item, field, value)
# Mise à jour des stats produit lors du premier cochage
if not was_checked and item.is_checked and item.product_id:
product = await session.get(Product, item.product_id)
if product:
today = date_type.today()
if product.last_purchased_at and product.last_purchased_at < today:
days = (today - product.last_purchased_at).days
if product.avg_interval_days is None:
product.avg_interval_days = Decimal(str(days))
else:
# Moyenne mobile exponentielle (70 % passé, 30 % nouvel intervalle)
product.avg_interval_days = Decimal(str(
round(float(product.avg_interval_days) * 0.7 + days * 0.3, 1)
))
product.last_purchased_at = today
product.frequency_score += 1
await session.commit()
await session.refresh(item, ["product"])
return _item_to_response(item)