import 'package:flutter/material.dart'; import '../../../core/api/api_exception.dart'; import '../../../core/ui/category_then_product_picker.dart'; import '../../../core/ui/product_picker_field.dart'; import '../../../core/utils/global_error_handler.dart'; import '../../admin/domain/admin_category_node.dart'; import '../data/receipt_import_session.dart'; import '../domain/parsed_receipt_item.dart'; import '../utils/receipt_import_utils.dart'; enum ImportProductEntryMode { existing, create } typedef _Destination = ImportDestination; class EditDialog extends StatefulWidget { final ParsedReceiptItem item; final ItemEdit current; final List products; final List categoryTree; final Future Function(String name, int? categoryId)? onCreate; final ImportProductEntryMode? initialEntryMode; final bool canLearnGlobalAlias; const EditDialog({ super.key, required this.item, required this.current, required this.products, required this.categoryTree, this.onCreate, this.initialEntryMode, this.canLearnGlobalAlias = false, }); @override State createState() => _EditDialogState(); } class _EditDialogState extends State { late final TextEditingController _quantityCtrl; late final TextEditingController _unitCtrl; late final TextEditingController _packageCountCtrl; late final TextEditingController _newProductNameCtrl; int? _productId; String? _productName; int? _productCategoryId; String? _productCategoryPath; CategorySelectionSource? _productCategorySource; int? _newCategoryId; String? _newCategoryPath; CategorySelectionSource? _newCategorySource; _Destination _destination = _Destination.inventory; ImportProductEntryMode _entryMode = ImportProductEntryMode.existing; bool _isCreatingProduct = false; bool _learnAlias = false; bool _learnAliasGlobally = false; // Lokal lista — utökas om nya produkter skapas under dialogen late List _localProducts; late CategoryLookup _lookup; @override void initState() { super.initState(); _lookup = CategoryLookup.fromTree(widget.categoryTree); _localProducts = List.of(widget.products); _productId = widget.current.productId; _productName = widget.current.productName == null ? null : normalizeProductName(widget.current.productName!); _learnAlias = widget.current.learnAlias; _learnAliasGlobally = widget.current.learnAliasGlobally; _destination = widget.current.destination; _entryMode = widget.initialEntryMode ?? (_productId == null ? ImportProductEntryMode.create : ImportProductEntryMode.existing); _productCategoryId = widget.current.categoryId ?? _categoryIdForProduct(_productId); _productCategoryPath = widget.current.categoryPath ?? _lookup.pathFor(_productCategoryId); _productCategorySource = widget.current.categorySource; _newCategoryId = widget.current.categoryId ?? widget.item.categorySuggestionId; _newCategoryPath = widget.current.categoryPath ?? widget.item.categorySuggestionPath; _newCategorySource = widget.current.categorySource; final inferred = inferPackageFields( rawName: widget.item.rawName, quantity: widget.current.quantity ?? widget.item.quantity, unit: widget.current.unit ?? widget.item.unit, ); _quantityCtrl = TextEditingController( text: (widget.current.packQuantity ?? inferred.packQuantity)?.toString() ?? '', ); _unitCtrl = TextEditingController( text: widget.current.packUnit ?? inferred.packUnit ?? '', ); _packageCountCtrl = TextEditingController( text: (widget.current.packageCount ?? inferred.packageCount).toString(), ); _newProductNameCtrl = TextEditingController( text: normalizeProductName(widget.current.productName ?? widget.item.rawName), ); } @override void dispose() { _quantityCtrl.dispose(); _unitCtrl.dispose(); _packageCountCtrl.dispose(); _newProductNameCtrl.dispose(); super.dispose(); } // ── Hjälpmetoder ────────────────────────────────────────────────────────── int? _categoryIdForProduct(int? productId) { if (productId == null) return null; return _localProducts .cast() .firstWhere((p) => p?.id == productId, orElse: () => null) ?.categoryId; } // ── Kategoripicker ───────────────────────────────────────────────────────── Future _openCreateCategoryPicker({int? preselectedCategoryId}) async { final selected = await CategoryThenProductPicker.showCategorySheet( context, categoryTree: widget.categoryTree, preselectedCategoryId: preselectedCategoryId ?? _newCategoryId ?? widget.item.categorySuggestionId, ); if (selected == null || !mounted) return; setState(() { _newCategoryId = selected.id; _newCategoryPath = selected.path; _newCategorySource = CategorySelectionSource.manual; }); } Future _openExistingCategoryPicker({int? preselectedCategoryId}) async { Future Function(String, int)? onCreateWrapped; if (widget.onCreate != null) { onCreateWrapped = (name, categoryId) async { final newProduct = await widget.onCreate!(name, categoryId); if (newProduct != null && mounted) { setState(() => _localProducts = [..._localProducts, newProduct]); } return newProduct; }; } final id = await CategoryThenProductPicker.show( context, categoryTree: widget.categoryTree, products: _localProducts, currentProductId: _productId, preselectedCategoryId: preselectedCategoryId, initialQuery: widget.item.rawName, onCreate: onCreateWrapped, ); if (id == null || !mounted) return; setState(() { _productId = id; final selectedProduct = _localProducts .cast() .firstWhere((p) => p?.id == id, orElse: () => null); _productName = selectedProduct?.name == null ? null : normalizeProductName(selectedProduct!.name); _productCategoryId = _categoryIdForProduct(id); _productCategoryPath = _lookup.pathFor(_productCategoryId); _productCategorySource = CategorySelectionSource.manual; }); } /// Applicerar AI-förslag och öppnar kategoriträdet för bekräftelse. void _applyAiSuggestion() { int? preselectedCategoryId = widget.item.categorySuggestionId; final suggestedId = widget.item.suggestedProductId; if (suggestedId != null) { setState(() { _productId = suggestedId; _productName = widget.item.suggestedProductName == null ? null : normalizeProductName(widget.item.suggestedProductName!); _productCategoryId = _categoryIdForProduct(suggestedId) ?? widget.item.categorySuggestionId; _productCategoryPath = _lookup.pathFor(_productCategoryId) ?? widget.item.categorySuggestionPath; _productCategorySource = CategorySelectionSource.ai; }); preselectedCategoryId = _productCategoryId; } _openExistingCategoryPicker(preselectedCategoryId: preselectedCategoryId); } // ── Spara ────────────────────────────────────────────────────────────────── bool get _canConfirm { if (_isCreatingProduct) return false; if (_entryMode == ImportProductEntryMode.create) return true; return _productId != null; } Future _confirm() async { final originalUnit = widget.current.unit ?? widget.item.unit; final newUnit = _unitCtrl.text.trim().isEmpty ? originalUnit : _unitCtrl.text.trim(); await _confirmUnitChange(originalUnit!, newUnit!); if (_entryMode == ImportProductEntryMode.create) { final trimmedName = _newProductNameCtrl.text.trim(); if (trimmedName.isEmpty) { showGlobalErrorDialog(context, 'Ange ett produktnamn först.'); return; } if (widget.onCreate == null) { showGlobalErrorDialog(context, 'Produktskapande är inte tillgängligt i den här vyn.'); return; } setState(() => _isCreatingProduct = true); try { final newProduct = await widget.onCreate!(trimmedName, _newCategoryId); if (newProduct == null || !mounted) { if (mounted) { showGlobalErrorDialog(context, 'Kunde inte skapa produkten. Försök igen.'); } return; } if (!_localProducts.any((p) => p.id == newProduct.id)) { _localProducts = [..._localProducts, newProduct]; } _productId = newProduct.id; _productName = newProduct.name; _productCategoryId = _newCategoryId; _productCategoryPath = _newCategoryPath; _productCategorySource = _newCategorySource ?? CategorySelectionSource.manual; } on ApiException catch (e) { if (mounted) { showGlobalErrorDialog( context, e.message.trim().isEmpty ? 'Kunde inte skapa produkten. Försök igen.' : e.message, ); } return; } catch (_) { if (mounted) { showGlobalErrorDialog(context, 'Kunde inte skapa produkten. Försök igen.'); } return; } finally { if (mounted) setState(() => _isCreatingProduct = false); } if (!mounted || _productId == null) return; } final packQuantity = double.tryParse(_quantityCtrl.text.replaceAll(',', '.')); final packageCount = double.tryParse(_packageCountCtrl.text.replaceAll(',', '.')) ?? 1.0; final packUnit = _unitCtrl.text.trim().isEmpty ? (widget.current.packUnit ?? widget.current.unit ?? widget.item.unit) : _unitCtrl.text.trim(); final totalQuantity = packQuantity != null ? packQuantity * packageCount : widget.item.quantity; Navigator.pop( context, ItemEdit( productId: _productId, productName: _productName, learnAlias: _learnAlias, learnAliasGlobally: _learnAlias && widget.canLearnGlobalAlias && _learnAliasGlobally, categoryId: _productCategoryId, categoryPath: _productCategoryPath, categorySource: _productCategorySource, quantity: totalQuantity, unit: packUnit, packQuantity: packQuantity, packUnit: packUnit, packageCount: packageCount, destination: _destination, ), ); } Future _confirmUnitChange(String originalUnit, String newUnit) async { if (originalUnit == newUnit) return; return showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: const Text('Bekräfta enhetsändring'), content: Text( 'Du försöker ändra enheten från "$originalUnit" till "$newUnit". Vill du fortsätta med denna ändring?', ), actions: [ TextButton( child: const Text('Avbryt'), onPressed: () { _unitCtrl.text = originalUnit; Navigator.of(context).pop(); }, ), TextButton( child: const Text('Bekräfta'), onPressed: () { Navigator.of(context).pop(); }, ), ], ); }, ); } // ── Build ────────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { final theme = Theme.of(context); final item = widget.item; final aiLabel = _resolveAiLabel(item); final suggestedProductLabel = _resolveSuggestedProductLabel(item); final currentPackQuantity = double.tryParse(_quantityCtrl.text.replaceAll(',', '.')); final currentPackageCount = double.tryParse(_packageCountCtrl.text.replaceAll(',', '.')) ?? 1.0; final currentUnit = _unitCtrl.text.trim().isEmpty ? (widget.current.packUnit ?? widget.current.unit ?? item.unit) : _unitCtrl.text.trim(); final totalPreview = currentPackQuantity == null ? null : currentPackQuantity * currentPackageCount; return AlertDialog( title: Text( normalizeProductName(item.rawName), maxLines: 2, overflow: TextOverflow.ellipsis, ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildDestinationPicker(theme), const SizedBox(height: 12), _buildEntryModePicker(theme), const SizedBox(height: 12), if (_entryMode == ImportProductEntryMode.existing) _buildExistingProductSection(theme, item, aiLabel, suggestedProductLabel) else _buildCreateProductSection(theme, aiLabel), const SizedBox(height: 12), _buildAliasSection(theme, item), const SizedBox(height: 12), if (_destination == _Destination.inventory) _buildQuantitySection(theme, totalPreview, currentUnit) else Text( 'Baslager sparar bara produkt — ingen mängd eller enhet.', style: theme.textTheme.bodySmall ?.copyWith(color: theme.colorScheme.onSurfaceVariant), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Avbryt'), ), FilledButton( onPressed: _canConfirm ? _confirm : null, child: _isCreatingProduct ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : Text( _entryMode == ImportProductEntryMode.create ? 'Skapa och välj' : 'OK', ), ), ], ); } // ── Byggare för delsektioner ─────────────────────────────────────────────── Widget _buildDestinationPicker(ThemeData theme) => SegmentedButton<_Destination>( segments: const [ ButtonSegment( value: ImportDestination.inventory, icon: Icon(Icons.kitchen_outlined, size: 16), label: Text('Inventarie'), ), ButtonSegment( value: ImportDestination.pantry, icon: Icon(Icons.inventory_2_outlined, size: 16), label: Text('Baslager'), ), ], selected: {_destination}, onSelectionChanged: (s) => setState(() => _destination = s.first), style: const ButtonStyle(visualDensity: VisualDensity.compact), ); Widget _buildEntryModePicker(ThemeData theme) => SegmentedButton( segments: const [ ButtonSegment( value: ImportProductEntryMode.existing, icon: Icon(Icons.search, size: 16), label: Text('Befintlig'), ), ButtonSegment( value: ImportProductEntryMode.create, icon: Icon(Icons.add_box_outlined, size: 16), label: Text('Ny produkt'), ), ], selected: {_entryMode}, onSelectionChanged: (s) => setState(() => _entryMode = s.first), style: const ButtonStyle(visualDensity: VisualDensity.compact), ); Widget _buildAliasSection(ThemeData theme, ParsedReceiptItem item) { final alreadyAliasMatch = _entryMode == ImportProductEntryMode.existing && _productId != null && item.matchedVia == 'alias' && item.matchedProductId == _productId; if (alreadyAliasMatch) { return Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: theme.colorScheme.primaryContainer.withValues(alpha: 0.45), borderRadius: BorderRadius.circular(12), ), child: Text( 'Det här kvittonamnet matchades redan via alias. Ingen ny aliasinlärning behövs.', style: theme.textTheme.bodySmall, ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CheckboxListTile( contentPadding: EdgeInsets.zero, value: _learnAlias, onChanged: (value) => setState(() { _learnAlias = value ?? false; if (!_learnAlias) _learnAliasGlobally = false; }), title: const Text('Lär detta kvittonamn för framtiden'), subtitle: const Text( 'Sparar ett alias så att samma kvittonamn kan matchas direkt vid nästa import.', ), controlAffinity: ListTileControlAffinity.leading, ), if (widget.canLearnGlobalAlias && _learnAlias) Padding( padding: const EdgeInsets.only(left: 12), child: SegmentedButton( segments: const [ ButtonSegment( value: false, label: Text('Privat alias'), icon: Icon(Icons.lock_outline, size: 16), ), ButtonSegment( value: true, label: Text('Global fallback'), icon: Icon(Icons.public_outlined, size: 16), ), ], selected: {_learnAliasGlobally}, onSelectionChanged: (selection) => setState(() => _learnAliasGlobally = selection.first), style: const ButtonStyle(visualDensity: VisualDensity.compact), ), ), ], ); } Widget _buildExistingProductSection( ThemeData theme, ParsedReceiptItem item, String? aiLabel, String? suggestedProductLabel, ) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (suggestedProductLabel != null) ...[ Tooltip( message: 'Trolig matchning baserat på produktnamn i databasen', child: ActionChip( avatar: Icon(Icons.search, size: 14, color: Colors.blue.shade700), label: Text('Namnförslag: $suggestedProductLabel', style: Theme.of(context).textTheme.labelSmall), backgroundColor: Colors.blue.shade50, side: BorderSide(color: Colors.blue.shade300), visualDensity: VisualDensity.compact, onPressed: _applyAiSuggestion, ), ), const SizedBox(height: 8), ], Row( children: [ Expanded( child: ProductPickerField( products: _localProducts, value: _productId, label: 'Produkt', initialQuery: item.rawName, onChanged: (id) { setState(() { _productId = id; final selectedName = _localProducts .cast() .firstWhere((p) => p?.id == id, orElse: () => null) ?.name; _productName = selectedName == null ? null : normalizeProductName(selectedName); _productCategoryId = _categoryIdForProduct(id); _productCategoryPath = _lookup.pathFor(_productCategoryId); _productCategorySource = id == null ? null : CategorySelectionSource.manual; }); }, ), ), const SizedBox(width: 8), Tooltip( message: 'Välj via kategori', child: OutlinedButton( style: OutlinedButton.styleFrom( minimumSize: const Size(44, 56), padding: EdgeInsets.zero, ), onPressed: _openExistingCategoryPicker, child: const Icon(Icons.account_tree_outlined, size: 20), ), ), ], ), if (_productCategoryPath != null) ...[ const SizedBox(height: 8), ActionChip( avatar: Icon(Icons.account_tree_outlined, size: 14, color: Theme.of(context).colorScheme.primary), label: Text('Kategori: $_productCategoryPath', style: Theme.of(context).textTheme.labelSmall, overflow: TextOverflow.ellipsis), side: BorderSide(color: Theme.of(context).colorScheme.outlineVariant), visualDensity: VisualDensity.compact, onPressed: () => _openExistingCategoryPicker(preselectedCategoryId: _productCategoryId), ), ], if (aiLabel != null) ...[ const SizedBox(height: 8), ActionChip( avatar: Icon(Icons.auto_awesome, size: 14, color: Colors.green.shade700), label: Text('AI-kategori: $aiLabel', style: Theme.of(context).textTheme.labelSmall), backgroundColor: Colors.green.shade50, side: BorderSide(color: Colors.green.shade300), visualDensity: VisualDensity.compact, onPressed: () => _openExistingCategoryPicker( preselectedCategoryId: item.categorySuggestionId, ), ), ], ], ); } Widget _buildCreateProductSection(ThemeData theme, String? aiLabel) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( controller: _newProductNameCtrl, textCapitalization: TextCapitalization.sentences, decoration: const InputDecoration( labelText: 'Produktnamn', border: OutlineInputBorder(), ), onChanged: (_) => setState(() {}), ), const SizedBox(height: 8), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: _openCreateCategoryPicker, icon: const Icon(Icons.account_tree_outlined), label: Text( _newCategoryPath == null ? 'Välj kategori' : 'Kategori: $_newCategoryPath', overflow: TextOverflow.ellipsis, ), ), ), if (_newCategoryPath != null) ...[ const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Vald kategori', style: theme.textTheme.labelSmall ?.copyWith(color: theme.colorScheme.onSurfaceVariant), ), const SizedBox(height: 4), Text( _newCategoryPath!, style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), ), ], ), ), if (aiLabel != null) ...[ const SizedBox(height: 8), Wrap( children: [ ActionChip( avatar: Icon(Icons.auto_awesome, size: 14, color: Colors.green.shade700), label: Text('AI-förslag: $aiLabel', style: Theme.of(context).textTheme.labelSmall), backgroundColor: Colors.green.shade50, side: BorderSide(color: Colors.green.shade300), visualDensity: VisualDensity.compact, onPressed: widget.item.categorySuggestionId == null ? null : () => _openCreateCategoryPicker( preselectedCategoryId: widget.item.categorySuggestionId, ), ), ], ), ], ], ], ); } Widget _buildQuantitySection( ThemeData theme, double? totalPreview, String? currentUnit, ) { return Column( children: [ Row( children: [ Expanded( child: TextField( controller: _quantityCtrl, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration( labelText: 'Mängd per förpackning', border: OutlineInputBorder(), ), onChanged: (_) => setState(() {}), ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: _unitCtrl, decoration: const InputDecoration( labelText: 'Enhet', border: OutlineInputBorder(), ), onChanged: (_) => setState(() {}), ), ), ], ), const SizedBox(height: 8), TextField( controller: _packageCountCtrl, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration( labelText: 'Antal förpackningar', border: OutlineInputBorder(), ), onChanged: (_) => setState(() {}), ), if (totalPreview != null && currentUnit != null && currentUnit.isNotEmpty) ...[ const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.green.shade50, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.green.shade200), ), child: Text( 'Totalt: ${formatSwedishNumber(totalPreview)} $currentUnit ' '(mängd × antal förpackningar).', style: theme.textTheme.bodySmall ?.copyWith(color: Colors.green.shade800), ), ), ], ], ); } // ── Statiska hjälpare ────────────────────────────────────────────────────── static String? _resolveAiLabel(ParsedReceiptItem item) { final path = item.categorySuggestionPath; if (path != null && path.isNotEmpty) return path; final name = item.categorySuggestionName; if (name != null && name.isNotEmpty) return name; return null; } static String? _resolveSuggestedProductLabel(ParsedReceiptItem item) { final suggested = item.suggestedProductName; if (suggested != null && suggested.isNotEmpty) { return normalizeProductName(suggested); } final matched = item.matchedProductName; if (matched != null && matched.isNotEmpty) { return normalizeProductName(matched); } return null; } }