import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; 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'; class InventoryScreen extends ConsumerStatefulWidget { const InventoryScreen({super.key}); @override ConsumerState createState() => _InventoryScreenState(); } class _InventoryScreenState extends ConsumerState { final Set _selectedIds = {}; static const _locationOptions = ['', 'Kyl', 'Frys', 'Skafferi']; static const _weightOrVolumeUnits = { 'g', 'gram', 'mg', 'milligram', 'hg', 'hektogram', 'kg', 'kilo', 'kilogram', 'ml', 'milliliter', 'cl', 'centiliter', 'dl', 'deciliter', 'l', 'liter', }; List<({String value, String label})> _sortOptions(BuildContext context) => [ (value: '', label: context.l10n.inventorySortLatest), (value: 'nameAsc', label: context.l10n.inventorySortNameAsc), (value: 'bestBeforeAsc', label: context.l10n.inventorySortBestBeforeAsc), (value: 'bestBeforeDesc', label: context.l10n.inventorySortBestBeforeDesc), (value: 'l1CategoryAsc', label: 'L1-kategori (A-O)'), ]; void _startSelection(int id) { setState(() { _selectedIds.add(id); }); } void _toggleSelection(int id) { setState(() { if (_selectedIds.contains(id)) { _selectedIds.remove(id); } else { _selectedIds.add(id); } }); } void _clearSelection() { setState(() { _selectedIds.clear(); }); } void _selectAllVisible(List visibleItems) { setState(() { _selectedIds ..clear() ..addAll(visibleItems.map((item) => item.id)); }); } bool _isWeightOrVolumeUnit(String unit) { return _weightOrVolumeUnits.contains(unit.trim().toLowerCase()); } String _preferredUnit(List units) { for (final unit in units) { if (_isWeightOrVolumeUnit(unit)) return unit; } return units.first; } Future _askMergeTargetUnit(BuildContext context, List units) async { if (units.isEmpty) return null; if (units.length == 1) return units.first; var selected = _preferredUnit(units); return showDialog( context: context, builder: (dialogContext) => StatefulBuilder( builder: (dialogContext, setDialogState) => AlertDialog( title: const Text('Välj enhet för merge'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Poster har olika enhet. Välj vilken enhet den sammanslagna posten ska använda.', ), const SizedBox(height: 12), DropdownButtonFormField( initialValue: selected, decoration: const InputDecoration( labelText: 'Enhet', border: OutlineInputBorder(), ), items: units .map( (unit) => DropdownMenuItem( value: unit, child: Text(unit), ), ) .toList(), onChanged: (value) { if (value == null) return; setDialogState(() => selected = value); }, ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), child: Text(context.l10n.cancelAction), ), FilledButton( onPressed: () => Navigator.pop(dialogContext, selected), child: const Text('Merge'), ), ], ), ), ); } Future _mergeSelected(BuildContext context, List allItems) async { final selectedItems = allItems .where((item) => _selectedIds.contains(item.id)) .toList(); if (selectedItems.length < 2) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Välj minst två poster för merge.')), ); return; } final productIds = selectedItems.map((item) => item.productId).toSet(); if (productIds.length > 1) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Valda poster måste ha samma produkt för merge.')), ); return; } final units = []; final seenUnits = {}; for (final item in selectedItems) { final unit = item.unit.trim(); final key = unit.toLowerCase(); if (seenUnits.add(key)) { units.add(unit); } } final targetUnit = await _askMergeTargetUnit(context, units); if (!context.mounted || targetUnit == null) return; try { final token = await ref.read(authStateProvider.future); final ids = selectedItems.map((item) => item.id).toList(); await ref.read(inventoryRepositoryProvider).mergeInventoryItems( ids, targetUnit: units.length > 1 ? targetUnit : null, token: token, ); ref.invalidate(inventoryProvider); if (!mounted) return; setState(() => _selectedIds.clear()); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Poster sammanslagna.')), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)), ); } } Future _deleteSelected(BuildContext context, List allItems) async { final ids = allItems .where((item) => _selectedIds.contains(item.id)) .map((item) => item.id) .toList(); if (ids.isEmpty) return; final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('Ta bort markerade poster'), content: Text('Vill du verkligen ta bort ${ids.length} markerade poster?'), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: Text(context.l10n.cancelAction), ), FilledButton( onPressed: () => Navigator.pop(dialogContext, true), child: Text(context.l10n.deleteAction), ), ], ), ); if (confirmed != true || !context.mounted) return; try { final token = await ref.read(authStateProvider.future); await ref.read(inventoryRepositoryProvider).bulkDeleteInventoryItems(ids, token: token); ref.invalidate(inventoryProvider); if (!mounted) return; setState(() => _selectedIds.clear()); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${ids.length} poster borttagna.')), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)), ); } } Future _openBulkActions(BuildContext context, List allItems) async { if (_selectedIds.isEmpty) return; final action = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('Hantera markerade poster'), content: Text('Du har markerat ${_selectedIds.length} poster. Välj åtgärd.'), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), child: Text(context.l10n.cancelAction), ), OutlinedButton( onPressed: () => Navigator.pop(dialogContext, 'delete'), child: Text(context.l10n.deleteAction), ), FilledButton( onPressed: () => Navigator.pop(dialogContext, 'merge'), child: const Text('Merge'), ), ], ), ); if (action == 'merge') { await _mergeSelected(context, allItems); } else if (action == 'delete') { await _deleteSelected(context, allItems); } } 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) { final location = ref.watch(inventoryLocationFilterProvider); final sort = ref.watch(inventorySortFilterProvider); final inventoryAsync = ref.watch(inventoryProvider); return inventoryAsync.when( loading: () => LoadingStateView(label: context.l10n.inventoryLoading), error: (e, _) => ErrorStateView( message: mapErrorToUserMessage(e, context), onRetry: () => ref.invalidate(inventoryProvider), ), data: (items) { final itemIds = items.map((item) => item.id).toSet(); final staleIds = _selectedIds.where((id) => !itemIds.contains(id)).toList(); if (staleIds.isNotEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; setState(() { _selectedIds.removeWhere((id) => !itemIds.contains(id)); }); }); } final visibleItems = _sortedVisibleItems(items, sort); if (visibleItems.isEmpty) { return Stack( children: [ ListView( key: const PageStorageKey('inventory-empty-list'), padding: const EdgeInsets.only(bottom: 88), children: [ _buildHeaderSection(context), if (_selectedIds.isNotEmpty) _buildSelectedSection(context, visibleItems, items), _buildFilterSection(context, location, sort), _buildEmptyState(context), ], ), Positioned( right: 16, bottom: 16, child: FloatingActionButton.extended( heroTag: 'inventory_add_empty', onPressed: () => context.push('/inventory/create'), icon: const Icon(Icons.add), label: Text(context.l10n.addAction), ), ), ], ); } return Stack( children: [ ListView.separated( key: const PageStorageKey('inventory-main-list'), padding: const EdgeInsets.only(bottom: 88), itemCount: visibleItems.length + (_selectedIds.isEmpty ? 2 : 3), separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, index) { 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, selectableMode: _selectedIds.isNotEmpty, selected: _selectedIds.contains(item.id), onToggleSelected: () => _toggleSelection(item.id), onLongPress: () { if (_selectedIds.isEmpty) { _startSelection(item.id); } else { _toggleSelection(item.id); } }, ); }, ), Positioned( right: 16, bottom: 16, child: Column( mainAxisSize: MainAxisSize.min, children: [ FloatingActionButton.extended( heroTag: 'inventory_add', onPressed: () => context.push('/inventory/create'), icon: const Icon(Icons.add), label: Text(context.l10n.addAction), ), const SizedBox(height: 8), FloatingActionButton.extended( heroTag: 'inventory_go_recipes', onPressed: () => context.go('/recipes'), icon: const Icon(Icons.restaurant_menu), label: Text(context.l10n.inventoryRecipesAction), ), ], ), ), ], ); }, ); } }