import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/api/api_error_mapper.dart'; import '../../../core/forms/form_options.dart'; import '../../../core/ui/product_picker_field.dart'; import '../../../features/inventory/data/inventory_providers.dart'; 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}); @override ConsumerState createState() => _PantryScreenState(); } class _PantryScreenState extends ConsumerState { int? _selectedProductId; bool _isSubmitting = false; Future _addToInventory(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('Lägg "${item.displayName}" i inventarie'), content: SizedBox( width: 380, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: quantityController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration( labelText: 'Mängd', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), DropdownButtonFormField( initialValue: selectedUnit, isExpanded: true, decoration: const InputDecoration( labelText: 'Enhet', border: 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: const InputDecoration( labelText: 'Plats (valfri)', border: OutlineInputBorder(), ), items: [ const DropdownMenuItem( value: null, child: Text('Ingen plats vald'), ), ...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(context).colorScheme.error, ), ), ], ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Avbryt'), ), FilledButton( onPressed: () { final quantity = double.tryParse(quantityController.text.trim().replaceAll(',', '.')); if (quantity == null || quantity <= 0) { setDialogState(() { formError = 'Ange en giltig mängd över 0.'; }); return; } Navigator.pop(ctx, { 'quantity': quantity, 'unit': selectedUnit, 'location': selectedLocation, }); }, child: const Text('Lägg till'), ), ], ); }, ); }, ); quantityController.dispose(); if (payload == null) return; try { final token = await ref.read(authStateProvider.future); await ref.read(inventoryRepositoryProvider).createInventoryItem( { '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(inventoryProvider); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${item.displayName} tillagd i inventarie.')), ); } catch (error) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(mapErrorToUserMessage(error, context))), ); } } Future _addItem() async { final selectedId = _selectedProductId; if (selectedId == null || _isSubmitting) return; setState(() => _isSubmitting = true); try { final token = await ref.read(authStateProvider.future); await ref .read(pantryRepositoryProvider) .createPantryItem(selectedId, token: token); ref.invalidate(pantryProvider); if (mounted) setState(() => _selectedProductId = null); } catch (error) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(mapErrorToUserMessage(error, context))), ); } finally { if (mounted) setState(() => _isSubmitting = false); } } Future _removeItem(PantryItem item) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Ta bort från baslager?'), content: Text('Vill du ta bort "${item.displayName}"?'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Avbryt'), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Ta bort'), ), ], ), ); 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) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(mapErrorToUserMessage(error, context))), ); } } 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 'Övrigt'; } @override Widget build(BuildContext context) { final pantryAsync = ref.watch(pantryProvider); final productsAsync = ref.watch(pantryProductsProvider); if (pantryAsync.isLoading || productsAsync.isLoading) { return const LoadingStateView(label: 'Laddar baslager...'); } if (pantryAsync.hasError || productsAsync.hasError) { final error = pantryAsync.error ?? productsAsync.error; return ErrorStateView( message: mapErrorToUserMessage(error ?? 'Okänt fel', context), onRetry: () { ref.invalidate(pantryProvider); ref.invalidate(pantryProductsProvider); }, ); } final pantryItems = pantryAsync.maybeWhen(data: (d) => d, orElse: () => null) ?? const []; final products = productsAsync.maybeWhen(data: (d) => d, orElse: () => null) ?? 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)) .toList() ..sort( (a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()), ); final availableOptions = availableProducts .map((p) => (id: p.id, name: p.displayName)) .toList(); final grouped = >{}; for (final item in pantryItems) { final category = _resolveCategory(item, productById); grouped.putIfAbsent(category, () => []).add(item); } final categories = grouped.keys.toList() ..sort((a, b) { if (a == 'Övrigt') return 1; if (b == 'Övrigt') return -1; return a.toLowerCase().compareTo(b.toLowerCase()); }); return ListView( padding: const EdgeInsets.all(16), children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Produkter du alltid räknar med att ha hemma.', style: Theme.of(context).textTheme.bodyMedium, ), IconButton( tooltip: 'Gå till recept', icon: const Icon(Icons.restaurant_menu), onPressed: () => context.go('/recipes'), ), ], ), const SizedBox(height: 12), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: ProductPickerField( products: availableOptions, value: _selectedProductId, enabled: !_isSubmitting && availableProducts.isNotEmpty, label: 'Produkt', onChanged: (value) => setState(() => _selectedProductId = value), ), ), const SizedBox(width: 8), FilledButton( onPressed: (_selectedProductId == null || _isSubmitting || availableProducts.isEmpty) ? null : _addItem, child: _isSubmitting ? const SizedBox( height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Lägg till'), ), ], ), const SizedBox(height: 20), Text( '${pantryItems.length} ${pantryItems.length == 1 ? 'produkt' : 'produkter'} i baslagret', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 12), if (pantryItems.isEmpty) const EmptyStateView( title: 'Baslagret är tomt', description: 'Lägg till produkter ovan.', ) else ...categories.map((category) { final items = grouped[category]!..sort( (a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()), ); return Padding( padding: const EdgeInsets.only(bottom: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( category, style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 8), ...items.map( (item) => Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( title: Text(item.displayName), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ const Tooltip( message: 'Konsumera (inte tillgängligt i baslager)', child: IconButton( onPressed: null, icon: Icon(Icons.remove_circle_outline), ), ), const Tooltip( message: 'Redigera (inte tillgängligt i baslager)', child: IconButton( onPressed: null, icon: Icon(Icons.edit_outlined), ), ), IconButton( tooltip: 'Lägg i inventarie', icon: const Icon(Icons.inventory_2_outlined), onPressed: () => _addToInventory(item), ), IconButton( tooltip: 'Ta bort', icon: const Icon(Icons.delete_outline, color: Colors.red), onPressed: () => _removeItem(item), ), ], ), ), ), ), ], ), ); }), ], ); } }