feat(shopping): listes projet + déduplication nommage hebdo
Backend : - Migration 007 : list_type VARCHAR(20) sur shopping.lists (weekly/project), url/description/image_url sur shopping.list_items - Modèle ShoppingList : champ list_type - Modèle ListItem : champs url, description, image_url - Schémas : list_type sur Create/Response, nouveaux champs sur ItemCreate/Update/Response - _unique_week_label() : évite les doublons S22 2026 → S22 2026 (2) - finish_shopping : carry-over uniquement pour list_type='weekly' Frontend : - api/shopping.ts : list_type, champs enrichis item, createProjectList() - ProjectItemCard.tsx : carte avec image, description, URL, boutique, cochage - ShoppingPage : · Séparation weekly / project dans la sélection de liste active · Section "Listes projet" sur l'écran vide avec navigation · Badge PROJET dans l'en-tête · Bouton "Clôturer la semaine" et badge "semaine dépassée" masqués sur projet · Bouton "+ Ajouter" (mobile + laptop) sur les listes projet · Vue grille ProjectItemCard pour les listes projet · Modale création liste projet (nom + boutique) · Modale ajout/édition item projet (nom, description, URL, image URL) v0.5.14 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,19 @@ def _iso_week_label() -> str:
|
||||
return f"S{iso[1]} {iso[0]}"
|
||||
|
||||
|
||||
async def _unique_week_label(session: AsyncSession) -> str:
|
||||
base = _iso_week_label()
|
||||
existing = (await session.execute(
|
||||
select(ShoppingList.name).where(ShoppingList.name.like(f"{base}%"))
|
||||
)).scalars().all()
|
||||
if base not in existing:
|
||||
return base
|
||||
counter = 2
|
||||
while f"{base} ({counter})" in existing:
|
||||
counter += 1
|
||||
return f"{base} ({counter})"
|
||||
|
||||
|
||||
def _item_to_response(item: ListItem) -> ListItemResponse:
|
||||
display_name = item.custom_name or (item.product.name if item.product else "Article inconnu")
|
||||
return ListItemResponse(
|
||||
@@ -40,6 +53,9 @@ def _item_to_response(item: ListItem) -> ListItemResponse:
|
||||
price_recorded=item.price_recorded,
|
||||
carried_over=item.carried_over,
|
||||
sort_order=item.sort_order,
|
||||
url=item.url,
|
||||
description=item.description,
|
||||
image_url=item.image_url,
|
||||
)
|
||||
|
||||
|
||||
@@ -48,6 +64,7 @@ def _list_to_response(lst: ShoppingList) -> ShoppingListResponse:
|
||||
return ShoppingListResponse(
|
||||
id=lst.id,
|
||||
name=lst.name,
|
||||
list_type=lst.list_type,
|
||||
store_id=lst.store_id,
|
||||
week_date=lst.week_date,
|
||||
status=lst.status,
|
||||
@@ -167,8 +184,8 @@ async def create_shopping_list(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
data = payload.model_dump()
|
||||
if not data.get('name'):
|
||||
data['name'] = _iso_week_label()
|
||||
if not data.get('name') and data.get('list_type', 'weekly') == 'weekly':
|
||||
data['name'] = await _unique_week_label(session)
|
||||
lst = ShoppingList(**data)
|
||||
session.add(lst)
|
||||
await session.commit()
|
||||
@@ -380,7 +397,7 @@ async def generate_magic_list(session: AsyncSession = Depends(get_session)):
|
||||
result = await session.execute(query)
|
||||
rows = result.mappings().all()
|
||||
|
||||
new_list = ShoppingList(name=_iso_week_label(), status="draft")
|
||||
new_list = ShoppingList(name=await _unique_week_label(session), list_type="weekly", status="draft")
|
||||
session.add(new_list)
|
||||
await session.flush()
|
||||
|
||||
@@ -425,8 +442,13 @@ async def finish_shopping(list_id: uuid.UUID, session: AsyncSession = Depends(ge
|
||||
lst.status = "done"
|
||||
|
||||
unchecked = [i for i in lst.items if not i.is_checked]
|
||||
if unchecked:
|
||||
new_list = ShoppingList(store_id=lst.store_id, status="draft", name=_iso_week_label())
|
||||
if unchecked and lst.list_type == 'weekly':
|
||||
new_list = ShoppingList(
|
||||
store_id=lst.store_id,
|
||||
list_type="weekly",
|
||||
status="draft",
|
||||
name=await _unique_week_label(session),
|
||||
)
|
||||
session.add(new_list)
|
||||
await session.flush()
|
||||
for item in unchecked:
|
||||
|
||||
Reference in New Issue
Block a user