diff --git a/flutter/lib/features/inventory/presentation/inventory_screen.dart b/flutter/lib/features/inventory/presentation/inventory_screen.dart index 7f4cd2ef..b8f48f08 100644 --- a/flutter/lib/features/inventory/presentation/inventory_screen.dart +++ b/flutter/lib/features/inventory/presentation/inventory_screen.dart @@ -6,6 +6,7 @@ import '../../../core/api/api_error_mapper.dart'; import '../../../core/l10n/l10n.dart'; import '../../../core/ui/async_state_views.dart'; import '../../auth/data/auth_providers.dart'; +import '../domain/inventory_item.dart'; import '../data/inventory_providers.dart'; import 'swipeable_inventory_tile.dart'; @@ -56,11 +57,11 @@ class _InventoryScreenState extends ConsumerState { }); } - void _selectAllVisible(List visibleItems) { + void _selectAllVisible(List visibleItems) { setState(() { _selectedIds ..clear() - ..addAll(visibleItems.map((item) => item.id as int)); + ..addAll(visibleItems.map((item) => item.id)); }); } @@ -130,7 +131,7 @@ class _InventoryScreenState extends ConsumerState { ); } - Future _mergeSelected(BuildContext context, List allItems) async { + Future _mergeSelected(BuildContext context, List allItems) async { final selectedItems = allItems .where((item) => _selectedIds.contains(item.id)) .toList(); @@ -165,7 +166,7 @@ class _InventoryScreenState extends ConsumerState { try { final token = await ref.read(authStateProvider.future); - final ids = selectedItems.map((item) => item.id as int).toList(); + final ids = selectedItems.map((item) => item.id).toList(); await ref.read(inventoryRepositoryProvider).mergeInventoryItems( ids, targetUnit: units.length > 1 ? targetUnit : null, @@ -185,10 +186,10 @@ class _InventoryScreenState extends ConsumerState { } } - Future _deleteSelected(BuildContext context, List allItems) async { + Future _deleteSelected(BuildContext context, List allItems) async { final ids = allItems .where((item) => _selectedIds.contains(item.id)) - .map((item) => item.id as int) + .map((item) => item.id) .toList(); if (ids.isEmpty) return; @@ -229,7 +230,7 @@ class _InventoryScreenState extends ConsumerState { } } - Future _openBulkActions(BuildContext context, List allItems) async { + Future _openBulkActions(BuildContext context, List allItems) async { if (_selectedIds.isEmpty) return; final action = await showDialog( context: context, @@ -260,8 +261,155 @@ class _InventoryScreenState extends ConsumerState { } } + List _sortedVisibleItems(List items, String sort) { + final visibleItems = [...items]; + if (sort == 'l1CategoryAsc') { + visibleItems.sort((a, b) { + final byCategory = a.l1Category.toLowerCase().compareTo( + b.l1Category.toLowerCase(), + ); + if (byCategory != 0) return byCategory; + return a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()); + }); + } + return visibleItems; + } + + Widget _buildFilterSection(BuildContext context, String location, String sort) { + return Padding( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.inventoryFilterAndSort, + style: const TextStyle(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: _locationOptions + .map( + (option) => ChoiceChip( + label: Text(option.isEmpty ? context.l10n.inventoryAllFilter : option), + selected: location == option, + onSelected: (_) => ref + .read(inventoryLocationFilterProvider.notifier) + .setValue(option), + ), + ) + .toList(), + ), + const SizedBox(height: 8), + DropdownButtonFormField( + initialValue: sort, + isExpanded: true, + decoration: InputDecoration( + labelText: context.l10n.inventorySortLabel, + border: const OutlineInputBorder(), + ), + items: _sortOptions(context) + .map( + (option) => DropdownMenuItem( + value: option.value, + child: Text(option.label), + ), + ) + .toList(), + onChanged: (value) { + ref.read(inventorySortFilterProvider.notifier).setValue(value ?? ''); + }, + ), + ], + ), + ); + } + + Widget _buildHeaderSection(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 4), + child: Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.l10n.profileInventoryTab, style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + Text( + 'Din personliga inventarie. Här ser du sådant du faktiskt äger, kan sortera på plats och bäst före, och flytta vidare till recept eller baslager.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 8), + const Wrap( + spacing: 8, + runSpacing: 8, + children: [ + Chip(label: Text('User-scope')), + Chip(label: Text('Bäst före')), + Chip(label: Text('Swipa för +/-')), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildSelectedSection( + BuildContext context, + List visibleItems, + List allItems, + ) { + return Padding( + padding: const EdgeInsets.fromLTRB(12, 4, 12, 4), + child: Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${_selectedIds.length} markerade poster', + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + FilledButton.icon( + onPressed: () => _openBulkActions(context, allItems), + icon: const Icon(Icons.playlist_add_check), + label: const Text('Hantera markerade'), + ), + OutlinedButton.icon( + onPressed: () => _selectAllVisible(visibleItems), + icon: const Icon(Icons.select_all), + label: const Text('Markera alla synliga'), + ), + OutlinedButton.icon( + onPressed: _clearSelection, + icon: const Icon(Icons.deselect), + label: const Text('Avmarkera'), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildEmptyState(BuildContext context) { + return EmptyStateView(title: context.l10n.inventoryEmpty); + } + @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final location = ref.watch(inventoryLocationFilterProvider); final sort = ref.watch(inventorySortFilterProvider); final inventoryAsync = ref.watch(inventoryProvider); @@ -284,139 +432,7 @@ class _InventoryScreenState extends ConsumerState { }); } - final visibleItems = [...items]; - if (sort == 'l1CategoryAsc') { - visibleItems.sort((a, b) { - final byCategory = a.l1Category.toLowerCase().compareTo( - b.l1Category.toLowerCase(), - ); - if (byCategory != 0) return byCategory; - return a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()); - }); - } - - final filterSection = Padding( - padding: const EdgeInsets.fromLTRB(12, 12, 12, 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - context.l10n.inventoryFilterAndSort, - style: const TextStyle(fontWeight: FontWeight.w600), - ), - const SizedBox(height: 8), - Wrap( - spacing: 8, - runSpacing: 8, - children: _locationOptions - .map( - (option) => ChoiceChip( - label: Text(option.isEmpty ? context.l10n.inventoryAllFilter : option), - selected: location == option, - onSelected: (_) => ref - .read(inventoryLocationFilterProvider.notifier) - .setValue(option), - ), - ) - .toList(), - ), - const SizedBox(height: 8), - DropdownButtonFormField( - initialValue: sort, - isExpanded: true, - decoration: InputDecoration( - labelText: context.l10n.inventorySortLabel, - border: const OutlineInputBorder(), - ), - items: _sortOptions(context) - .map( - (option) => DropdownMenuItem( - value: option.value, - child: Text(option.label), - ), - ) - .toList(), - onChanged: (value) { - ref - .read(inventorySortFilterProvider.notifier) - .setValue(value ?? ''); - }, - ), - ], - ), - ); - - final headerSection = Padding( - padding: const EdgeInsets.fromLTRB(12, 12, 12, 4), - child: Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(context.l10n.profileInventoryTab, style: Theme.of(context).textTheme.titleMedium), - const SizedBox(height: 8), - Text( - 'Din personliga inventarie. Här ser du sådant du faktiskt äger, kan sortera på plats och bäst före, och flytta vidare till recept eller baslager.', - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 8), - const Wrap( - spacing: 8, - runSpacing: 8, - children: [ - Chip(label: Text('User-scope')), - Chip(label: Text('Bäst före')), - Chip(label: Text('Swipa för +/-')), - ], - ), - ], - ), - ), - ), - ); - - final selectedSection = _selectedIds.isEmpty - ? const SizedBox.shrink() - : Padding( - padding: const EdgeInsets.fromLTRB(12, 4, 12, 4), - child: Card( - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${_selectedIds.length} markerade poster', - style: Theme.of(context).textTheme.titleSmall, - ), - const SizedBox(height: 8), - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - FilledButton.icon( - onPressed: () => _openBulkActions(context, items), - icon: const Icon(Icons.playlist_add_check), - label: const Text('Hantera markerade'), - ), - OutlinedButton.icon( - onPressed: () => _selectAllVisible(visibleItems), - icon: const Icon(Icons.select_all), - label: const Text('Markera alla synliga'), - ), - OutlinedButton.icon( - onPressed: _clearSelection, - icon: const Icon(Icons.deselect), - label: const Text('Avmarkera'), - ), - ], - ), - ], - ), - ), - ), - ); + final visibleItems = _sortedVisibleItems(items, sort); if (visibleItems.isEmpty) { return Stack( @@ -425,10 +441,10 @@ class _InventoryScreenState extends ConsumerState { key: const PageStorageKey('inventory-empty-list'), padding: const EdgeInsets.only(bottom: 88), children: [ - headerSection, - selectedSection, - filterSection, - EmptyStateView(title: context.l10n.inventoryEmpty), + _buildHeaderSection(context), + if (_selectedIds.isNotEmpty) _buildSelectedSection(context, visibleItems, items), + _buildFilterSection(context, location, sort), + _buildEmptyState(context), ], ), Positioned( @@ -452,9 +468,11 @@ class _InventoryScreenState extends ConsumerState { itemCount: visibleItems.length + (_selectedIds.isEmpty ? 2 : 3), separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, index) { - if (index == 0) return filterSection; - if (index == 1) return headerSection; - if (_selectedIds.isNotEmpty && index == 2) return selectedSection; + if (index == 0) return _buildFilterSection(context, location, sort); + if (index == 1) return _buildHeaderSection(context); + if (_selectedIds.isNotEmpty && index == 2) { + return _buildSelectedSection(context, visibleItems, items); + } final item = visibleItems[index - (_selectedIds.isEmpty ? 2 : 3)]; return SwipeableInventoryTile( item: item,