From 404a5f44ac3b07f0753ac17f835df9aa3f95dd24 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Sat, 16 May 2026 18:54:22 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20aggrega=20duplicati=20nella=20lista=20d?= =?UTF-8?q?ella=20spesa=20con=20quantit=C3=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quando si genera la lista dal piano pasti, gli alimenti ripetuti in più giorni o slot vengono aggregati in un unico elemento con contatore. Es. zucchine lunedì + mercoledì → "zucchine (x2)". - ShoppingItem: aggiunto campo quantity (default 1, persistito in JSON) - ShoppingListProvider.addAll: conta le occorrenze per nome (case-insensitive) e crea un ShoppingItem con quantity = conteggio - ShoppingItemTile: mostra "(xN)" colorato in primary quando quantity > 1, diventa muted insieme al nome quando l'elemento è completato Co-Authored-By: Claude Sonnet 4.6 --- .../shopping_list/models/shopping_item.dart | 7 +++- .../widgets/shopping_item_tile.dart | 35 ++++++++++++++----- .../providers/shopping_list_provider.dart | 26 +++++++++----- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/lib/features/shopping_list/models/shopping_item.dart b/lib/features/shopping_list/models/shopping_item.dart index 8f8854d..e126947 100644 --- a/lib/features/shopping_list/models/shopping_item.dart +++ b/lib/features/shopping_list/models/shopping_item.dart @@ -2,29 +2,34 @@ class ShoppingItem { final String id; final String name; final bool checked; + final int quantity; const ShoppingItem({ required this.id, required this.name, this.checked = false, + this.quantity = 1, }); - ShoppingItem copyWith({String? id, String? name, bool? checked}) => + ShoppingItem copyWith({String? id, String? name, bool? checked, int? quantity}) => ShoppingItem( id: id ?? this.id, name: name ?? this.name, checked: checked ?? this.checked, + quantity: quantity ?? this.quantity, ); Map toJson() => { 'id': id, 'name': name, 'checked': checked, + 'quantity': quantity, }; factory ShoppingItem.fromJson(Map json) => ShoppingItem( id: json['id'] as String, name: json['name'] as String, checked: json['checked'] as bool? ?? false, + quantity: json['quantity'] as int? ?? 1, ); } diff --git a/lib/features/shopping_list/presentation/widgets/shopping_item_tile.dart b/lib/features/shopping_list/presentation/widgets/shopping_item_tile.dart index d022c8c..fccc768 100644 --- a/lib/features/shopping_list/presentation/widgets/shopping_item_tile.dart +++ b/lib/features/shopping_list/presentation/widgets/shopping_item_tile.dart @@ -32,14 +32,33 @@ class ShoppingItemTile extends StatelessWidget { ), ), Expanded( - child: Text( - item.name, - style: item.checked - ? theme.textTheme.bodyMedium?.copyWith( - decoration: TextDecoration.lineThrough, - color: muted, - ) - : theme.textTheme.bodyMedium, + child: Row( + children: [ + Expanded( + child: Text( + item.name, + style: item.checked + ? theme.textTheme.bodyMedium?.copyWith( + decoration: TextDecoration.lineThrough, + color: muted, + ) + : theme.textTheme.bodyMedium, + ), + ), + if (item.quantity > 1) + Padding( + padding: const EdgeInsets.only(left: 6), + child: Text( + '(x${item.quantity})', + style: theme.textTheme.bodySmall?.copyWith( + color: item.checked + ? muted + : theme.colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + ), + ], ), ), IconButton( diff --git a/lib/features/shopping_list/providers/shopping_list_provider.dart b/lib/features/shopping_list/providers/shopping_list_provider.dart index e4b020a..d100f40 100644 --- a/lib/features/shopping_list/providers/shopping_list_provider.dart +++ b/lib/features/shopping_list/providers/shopping_list_provider.dart @@ -49,16 +49,26 @@ class ShoppingListProvider extends ChangeNotifier { void addAll(List names) { final existing = _items.map((i) => i.name.toLowerCase()).toSet(); - final seen = {}; - final toAdd = []; + + // Conta le occorrenze mantenendo il nome nella sua forma originale (prima occorrenza) + final counts = {}; + final canonical = {}; for (final name in names) { final key = name.toLowerCase().trim(); - if (key.isNotEmpty && !existing.contains(key) && seen.add(key)) { - toAdd.add(ShoppingItem( - id: '${DateTime.now().millisecondsSinceEpoch}_${seen.length}', - name: name.trim(), - )); - } + if (key.isEmpty) continue; + counts[key] = (counts[key] ?? 0) + 1; + canonical.putIfAbsent(key, () => name.trim()); + } + + final toAdd = []; + var i = 0; + for (final entry in counts.entries) { + if (existing.contains(entry.key)) continue; + toAdd.add(ShoppingItem( + id: '${DateTime.now().millisecondsSinceEpoch}_${i++}', + name: canonical[entry.key]!, + quantity: entry.value, + )); } if (toAdd.isEmpty) return; _items = [..._items, ...toAdd];