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:
@@ -0,0 +1,24 @@
|
||||
"""004 - stats d'achat sur les produits (last_purchased_at, avg_interval_days)
|
||||
|
||||
Revision ID: 004
|
||||
Revises: 003
|
||||
Create Date: 2026-05-25
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '004'
|
||||
down_revision = '003'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('products', sa.Column('last_purchased_at', sa.Date(), nullable=True), schema='shopping')
|
||||
op.add_column('products', sa.Column('avg_interval_days', sa.Numeric(8, 1), nullable=True), schema='shopping')
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('products', 'avg_interval_days', schema='shopping')
|
||||
op.drop_column('products', 'last_purchased_at', schema='shopping')
|
||||
@@ -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)
|
||||
|
||||
@@ -24,6 +24,8 @@ class Product(Base):
|
||||
quantity_per_unit: Mapped[Decimal | None] = mapped_column(Numeric(8, 3))
|
||||
default_store_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("shopping.stores.id", ondelete="SET NULL"))
|
||||
frequency_score: Mapped[int] = mapped_column(Integer, server_default=text("0"))
|
||||
last_purchased_at: Mapped[date | None] = mapped_column(Date)
|
||||
avg_interval_days: Mapped[Decimal | None] = mapped_column(Numeric(8, 1))
|
||||
owner_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True))
|
||||
created_at: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), server_default=text("now()"))
|
||||
|
||||
|
||||
@@ -69,6 +69,8 @@ class ProductResponse(BaseModel):
|
||||
quantity_per_unit: Decimal | None
|
||||
default_store_id: uuid.UUID | None
|
||||
frequency_score: int
|
||||
last_purchased_at: date | None
|
||||
avg_interval_days: Decimal | None
|
||||
image_path: str | None
|
||||
thumbnail_path: str | None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user