import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import '../../../core/api/api_error_mapper.dart'; import '../../../core/forms/form_options.dart'; import '../../../core/l10n/l10n.dart'; import '../../../core/ui/async_state_views.dart'; import '../../../core/utils/display_labels.dart'; import '../../auth/data/auth_providers.dart'; import '../../inventory/data/inventory_providers.dart'; import '../data/pantry_providers.dart'; import '../domain/pantry_item.dart'; final _logger = Logger('PantryScreen'); class PantryScreen extends ConsumerStatefulWidget { const PantryScreen({super.key}); @override ConsumerState createState() => _PantryScreenState(); } class _PantryScreenState extends ConsumerState { static const _locationOptions = ['', 'Kyl', 'Frys', 'Skafferi']; String _locationFilter = ''; String _sort = 'nameAsc'; List<({String value, String label})> _sortOptions() => const [ (value: 'nameAsc', label: 'Namn (A-O)'), (value: 'nameDesc', label: 'Namn (O-A)'), (value: 'l1CategoryAsc', label: 'L1-kategori (A-O)'), (value: 'locationAsc', label: 'Plats (A-O)'), ]; @override void initState() { super.initState(); _logger.info('Initializing PantryScreen'); } Future _moveToInventory(PantryItem item) async { final quantityController = TextEditingController(text: '1'); String selectedUnit = 'st'; String? selectedLocation; String? formError; final payload = await showDialog>( context: context, builder: (ctx) { return StatefulBuilder( builder: (ctx, setDialogState) { return AlertDialog( title: Text(context.l10n.pantryAddToInventoryTitle(item.displayName)), content: SizedBox( width: 380, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: quantityController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: InputDecoration( labelText: context.l10n.inventoryQuantityDisplayLabel, border: const OutlineInputBorder(), ), ), const SizedBox(height: 12), DropdownButtonFormField( initialValue: selectedUnit, isExpanded: true, decoration: InputDecoration( labelText: context.l10n.unitLabel, border: const OutlineInputBorder(), ), items: unitOptions .map((option) => DropdownMenuItem( value: option.value, child: Text(option.label), )) .toList(), onChanged: (value) { if (value == null) return; setDialogState(() => selectedUnit = value); }, ), const SizedBox(height: 12), DropdownButtonFormField( initialValue: selectedLocation, isExpanded: true, decoration: InputDecoration( labelText: context.l10n.locationOptionalLabel, border: const OutlineInputBorder(), ), items: [ DropdownMenuItem( value: null, child: Text(context.l10n.pantryNoLocation), ), ...inventoryLocationOptions.map( (location) => DropdownMenuItem( value: location, child: Text(location), ), ), ], onChanged: (value) { setDialogState(() => selectedLocation = value); }, ), if (formError != null) ...[ const SizedBox(height: 8), Text( formError!, style: TextStyle(color: Theme.of(ctx).colorScheme.error), ), ], ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: Text(context.l10n.cancelAction), ), FilledButton( onPressed: () { final quantity = double.tryParse( quantityController.text.trim().replaceAll(',', '.'), ); if (quantity == null || quantity <= 0) { setDialogState(() { formError = context.l10n.pantryInvalidQuantity; }); return; } Navigator.pop(ctx, { 'quantity': quantity, 'unit': selectedUnit, 'location': selectedLocation, }); }, child: Text(context.l10n.addAction), ), ], ); }, ); }, ); quantityController.dispose(); if (payload == null) return; try { final token = await ref.read(authStateProvider.future); await ref.read(pantryRepositoryProvider).movePantryItemToInventory( item.id, body: { 'productId': item.productId, 'quantity': payload['quantity'] as double, 'unit': payload['unit'] as String, if (payload['location'] != null) 'location': payload['location'] as String, }, token: token, ); ref.invalidate(pantryProvider); ref.invalidate(inventoryProvider); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Flyttade "${item.displayName}" till inventarie.')), ); } catch (error) { _logger.severe('Failed to add item to inventory: $error'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( buildCopyableErrorSnackBar(context, mapErrorToUserMessage(error, context)), ); } } Future _removeItem(PantryItem item) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(context.l10n.pantryRemoveTitle), content: Text(context.l10n.pantryRemoveContent(item.displayName)), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: Text(context.l10n.cancelAction), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), child: Text(context.l10n.deleteAction), ), ], ), ); if (confirmed != true) return; try { final token = await ref.read(authStateProvider.future); await ref.read(pantryRepositoryProvider).deletePantryItem(item.id, token: token); ref.invalidate(pantryProvider); } catch (error) { _logger.severe('Failed to remove pantry item: $error'); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( buildCopyableErrorSnackBar(context, mapErrorToUserMessage(error, context)), ); } } String _resolveL1Category(PantryItem item) { return item.l1CategoryOrNull ?? context.l10n.pantryOtherCategory; } @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final textTheme = theme.textTheme; final pantryAsync = ref.watch(pantryProvider); if (pantryAsync.isLoading) { return LoadingStateView(label: context.l10n.pantryLoading); } if (pantryAsync.hasError) { final error = pantryAsync.error; _logger.severe('Error loading pantry or products: $error'); return buildCopyableErrorPanel( context: context, message: mapErrorToUserMessage(error ?? 'Okänt fel', context), onRetry: () { ref.invalidate(pantryProvider); }, title: 'Kunde inte läsa baslagret', ); } final pantryItems = pantryAsync.maybeWhen(data: (d) => d, orElse: () => null) ?? const []; final filteredItems = pantryItems.where((item) { if (_locationFilter.isEmpty) return true; return (item.location ?? '').trim() == _locationFilter; }).toList(); final l1LowerByItemId = { for (final item in filteredItems) item.id: _resolveL1Category(item).toLowerCase(), }; filteredItems.sort((a, b) { if (_sort == 'nameDesc') { return b.displayName.toLowerCase().compareTo(a.displayName.toLowerCase()); } if (_sort == 'locationAsc') { final byLocation = (a.location ?? '').toLowerCase().compareTo( (b.location ?? '').toLowerCase(), ); if (byLocation != 0) return byLocation; return a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()); } if (_sort == 'l1CategoryAsc') { final byL1 = (l1LowerByItemId[a.id] ?? '').compareTo(l1LowerByItemId[b.id] ?? ''); if (byL1 != 0) return byL1; return a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()); } 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: _locationFilter == option, onSelected: (_) => setState(() => _locationFilter = option), ), ) .toList(), ), const SizedBox(height: 8), DropdownButtonFormField( initialValue: _sort, isExpanded: true, decoration: InputDecoration( labelText: context.l10n.inventorySortLabel, border: const OutlineInputBorder(), ), items: _sortOptions() .map( (option) => DropdownMenuItem( value: option.value, child: Text(option.label), ), ) .toList(), onChanged: (value) { setState(() => _sort = value ?? 'nameAsc'); }, ), ], ), ); 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('Baslager', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), Text( 'Det här är ditt user-scope baslager. Här lagrar du sådant du vill ha lätt åtkomligt och kan flytta poster vidare till inventarie när det behövs.', 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('Flytta till inventarie')), Chip(label: Text('Plats + kategori')), ], ), ], ), ), ), ); final content = filteredItems.isEmpty ? ListView( key: const PageStorageKey('pantry-empty-list'), padding: const EdgeInsets.fromLTRB(12, 0, 12, 96), children: [ headerSection, filterSection, const EmptyStateView( title: 'Baslagret är tomt', description: 'Lägg till produkter med plusknappen.', ), ], ) : ListView.separated( key: const PageStorageKey('pantry-main-list'), padding: const EdgeInsets.fromLTRB(12, 0, 12, 96), itemCount: filteredItems.length + 2, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, index) { if (index == 0) return filterSection; if (index == 1) return headerSection; final item = filteredItems[index - 2]; final l1Category = _resolveL1Category(item); final location = normalizedOptionalText(item.location); return ListTile( title: Text(item.displayName), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ if (location != null) Text( locationLabel('${context.l10n.locationLabel}: ', location), overflow: TextOverflow.ellipsis, ), const SizedBox(height: 6), Align( alignment: Alignment.centerLeft, child: Chip( label: Text( l1CategoryChipLabel('L1: ', l1Category), maxLines: 1, overflow: TextOverflow.ellipsis, ), padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, side: BorderSide(color: colorScheme.outlineVariant), backgroundColor: colorScheme.surface, labelStyle: textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), ), ), ], ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( tooltip: 'Flytta till inventarie', icon: const Icon(Icons.inventory_2_outlined), onPressed: () => _moveToInventory(item), ), IconButton( tooltip: 'Ta bort', icon: const Icon( Icons.delete_outline, color: Colors.red, ), onPressed: () => _removeItem(item), ), ], ), ); }, ); return Stack( children: [ content, Positioned( right: 16, bottom: 16, child: Column( mainAxisSize: MainAxisSize.min, children: [ FloatingActionButton.extended( heroTag: 'pantry_add', onPressed: () => context.push('/inventory/create?destination=pantry'), icon: const Icon(Icons.add), label: Text(context.l10n.addAction), ), const SizedBox(height: 8), FloatingActionButton.extended( heroTag: 'pantry_go_recipes', onPressed: () => context.go('/recipes'), icon: const Icon(Icons.restaurant_menu), label: Text(context.l10n.inventoryRecipesAction), ), ], ), ), ], ); } }