505c89c731
Test Suite / test (24.15.0) (push) Has been cancelled
- Updated error handling in AdminAliasesPanel, AdminDatabasePanel, AdminPendingProductsPanel, and AdminProductsPanel to ensure consistent snackbar display without extra parentheses. - Refined error handling in ConsumeInventoryScreen, CreateInventoryScreen, InventoryDetailScreen, InventoryEditScreen, and SwipeableInventoryTile to maintain consistent snackbar formatting. - Improved error handling in MealPlanScreen, PantryScreen, ProfileScreen, and RecipeDetailScreen to ensure proper user feedback on errors.
263 lines
8.1 KiB
Dart
263 lines
8.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../../core/api/api_error_mapper.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<AdminAliasesPanel> createState() => _AdminAliasesPanelState();
|
|
}
|
|
|
|
class _AdminAliasesPanelState extends ConsumerState<AdminAliasesPanel> {
|
|
bool _isLoading = true;
|
|
bool _isSaving = false;
|
|
String? _error;
|
|
String _search = '';
|
|
List<ReceiptAlias> _aliases = [];
|
|
List<AdminProduct> _products = [];
|
|
|
|
final TextEditingController _aliasController = TextEditingController();
|
|
int? _selectedProductId;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_load();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_aliasController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
setState(() {
|
|
_isLoading = true;
|
|
_error = null;
|
|
});
|
|
try {
|
|
final results = await Future.wait<dynamic>([
|
|
ref.read(adminRepositoryProvider).listReceiptAliases(),
|
|
ref.read(adminRepositoryProvider).listProducts(),
|
|
]);
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_aliases = (results[0] as List<ReceiptAlias>)
|
|
..sort((a, b) => a.receiptName.compareTo(b.receiptName));
|
|
_products = (results[1] as List<AdminProduct>)
|
|
..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<void> _upsertAlias() async {
|
|
final rawAlias = _aliasController.text.trim().toLowerCase();
|
|
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 {
|
|
await ref.read(adminRepositoryProvider).upsertReceiptAlias(
|
|
receiptName: rawAlias,
|
|
productId: productId,
|
|
isGlobal: true,
|
|
);
|
|
if (!mounted) return;
|
|
_aliasController.clear();
|
|
setState(() => _selectedProductId = null);
|
|
await _load();
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Alias sparad.')),
|
|
);
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)),
|
|
);
|
|
} finally {
|
|
if (mounted) setState(() => _isSaving = false);
|
|
}
|
|
}
|
|
|
|
Future<void> _removeAlias(ReceiptAlias alias) async {
|
|
final confirmed = await showDialog<bool>(
|
|
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 Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(_error!, style: TextStyle(color: theme.colorScheme.error)),
|
|
const SizedBox(height: 16),
|
|
FilledButton(onPressed: _load, child: const Text('Försök igen')),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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 content = Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
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: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _aliasController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Kvittonamn (alias)',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: DropdownButtonFormField<int>(
|
|
value: _selectedProductId,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Produkt',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
items: _products
|
|
.map(
|
|
(product) => DropdownMenuItem<int>(
|
|
value: product.id,
|
|
child: Text(product.displayName),
|
|
),
|
|
)
|
|
.toList(),
|
|
onChanged: _isSaving
|
|
? null
|
|
: (value) => setState(() => _selectedProductId = value),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
FilledButton.icon(
|
|
onPressed: _isSaving ? null : _upsertAlias,
|
|
icon: _isSaving
|
|
? const SizedBox(
|
|
width: 14,
|
|
height: 14,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Icon(Icons.save_outlined),
|
|
label: const Text('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)
|
|
const Text('Inga alias hittades.')
|
|
else
|
|
...filteredAliases.map(
|
|
(alias) => Card(
|
|
child: ListTile(
|
|
title: Text(alias.receiptName),
|
|
subtitle: Text('Produkt: ${alias.displayProductName}'),
|
|
trailing: IconButton(
|
|
onPressed: () => _removeAlias(alias),
|
|
icon: const Icon(Icons.delete_outline),
|
|
tooltip: 'Ta bort alias',
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
|
|
if (!widget.embedded) {
|
|
return ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [content],
|
|
);
|
|
}
|
|
|
|
return content;
|
|
}
|
|
}
|
|
|