import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/api/api_error_mapper.dart'; import 'admin_form_shared.dart'; import '../data/admin_repository.dart'; import '../domain/admin_product.dart'; import '../domain/receipt_alias.dart'; class AdminAliasesPanel extends ConsumerStatefulWidget { final bool embedded; const AdminAliasesPanel({super.key, this.embedded = false}); @override ConsumerState createState() => _AdminAliasesPanelState(); } class _AdminAliasesPanelState extends ConsumerState { bool _isLoading = true; bool _isSaving = false; String? _error; String _search = ''; List _aliases = []; List _products = []; final TextEditingController _aliasController = TextEditingController(); int? _selectedProductId; int? _editingAliasId; @override void initState() { super.initState(); _load(); } @override void dispose() { _aliasController.dispose(); super.dispose(); } Future _load() async { setState(() { _isLoading = true; _error = null; }); try { final results = await Future.wait([ ref.read(adminRepositoryProvider).listReceiptAliases(), ref.read(adminRepositoryProvider).listGlobalProducts(), ]); if (!mounted) return; setState(() { _aliases = (results[0] as List) ..sort((a, b) => a.receiptName.compareTo(b.receiptName)); _products = (results[1] as List) ..sort( (a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()), ); }); } catch (e) { if (!mounted) return; setState(() => _error = mapErrorToUserMessage(e, context)); } finally { if (mounted) setState(() => _isLoading = false); } } Future _upsertAlias() async { final rawAlias = _aliasController.text.trim(); final productId = _selectedProductId; if (rawAlias.isEmpty || productId == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Ange alias och välj produkt.')), ); return; } setState(() => _isSaving = true); try { final repo = ref.read(adminRepositoryProvider); final isEditing = _editingAliasId != null; if (isEditing) { await repo.updateReceiptAlias( _editingAliasId!, receiptName: rawAlias, productId: productId, ); } else { await repo.upsertReceiptAlias( receiptName: rawAlias, productId: productId, isGlobal: true, ); } if (!mounted) return; _aliasController.clear(); setState(() { _selectedProductId = null; _editingAliasId = null; }); await _load(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(isEditing ? 'Alias uppdaterat.' : 'Alias sparad.')), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)), ); } finally { if (mounted) setState(() => _isSaving = false); } } void _startEditAlias(ReceiptAlias alias) { if (!alias.isGlobal) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Privata alias redigeras av respektive användare.')), ); return; } setState(() { _editingAliasId = alias.id; _aliasController.text = alias.receiptName; _selectedProductId = alias.productId; }); } void _cancelEditAlias() { setState(() { _editingAliasId = null; _aliasController.clear(); _selectedProductId = null; }); } Future _removeAlias(ReceiptAlias alias) async { final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('Ta bort alias'), content: Text('Ta bort alias "${alias.receiptName}"?'), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: const Text('Avbryt'), ), FilledButton( onPressed: () => Navigator.pop(dialogContext, true), child: const Text('Ta bort'), ), ], ), ); if (confirmed != true || !mounted) return; try { await ref.read(adminRepositoryProvider).removeReceiptAlias(alias.id); if (!mounted) return; await _load(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Alias borttaget.')), ); } 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 alias', ); } final filteredAliases = _aliases.where((alias) { final query = _search.trim().toLowerCase(); if (query.isEmpty) return true; return alias.receiptName.contains(query) || alias.displayProductName.toLowerCase().contains(query); }).toList(); final productById = { for (final product in _products) product.id: product, }; Widget buildAliasCard(ReceiptAlias alias) { final product = productById[alias.productId]; final categoryPath = product?.categoryPath ?? 'okänd'; return Card( child: ListTile( leading: const Icon(Icons.link_outlined), title: Row( children: [ Expanded(child: Text(alias.receiptName, style: const TextStyle(fontWeight: FontWeight.w500))), buildCategoryPathChip(categoryPath), ], ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( '→ ${alias.displayProductName}', style: const TextStyle(fontWeight: FontWeight.w400), ), Text( 'Produkt-ID: ${alias.productId}', style: TextStyle( fontSize: 11, color: Theme.of(context).colorScheme.outline, ), ), const SizedBox(height: 4), Chip( visualDensity: VisualDensity.compact, label: Text(alias.isGlobal ? 'Global' : 'Privat'), ), ], ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( onPressed: () => _startEditAlias(alias), icon: const Icon(Icons.edit_outlined), tooltip: alias.isGlobal ? 'Redigera alias' : 'Privata alias redigeras av användaren', ), IconButton( onPressed: () => _removeAlias(alias), icon: const Icon(Icons.delete_outline), tooltip: 'Ta bort alias', color: Theme.of(context).colorScheme.error, ), ], ), ), ); } final content = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Alias', style: theme.textTheme.titleMedium), const SizedBox(height: 8), Text( 'Globala alias används som fallback i kvittoimporten. När samma kvittonamn upprepas kan rätt produkt matchas direkt.', style: theme.textTheme.bodyMedium, ), const SizedBox(height: 8), const Wrap( spacing: 8, runSpacing: 8, children: [ Chip(label: Text('Fallback')), Chip(label: Text('Global')), Chip(label: Text('Receipt import')), ], ), ], ), ), ), const SizedBox(height: 12), Row( children: [ Expanded( child: TextField( controller: _aliasController, decoration: const InputDecoration( labelText: 'Kvittonamn (alias)', border: OutlineInputBorder(), ), ), ), const SizedBox(width: 8), Expanded( child: DropdownButtonFormField( key: ValueKey(_selectedProductId), initialValue: _selectedProductId, decoration: const InputDecoration( labelText: 'Produkt', border: OutlineInputBorder(), ), items: _products .map( (product) => DropdownMenuItem( value: product.id, child: Text(product.displayName), ), ) .toList(), onChanged: _isSaving ? null : (value) => setState(() => _selectedProductId = value), ), ), const SizedBox(width: 8), if (_editingAliasId != null) ...[ OutlinedButton( onPressed: _isSaving ? null : _cancelEditAlias, child: const Text('Avbryt'), ), const SizedBox(width: 8), ], FilledButton.icon( onPressed: _isSaving ? null : _upsertAlias, icon: _isSaving ? const SizedBox( width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2), ) : Icon(_editingAliasId != null ? Icons.edit_outlined : Icons.save_outlined), label: Text(_editingAliasId != null ? 'Uppdatera' : 'Spara'), ), ], ), const SizedBox(height: 12), TextField( decoration: const InputDecoration( labelText: 'Sök alias', prefixIcon: Icon(Icons.search), border: OutlineInputBorder(), ), onChanged: (value) => setState(() => _search = value), ), const SizedBox(height: 12), if (filteredAliases.isEmpty) Card( child: Padding( padding: const EdgeInsets.all(16), child: Text( 'Inga alias hittades.', style: theme.textTheme.bodyMedium, ), ), ), ], ); if (!widget.embedded) { if (filteredAliases.isEmpty) { return ListView( padding: const EdgeInsets.all(16), children: [content], ); } return ListView( padding: const EdgeInsets.all(16), children: [ content, const SizedBox(height: 8), ...filteredAliases.map(buildAliasCard), ], ); } if (filteredAliases.isEmpty) return content; return SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ content, const SizedBox(height: 8), ...filteredAliases.map(buildAliasCard), ], ), ); } }