From 84dfbc4a52867427ae03fdc484a6e0d812854172 Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Fri, 1 May 2026 02:05:53 +0200 Subject: [PATCH] feat: enhance ProductPickerField and ParsedReceiptItem to support category filtering in receipt import --- flutter/lib/core/ui/product_picker_field.dart | 31 ++++++++++++++++--- .../import/domain/parsed_receipt_item.dart | 3 ++ .../presentation/receipt_import_tab.dart | 8 +++-- .../pantry/presentation/pantry_screen.dart | 2 +- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/flutter/lib/core/ui/product_picker_field.dart b/flutter/lib/core/ui/product_picker_field.dart index 73e08070..fcd3bb2d 100644 --- a/flutter/lib/core/ui/product_picker_field.dart +++ b/flutter/lib/core/ui/product_picker_field.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; typedef ValueChanged = void Function(T value); /// A named record representing a selectable product option. -typedef ProductOption = ({int id, String name}); +typedef ProductOption = ({int id, String name, int? categoryId}); /// A form field that opens a searchable bottom sheet for selecting a product. /// @@ -110,19 +110,28 @@ class ProductPickerField extends StatelessWidget { /// Öppnar produktväljarens bottenark utan att binda den till en specifik widget-instans. /// Returnerar valt produkt-id, null (ingen ändring), eller [_clearSelectionToken] (rensa). + /// + /// [categoryFilter] — om satt visas bara produkter vars categoryId finns i listan. + /// Används med AI-kategorisuggestion för att förifiltrera på rätt kategorigren. static Future showSheet( BuildContext context, { required List products, int? value, String label = 'Produkt', String? initialQuery, + Set? categoryFilter, }) async { + // Filtrera på kategori om angiven, men visa alla om filtret ger nollresultat + final baseList = categoryFilter != null && categoryFilter.isNotEmpty + ? products.where((p) => p.categoryId != null && categoryFilter.contains(p.categoryId)).toList() + : products; + final useList = baseList.isNotEmpty ? baseList : products; + final result = await showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, builder: (sheetContext) { - // Skapa controller EN gång per öppnad ark — inte inuti StatefulBuilder final controller = TextEditingController(text: initialQuery ?? ''); var query = initialQuery ?? ''; @@ -130,11 +139,13 @@ class ProductPickerField extends StatelessWidget { builder: (ctx, setModalState) { final normalizedQuery = query.trim().toLowerCase(); final filtered = normalizedQuery.isEmpty - ? products - : products + ? useList + : useList .where((p) => p.name.toLowerCase().contains(normalizedQuery)) .toList(); + final isFiltered = useList.length < products.length; + return SizedBox( height: MediaQuery.of(ctx).size.height * 0.85, child: Column( @@ -144,7 +155,17 @@ class ProductPickerField extends StatelessWidget { child: Row( children: [ Expanded( - child: Text(label, style: Theme.of(ctx).textTheme.titleMedium), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: Theme.of(ctx).textTheme.titleMedium), + if (isFiltered) + Text( + 'Visar ${useList.length} produkter i föreslagen kategori', + style: Theme.of(ctx).textTheme.labelSmall?.copyWith(color: Colors.green.shade700), + ), + ], + ), ), TextButton.icon( onPressed: () => Navigator.pop(ctx, _clearSelectionToken), diff --git a/flutter/lib/features/import/domain/parsed_receipt_item.dart b/flutter/lib/features/import/domain/parsed_receipt_item.dart index f9213a07..de7419da 100644 --- a/flutter/lib/features/import/domain/parsed_receipt_item.dart +++ b/flutter/lib/features/import/domain/parsed_receipt_item.dart @@ -15,6 +15,7 @@ class ParsedReceiptItem { // AI-kategorisuggestion (premium) final String? categorySuggestionName; final String? categorySuggestionPath; + final int? categorySuggestionId; ParsedReceiptItem({ required this.rawName, @@ -29,6 +30,7 @@ class ParsedReceiptItem { this.suggestedProductName, this.categorySuggestionName, this.categorySuggestionPath, + this.categorySuggestionId, }); factory ParsedReceiptItem.fromJson(Map json) { @@ -46,6 +48,7 @@ class ParsedReceiptItem { suggestedProductName: json['suggestedProductName'] as String?, categorySuggestionName: cat?['categoryName'] as String?, categorySuggestionPath: cat?['path'] as String?, + categorySuggestionId: (cat?['categoryId'] as num?)?.toInt(), ); } } diff --git a/flutter/lib/features/import/presentation/receipt_import_tab.dart b/flutter/lib/features/import/presentation/receipt_import_tab.dart index e03e7cd4..919b71fd 100644 --- a/flutter/lib/features/import/presentation/receipt_import_tab.dart +++ b/flutter/lib/features/import/presentation/receipt_import_tab.dart @@ -98,14 +98,16 @@ class _EditDialogState extends State<_EditDialog> { _productName = item.suggestedProductName; }); } else if (aiCategory != null) { - // Öppna pickern med råtexten från kvittot som sökord — kategorinamnet - // matchar inte produktnamn, men rawName gör det troligtvis + // Öppna pickern filtrerad på AI-föreslagen kategori (categoryId). + // Visar bara produkter i den kategorin (eller rawName-sökning om kategorin är tom). + final catId = item.categorySuggestionId; ProductPickerField.showSheet( context, products: widget.products, value: _productId, label: 'Produkt', initialQuery: item.rawName, + categoryFilter: catId != null ? {catId} : null, ).then((id) { if (id != null && mounted) { setState(() { @@ -276,7 +278,7 @@ class _ReceiptImportTabState extends ConsumerState { setState(() { _products = list .cast>() - .map((e) => (id: e['id'] as int, name: e['name'] as String)) + .map((e) => (id: e['id'] as int, name: e['name'] as String, categoryId: (e['categoryId'] as num?)?.toInt())) .toList(); _loadingProducts = false; }); diff --git a/flutter/lib/features/pantry/presentation/pantry_screen.dart b/flutter/lib/features/pantry/presentation/pantry_screen.dart index c74a5671..f8662e5b 100644 --- a/flutter/lib/features/pantry/presentation/pantry_screen.dart +++ b/flutter/lib/features/pantry/presentation/pantry_screen.dart @@ -270,7 +270,7 @@ class _PantryScreenState extends ConsumerState { (a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()), ); final availableOptions = availableProducts - .map((p) => (id: p.id, name: p.displayName)) + .map((p) => (id: p.id, name: p.displayName, categoryId: null as int?)) .toList(); final grouped = >{};