diff --git a/flutter/lib/features/inventory/data/inventory_providers.dart b/flutter/lib/features/inventory/data/inventory_providers.dart index 5054c72f..5600658f 100644 --- a/flutter/lib/features/inventory/data/inventory_providers.dart +++ b/flutter/lib/features/inventory/data/inventory_providers.dart @@ -7,15 +7,36 @@ import '../domain/inventory_item.dart'; import '../domain/inventory_consumption.dart'; import 'inventory_repository.dart'; +class InventoryQuery { + final String location; + final String sort; + + const InventoryQuery({required this.location, required this.sort}); +} + +final inventoryLocationFilterProvider = StateProvider((ref) => ''); +final inventorySortFilterProvider = StateProvider((ref) => ''); + +final inventoryQueryProvider = Provider((ref) { + final location = ref.watch(inventoryLocationFilterProvider); + final sort = ref.watch(inventorySortFilterProvider); + return InventoryQuery(location: location, sort: sort); +}); + final inventoryRepositoryProvider = Provider((ref) { return InventoryRepository(ref.watch(apiClientProvider)); }); final inventoryProvider = FutureProvider>((ref) async { final token = await ref.watch(authStateProvider.future); + final query = ref.watch(inventoryQueryProvider); return guardedApiCall( ref, - () => ref.read(inventoryRepositoryProvider).fetchInventory(token: token), + () => ref.read(inventoryRepositoryProvider).fetchInventory( + location: query.location, + sort: query.sort, + token: token, + ), ); }); diff --git a/flutter/lib/features/inventory/presentation/consumption_history_screen.dart b/flutter/lib/features/inventory/presentation/consumption_history_screen.dart index e4dd3480..aafee345 100644 --- a/flutter/lib/features/inventory/presentation/consumption_history_screen.dart +++ b/flutter/lib/features/inventory/presentation/consumption_history_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/api/api_error_mapper.dart'; import '../../../core/ui/async_state_views.dart'; import '../data/inventory_providers.dart'; import '../domain/inventory_consumption.dart'; @@ -25,7 +26,7 @@ class ConsumptionHistoryScreen extends ConsumerWidget { body: historyAsync.when( loading: () => const LoadingStateView(label: 'Laddar historik...'), error: (e, _) => ErrorStateView( - message: e.toString(), + message: mapErrorToUserMessage(e), onRetry: () => ref.invalidate(consumptionHistoryProvider(itemId)), ), data: (history) { diff --git a/flutter/lib/features/inventory/presentation/inventory_detail_screen.dart b/flutter/lib/features/inventory/presentation/inventory_detail_screen.dart index cf899b19..093045f2 100644 --- a/flutter/lib/features/inventory/presentation/inventory_detail_screen.dart +++ b/flutter/lib/features/inventory/presentation/inventory_detail_screen.dart @@ -36,7 +36,7 @@ class InventoryDetailScreen extends ConsumerWidget { body: itemAsync.when( loading: () => const LoadingStateView(label: 'Laddar...'), error: (e, _) => ErrorStateView( - message: e.toString(), + message: mapErrorToUserMessage(e), onRetry: () => ref.invalidate(inventoryDetailProvider(itemId)), ), data: (item) => ListView( diff --git a/flutter/lib/features/inventory/presentation/inventory_edit_screen.dart b/flutter/lib/features/inventory/presentation/inventory_edit_screen.dart index 5189747c..4e402129 100644 --- a/flutter/lib/features/inventory/presentation/inventory_edit_screen.dart +++ b/flutter/lib/features/inventory/presentation/inventory_edit_screen.dart @@ -130,7 +130,7 @@ class _InventoryEditScreenState extends ConsumerState { body: itemAsync.when( loading: () => const LoadingStateView(label: 'Laddar...'), error: (e, _) => ErrorStateView( - message: e.toString(), + message: mapErrorToUserMessage(e), onRetry: () => ref.invalidate(inventoryDetailProvider(widget.itemId)), ), data: (item) { diff --git a/flutter/lib/features/inventory/presentation/inventory_screen.dart b/flutter/lib/features/inventory/presentation/inventory_screen.dart index f35ffe8f..0d9faa16 100644 --- a/flutter/lib/features/inventory/presentation/inventory_screen.dart +++ b/flutter/lib/features/inventory/presentation/inventory_screen.dart @@ -11,21 +11,87 @@ import '../domain/inventory_item.dart'; class InventoryScreen extends ConsumerWidget { const InventoryScreen({super.key}); + static const _locationOptions = ['', 'Kyl', 'Frys', 'Skafferi']; + static const _sortOptions = <({String value, String label})>[ + (value: '', label: 'Senast tillagda'), + (value: 'nameAsc', label: 'Namn A-O'), + (value: 'bestBeforeAsc', label: 'Bast fore stigande'), + (value: 'bestBeforeDesc', label: 'Bast fore fallande'), + ]; + @override Widget build(BuildContext context, WidgetRef ref) { + final location = ref.watch(inventoryLocationFilterProvider); + final sort = ref.watch(inventorySortFilterProvider); final inventoryAsync = ref.watch(inventoryProvider); return inventoryAsync.when( loading: () => const LoadingStateView(label: 'Laddar inventarie...'), error: (e, _) => ErrorStateView( - message: e.toString(), + message: mapErrorToUserMessage(e), onRetry: () => ref.invalidate(inventoryProvider), ), data: (items) { + final filterSection = Padding( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Filter och sortering', + style: TextStyle(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: _locationOptions + .map( + (option) => ChoiceChip( + label: Text(option.isEmpty ? 'Alla' : option), + selected: location == option, + onSelected: (_) => ref + .read(inventoryLocationFilterProvider.notifier) + .state = option, + ), + ) + .toList(), + ), + const SizedBox(height: 8), + DropdownButtonFormField( + value: sort, + isExpanded: true, + decoration: const InputDecoration( + labelText: 'Sortering', + border: OutlineInputBorder(), + ), + items: _sortOptions + .map( + (option) => DropdownMenuItem( + value: option.value, + child: Text(option.label), + ), + ) + .toList(), + onChanged: (value) { + ref.read(inventorySortFilterProvider.notifier).state = + value ?? ''; + }, + ), + ], + ), + ); + if (items.isEmpty) { return Stack( children: [ - const EmptyStateView(title: 'Inventariet är tomt.'), + ListView( + padding: const EdgeInsets.only(bottom: 88), + children: [ + filterSection, + const EmptyStateView(title: 'Inventariet ar tomt.'), + ], + ), Positioned( right: 16, bottom: 16, @@ -42,10 +108,11 @@ class InventoryScreen extends ConsumerWidget { children: [ ListView.separated( padding: const EdgeInsets.only(bottom: 88), - itemCount: items.length, + itemCount: items.length + 1, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, index) { - final item = items[index]; + if (index == 0) return filterSection; + final item = items[index - 1]; return _InventoryTile(item: item); }, ), diff --git a/flutter/lib/features/pantry/domain/pantry_item.dart b/flutter/lib/features/pantry/domain/pantry_item.dart index 76310e49..ebe7ae9d 100644 --- a/flutter/lib/features/pantry/domain/pantry_item.dart +++ b/flutter/lib/features/pantry/domain/pantry_item.dart @@ -4,6 +4,7 @@ class PantryItem { final String productName; final String? canonicalName; final String? category; + final int? categoryId; const PantryItem({ required this.id, @@ -11,6 +12,7 @@ class PantryItem { required this.productName, this.canonicalName, this.category, + this.categoryId, }); String get displayName { @@ -28,6 +30,7 @@ class PantryItem { productName: (product['name'] ?? '').toString(), canonicalName: product['canonicalName']?.toString(), category: product['category']?.toString(), + categoryId: (product['categoryId'] as num?)?.toInt(), ); } } \ No newline at end of file diff --git a/flutter/lib/features/pantry/domain/pantry_product.dart b/flutter/lib/features/pantry/domain/pantry_product.dart index b4ebded3..4b13e074 100644 --- a/flutter/lib/features/pantry/domain/pantry_product.dart +++ b/flutter/lib/features/pantry/domain/pantry_product.dart @@ -3,12 +3,16 @@ class PantryProduct { final String name; final String? canonicalName; final String? category; + final int? categoryId; + final String? categoryPath; const PantryProduct({ required this.id, required this.name, this.canonicalName, this.category, + this.categoryId, + this.categoryPath, }); String get displayName { @@ -19,11 +23,33 @@ class PantryProduct { } factory PantryProduct.fromJson(Map json) { + final categoryRef = json['categoryRef']; + final path = _buildCategoryPath(categoryRef); + return PantryProduct( id: (json['id'] as num).toInt(), name: (json['name'] ?? '').toString(), canonicalName: json['canonicalName']?.toString(), category: json['category']?.toString(), + categoryId: (json['categoryId'] as num?)?.toInt(), + categoryPath: path, ); } + + static String? _buildCategoryPath(dynamic rawCategoryRef) { + if (rawCategoryRef is! Map) return null; + + final names = []; + dynamic current = rawCategoryRef; + while (current is Map) { + final name = current['name']?.toString().trim(); + if (name != null && name.isNotEmpty) { + names.insert(0, name); + } + current = current['parent']; + } + + if (names.isEmpty) return null; + return names.join(' > '); + } } \ No newline at end of file diff --git a/flutter/lib/features/pantry/presentation/pantry_screen.dart b/flutter/lib/features/pantry/presentation/pantry_screen.dart index 15ec1bd7..b1c6d9c7 100644 --- a/flutter/lib/features/pantry/presentation/pantry_screen.dart +++ b/flutter/lib/features/pantry/presentation/pantry_screen.dart @@ -8,6 +8,7 @@ import '../../auth/data/auth_providers.dart'; import '../../../core/ui/async_state_views.dart'; import '../data/pantry_providers.dart'; import '../domain/pantry_item.dart'; +import '../domain/pantry_product.dart'; class PantryScreen extends ConsumerStatefulWidget { const PantryScreen({super.key}); @@ -219,6 +220,19 @@ class _PantryScreenState extends ConsumerState { } } + String _resolveCategory(PantryItem item, Map productById) { + final fromTree = productById[item.productId]?.categoryPath; + if (fromTree != null && fromTree.trim().isNotEmpty) { + return fromTree; + } + + if (item.category != null && item.category!.trim().isNotEmpty) { + return item.category!; + } + + return 'Ovrigt'; + } + @override Widget build(BuildContext context) { final pantryAsync = ref.watch(pantryProvider); @@ -241,6 +255,7 @@ class _PantryScreenState extends ConsumerState { final pantryItems = pantryAsync.valueOrNull ?? const []; final products = productsAsync.valueOrNull ?? const []; + final productById = {for (final product in products) product.id: product}; final pantryProductIds = pantryItems.map((e) => e.productId).toSet(); final availableProducts = products .where((product) => !pantryProductIds.contains(product.id)) @@ -252,9 +267,7 @@ class _PantryScreenState extends ConsumerState { final grouped = >{}; for (final item in pantryItems) { - final category = (item.category == null || item.category!.isEmpty) - ? 'Ovrigt' - : item.category!; + final category = _resolveCategory(item, productById); grouped.putIfAbsent(category, () => []).add(item); } final categories = grouped.keys.toList()