feat: aggrega duplicati nella lista della spesa con quantità
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 <noreply@anthropic.com>
This commit is contained in:
@@ -2,29 +2,34 @@ class ShoppingItem {
|
|||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final bool checked;
|
final bool checked;
|
||||||
|
final int quantity;
|
||||||
|
|
||||||
const ShoppingItem({
|
const ShoppingItem({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
this.checked = false,
|
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(
|
ShoppingItem(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
checked: checked ?? this.checked,
|
checked: checked ?? this.checked,
|
||||||
|
quantity: quantity ?? this.quantity,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'id': id,
|
'id': id,
|
||||||
'name': name,
|
'name': name,
|
||||||
'checked': checked,
|
'checked': checked,
|
||||||
|
'quantity': quantity,
|
||||||
};
|
};
|
||||||
|
|
||||||
factory ShoppingItem.fromJson(Map<String, dynamic> json) => ShoppingItem(
|
factory ShoppingItem.fromJson(Map<String, dynamic> json) => ShoppingItem(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
checked: json['checked'] as bool? ?? false,
|
checked: json['checked'] as bool? ?? false,
|
||||||
|
quantity: json['quantity'] as int? ?? 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ class ShoppingItemTile extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
item.name,
|
item.name,
|
||||||
@@ -42,6 +45,22 @@ class ShoppingItemTile extends StatelessWidget {
|
|||||||
: theme.textTheme.bodyMedium,
|
: 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(
|
IconButton(
|
||||||
onPressed: onRemove,
|
onPressed: onRemove,
|
||||||
icon: const Icon(Icons.close, size: 16),
|
icon: const Icon(Icons.close, size: 16),
|
||||||
|
|||||||
@@ -49,16 +49,26 @@ class ShoppingListProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
void addAll(List<String> names) {
|
void addAll(List<String> names) {
|
||||||
final existing = _items.map((i) => i.name.toLowerCase()).toSet();
|
final existing = _items.map((i) => i.name.toLowerCase()).toSet();
|
||||||
final seen = <String>{};
|
|
||||||
final toAdd = <ShoppingItem>[];
|
// Conta le occorrenze mantenendo il nome nella sua forma originale (prima occorrenza)
|
||||||
|
final counts = <String, int>{};
|
||||||
|
final canonical = <String, String>{};
|
||||||
for (final name in names) {
|
for (final name in names) {
|
||||||
final key = name.toLowerCase().trim();
|
final key = name.toLowerCase().trim();
|
||||||
if (key.isNotEmpty && !existing.contains(key) && seen.add(key)) {
|
if (key.isEmpty) continue;
|
||||||
toAdd.add(ShoppingItem(
|
counts[key] = (counts[key] ?? 0) + 1;
|
||||||
id: '${DateTime.now().millisecondsSinceEpoch}_${seen.length}',
|
canonical.putIfAbsent(key, () => name.trim());
|
||||||
name: name.trim(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final toAdd = <ShoppingItem>[];
|
||||||
|
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;
|
if (toAdd.isEmpty) return;
|
||||||
_items = [..._items, ...toAdd];
|
_items = [..._items, ...toAdd];
|
||||||
|
|||||||
Reference in New Issue
Block a user