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/l10n/l10n.dart'; import '../../../core/ui/category_then_product_picker.dart'; import '../../../core/ui/searchable_category_field.dart'; import '../../../core/ui/product_picker_field.dart'; import '../data/admin_repository.dart'; import '../domain/admin_category_node.dart'; import '../domain/admin_pantry_item.dart'; import '../domain/admin_product.dart'; import '../domain/user_admin.dart'; class AdminPantryPanel extends ConsumerStatefulWidget { final bool embedded; const AdminPantryPanel({super.key, this.embedded = false}); @override ConsumerState createState() => _AdminPantryPanelState(); } class _AdminPantryPanelState extends ConsumerState { bool _isLoading = true; String? _error; String _search = ''; int? _selectedUserId; List _items = []; List _products = []; List _categories = []; List _categoryOptions = []; List _users = []; @override void initState() { super.initState(); _load(); } Future _load() async { setState(() { _isLoading = true; _error = null; }); try { final results = await Future.wait([ ref.read(adminRepositoryProvider).listAdminPantry(userId: _selectedUserId), ref.read(adminRepositoryProvider).listGlobalProducts(), ref.read(adminRepositoryProvider).listCategoryTree(), ref.read(adminRepositoryProvider).listUsers(), ]); if (!mounted) return; setState(() { _items = results[0] as List; _products = results[1] as List; _categories = results[2] as List; _categoryOptions = _flattenCategoryOptions(_categories); _users = results[3] as List; }); } catch (e) { if (!mounted) return; setState(() => _error = mapErrorToUserMessage(e, context)); } finally { if (mounted) setState(() => _isLoading = false); } } List get _filtered { final q = _search.trim().toLowerCase(); if (q.isEmpty) return _items; return _items.where((item) { return item.displayName.toLowerCase().contains(q) || item.username.toLowerCase().contains(q) || item.userEmail.toLowerCase().contains(q) || (item.location ?? '').toLowerCase().contains(q) || (item.categoryPath ?? '').toLowerCase().contains(q); }).toList(); } List _flattenCategoryOptions( List nodes, [ List parents = const [], ]) { final result = []; for (final node in nodes) { final pathParts = [...parents, node.name]; final path = pathParts.join(' > '); result.add((value: node.id.toString(), label: path)); result.addAll(_flattenCategoryOptions(node.children, pathParts)); } return result; } Future _addItem() async { final values = await _showPantryFormDialog(initialOwnerUserId: _selectedUserId); if (values == null) return; try { await ref.read(adminRepositoryProvider).createAdminPantry( userId: values.ownerUserId, productId: values.productId, location: values.location, ); if (!mounted) return; await _load(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Baslager-post skapad.')), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)), ); } } Future _editItem(AdminPantryItem item) async { final values = await _showPantryFormDialog(initial: item); if (values == null) return; try { await ref.read(adminRepositoryProvider).updateAdminPantry( item.id, productId: values.productId, location: values.location, ); if (!mounted) return; await _load(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Baslager-post uppdaterad.')), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)), ); } } Future<_PantryFormValues?> _showPantryFormDialog({ AdminPantryItem? initial, int? initialOwnerUserId, }) { return showDialog<_PantryFormValues>( context: context, builder: (context) => _PantryFormDialog( users: _users, products: _products, categories: _categories, initial: initial, initialOwnerUserId: initialOwnerUserId, ), ); } Future _moveToInventory(AdminPantryItem 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 { await ref.read(adminRepositoryProvider).moveAdminPantryToInventory( item.id, { 'productId': item.productId, 'quantity': payload['quantity'] as double, 'unit': payload['unit'] as String, if (payload['location'] != null) 'location': payload['location'] as String, }, ); if (!mounted) return; await _load(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Flyttade "${item.displayName}" till inventarie.')), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)), ); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); if (_isLoading) return const Center(child: CircularProgressIndicator()); if (_error != null) { return buildCopyableErrorPanel( context: context, message: _error!, onRetry: _load, title: 'Kunde inte läsa admin pantry', ); } final filtered = _filtered; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Baslager', style: theme.textTheme.titleMedium), const SizedBox(height: 8), Text( 'Här ser du användarnas pantryposter. Du kan lägga till, ändra, flytta till inventarie och sätta/ändra kategori via produktval.', style: theme.textTheme.bodyMedium, ), const SizedBox(height: 8), const Wrap( spacing: 8, runSpacing: 8, children: [ Chip(label: Text('User-scope')), Chip(label: Text('Kategorier')), Chip(label: Text('Ändra/Lägg till')), Chip(label: Text('Flytta till inventarie')), Chip(label: Text('Ta bort')), ], ), ], ), ), ), const SizedBox(height: 12), Card( child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Expanded( child: DropdownButtonFormField( initialValue: _selectedUserId, decoration: const InputDecoration(labelText: 'Filtrera användare'), items: [ const DropdownMenuItem( value: null, child: Text('Alla användare'), ), ..._users.map( (user) => DropdownMenuItem( value: user.id, child: Text('${user.displayName} (${user.username})'), ), ), ], onChanged: (value) { setState(() => _selectedUserId = value); _load(); }, ), ), const SizedBox(width: 8), Expanded( child: TextField( decoration: const InputDecoration( prefixIcon: Icon(Icons.search), hintText: 'Sök produkt, kategori, användare eller plats', ), onChanged: (value) => setState(() => _search = value), ), ), const SizedBox(width: 8), OutlinedButton.icon( onPressed: _load, icon: const Icon(Icons.refresh), label: const Text('Uppdatera'), ), const SizedBox(width: 8), FilledButton.icon( onPressed: _addItem, icon: const Icon(Icons.add), label: const Text('Lägg till'), ), ], ), ), ), const SizedBox(height: 12), Text('Visar ${filtered.length} av ${_items.length} baslager-poster'), const SizedBox(height: 8), Expanded( child: filtered.isEmpty ? Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Baslager', style: theme.textTheme.titleMedium), const SizedBox(height: 8), Text( 'Här ser du användarnas pantryposter. Flytta dem tillbaka till inventarie eller ta bort poster som inte längre ska ligga kvar.', style: theme.textTheme.bodyMedium, ), const SizedBox(height: 8), Text( 'Inga pantry-poster hittades.', style: theme.textTheme.bodyMedium, ), ], ), ), ) : ListView.separated( itemCount: filtered.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, index) { final item = filtered[index]; return ListTile( title: Text(item.displayName), subtitle: Text( '${item.username} (${item.userEmail})' '${item.location == null || item.location!.trim().isEmpty ? '' : ' · ${item.location}'}' '${item.categoryPath == null || item.categoryPath!.trim().isEmpty ? '' : ' · ${item.categoryPath}'}', ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( tooltip: 'Ändra', icon: const Icon(Icons.edit_outlined), onPressed: () => _editItem(item), ), 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: () async { try { await ref.read(adminRepositoryProvider).removeAdminPantryItem(item.id); if (!mounted) return; await _load(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)), ); } }, ), ], ), ); }, ), ), ], ); } } class _PantryFormValues { final int? ownerUserId; final int productId; final String? location; const _PantryFormValues({ this.ownerUserId, required this.productId, this.location, }); } class _PantryFormDialog extends StatefulWidget { final List users; final List products; final List categories; final AdminPantryItem? initial; final int? initialOwnerUserId; const _PantryFormDialog({ required this.users, required this.products, required this.categories, this.initial, this.initialOwnerUserId, }); @override State<_PantryFormDialog> createState() => _PantryFormDialogState(); } class _PantryFormDialogState extends State<_PantryFormDialog> { late final TextEditingController _locationController; late final TextEditingController _categorySearchController; late List _categoryOptions; int? _ownerUserId; int? _productId; int? _categoryId; String? _categoryPath; String? _productErrorText; @override void initState() { super.initState(); final initial = widget.initial; _ownerUserId = initial?.userId ?? widget.initialOwnerUserId; _productId = initial?.productId; final initialProduct = _productById(_productId); _categoryId = initialProduct?.categoryId; _categoryPath = initialProduct?.categoryPath; _locationController = TextEditingController(text: initial?.location ?? ''); _categorySearchController = TextEditingController(text: _categoryPath ?? ''); _categoryOptions = _flattenCategoryOptions(widget.categories); } @override void dispose() { _locationController.dispose(); _categorySearchController.dispose(); super.dispose(); } void _applyCategorySelection(int id, String path) { setState(() { _categoryId = id; _categoryPath = path; _categorySearchController.text = path; if (_productId != null) { final current = _productById(_productId); if (current?.categoryId != _categoryId) { _productId = null; } } }); } AdminProduct? _productById(int? id) { if (id == null) return null; for (final product in widget.products) { if (product.id == id) return product; } return null; } List _productOptions() { final source = _categoryId == null ? widget.products : widget.products.where((p) => p.categoryId == _categoryId).toList(); final sorted = [...source] ..sort((a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase())); return sorted .map((p) => (id: p.id, name: p.displayName, categoryId: p.categoryId)) .toList(); } List _flattenCategoryOptions( List nodes, [ List parents = const [], ]) { final result = []; for (final node in nodes) { final pathParts = [...parents, node.name]; final path = pathParts.join(' > '); result.add((value: node.id.toString(), label: path)); result.addAll(_flattenCategoryOptions(node.children, pathParts)); } return result; } Future _pickCategory() async { final selected = await CategoryThenProductPicker.showCategorySheet( context, categoryTree: widget.categories, preselectedCategoryId: _categoryId, ); if (selected == null || !mounted) return; _applyCategorySelection(selected.id, selected.path); } @override Widget build(BuildContext context) { return AlertDialog( title: Text(widget.initial == null ? 'Lägg till baslager-post' : 'Ändra baslager-post'), content: SizedBox( width: 460, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.initial == null) ...[ DropdownButtonFormField( initialValue: _ownerUserId, items: widget.users .map((u) => DropdownMenuItem( value: u.id, child: Text( '${u.displayName} (${u.username})', overflow: TextOverflow.ellipsis, ), )) .toList(), onChanged: (value) => setState(() => _ownerUserId = value), decoration: const InputDecoration(labelText: 'Ägare (användare)'), ), const SizedBox(height: 12), ] else ...[ Text( 'Ägare: ${widget.initial!.username} (${widget.initial!.userEmail})', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 12), ], SearchableCategoryField( options: _categoryOptions, value: _categoryId?.toString(), label: 'Kategori (sökbar)', onChanged: (value) { if (value == null) return; final label = _categoryOptions .firstWhere((option) => option.value == value) .label; _applyCategorySelection(int.parse(value), label); }, ), const SizedBox(height: 12), Row( children: [ OutlinedButton.icon( onPressed: _pickCategory, icon: const Icon(Icons.category_outlined), label: const Text('Välj kategori'), ), const SizedBox(width: 8), OutlinedButton.icon( onPressed: () { setState(() { _categoryId = null; _categoryPath = null; }); }, icon: const Icon(Icons.clear), label: const Text('Rensa kategori'), ), ], ), const SizedBox(height: 12), ProductPickerField( products: _productOptions(), value: _productId, label: 'Produkt', errorText: _productErrorText, onChanged: (value) { setState(() { _productId = value; _productErrorText = null; }); }, ), const SizedBox(height: 12), TextFormField( controller: _locationController, decoration: const InputDecoration(labelText: 'Plats (valfritt)'), ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Avbryt'), ), FilledButton( onPressed: () { if (_productId == null) { setState(() => _productErrorText = 'Välj en produkt'); return; } if (widget.initial == null && _ownerUserId == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Välj användare')), ); return; } Navigator.of(context).pop( _PantryFormValues( ownerUserId: _ownerUserId, productId: _productId!, location: _locationController.text.trim().isEmpty ? null : _locationController.text.trim(), ), ); }, child: const Text('Spara'), ), ], ); } }