feat: implement update functionality for receipt aliases and add corresponding tests
This commit is contained in:
@@ -40,6 +40,7 @@ class ReceiptImportApiPaths {
|
||||
|
||||
class ReceiptAliasApiPaths {
|
||||
static const list = '/receipt-aliases';
|
||||
static String update(int id) => '/receipt-aliases/$id';
|
||||
static String remove(int id) => '/receipt-aliases/$id';
|
||||
}
|
||||
|
||||
|
||||
@@ -420,6 +420,19 @@ class AdminRepository {
|
||||
'isGlobal': isGlobal,
|
||||
});
|
||||
|
||||
Future<void> updateReceiptAlias(
|
||||
int id, {
|
||||
String? receiptName,
|
||||
int? productId,
|
||||
}) {
|
||||
final body = <String, dynamic>{
|
||||
if (receiptName != null) 'receiptName': receiptName,
|
||||
if (productId != null) 'productId': productId,
|
||||
};
|
||||
|
||||
return _patchVoid(ReceiptAliasApiPaths.update(id), body);
|
||||
}
|
||||
|
||||
Future<void> removeReceiptAlias(int id) =>
|
||||
_deleteVoid(ReceiptAliasApiPaths.remove(id));
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ class _AdminAliasesPanelState extends ConsumerState<AdminAliasesPanel> {
|
||||
|
||||
final TextEditingController _aliasController = TextEditingController();
|
||||
int? _selectedProductId;
|
||||
int? _editingAliasId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -79,18 +80,32 @@ class _AdminAliasesPanelState extends ConsumerState<AdminAliasesPanel> {
|
||||
|
||||
setState(() => _isSaving = true);
|
||||
try {
|
||||
await ref.read(adminRepositoryProvider).upsertReceiptAlias(
|
||||
receiptName: rawAlias,
|
||||
productId: productId,
|
||||
isGlobal: true,
|
||||
);
|
||||
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);
|
||||
setState(() {
|
||||
_selectedProductId = null;
|
||||
_editingAliasId = null;
|
||||
});
|
||||
await _load();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Alias sparad.')),
|
||||
SnackBar(content: Text(isEditing ? 'Alias uppdaterat.' : 'Alias sparad.')),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
@@ -102,6 +117,29 @@ class _AdminAliasesPanelState extends ConsumerState<AdminAliasesPanel> {
|
||||
}
|
||||
}
|
||||
|
||||
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<void> _removeAlias(ReceiptAlias alias) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
@@ -202,11 +240,23 @@ class _AdminAliasesPanelState extends ConsumerState<AdminAliasesPanel> {
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: IconButton(
|
||||
onPressed: () => _removeAlias(alias),
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
tooltip: 'Ta bort alias',
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -256,6 +306,7 @@ class _AdminAliasesPanelState extends ConsumerState<AdminAliasesPanel> {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<int>(
|
||||
key: ValueKey<int?>(_selectedProductId),
|
||||
initialValue: _selectedProductId,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Produkt',
|
||||
@@ -275,6 +326,13 @@ class _AdminAliasesPanelState extends ConsumerState<AdminAliasesPanel> {
|
||||
),
|
||||
),
|
||||
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
|
||||
@@ -283,8 +341,8 @@ class _AdminAliasesPanelState extends ConsumerState<AdminAliasesPanel> {
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.save_outlined),
|
||||
label: const Text('Spara'),
|
||||
: Icon(_editingAliasId != null ? Icons.edit_outlined : Icons.save_outlined),
|
||||
label: Text(_editingAliasId != null ? 'Uppdatera' : 'Spara'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -498,6 +498,74 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _editAliasForItem(int index) async {
|
||||
final items = _items;
|
||||
if (items == null || index < 0 || index >= items.length) return;
|
||||
|
||||
final item = items[index];
|
||||
final edit = _edits[index];
|
||||
final productId = edit?.productId;
|
||||
final initialAlias = item.rawName.trim();
|
||||
|
||||
if (productId == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Välj produkt för raden innan du redigerar alias.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final controller = TextEditingController(text: initialAlias);
|
||||
final aliasName = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Spara alias för raden'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Aliasnamn',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onSubmitted: (value) => Navigator.pop(ctx, value.trim()),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('Avbryt'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, controller.text.trim()),
|
||||
child: const Text('Spara'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
controller.dispose();
|
||||
|
||||
if (!mounted || aliasName == null) return;
|
||||
if (aliasName.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Aliasnamn kan inte vara tomt.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ref.read(adminRepositoryProvider).upsertReceiptAlias(
|
||||
receiptName: aliasName,
|
||||
productId: productId,
|
||||
isGlobal: false,
|
||||
);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Alias sparat.')),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
showGlobalErrorDialog(context, 'Kunde inte uppdatera alias: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addSelected() async {
|
||||
final items = _items;
|
||||
if (items == null) return;
|
||||
@@ -844,6 +912,7 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
|
||||
i,
|
||||
initialEntryMode: ImportProductEntryMode.create,
|
||||
),
|
||||
onAliasEditRequested: () => _editAliasForItem(i),
|
||||
onDeleteRequested: () => _deleteItem(i),
|
||||
matchedViaBadgeBuilder: _buildMatchedViaBadge,
|
||||
);
|
||||
@@ -877,6 +946,7 @@ class _ReceiptImportResultRow extends ConsumerWidget {
|
||||
final VoidCallback onEditRequested;
|
||||
final VoidCallback onSelectExistingRequested;
|
||||
final VoidCallback onCreateRequested;
|
||||
final VoidCallback onAliasEditRequested;
|
||||
final VoidCallback onDeleteRequested;
|
||||
final Widget Function(ParsedReceiptItem item, ThemeData theme)
|
||||
matchedViaBadgeBuilder;
|
||||
@@ -891,6 +961,7 @@ class _ReceiptImportResultRow extends ConsumerWidget {
|
||||
required this.onEditRequested,
|
||||
required this.onSelectExistingRequested,
|
||||
required this.onCreateRequested,
|
||||
required this.onAliasEditRequested,
|
||||
required this.onDeleteRequested,
|
||||
required this.matchedViaBadgeBuilder,
|
||||
});
|
||||
@@ -1088,7 +1159,7 @@ class _ReceiptImportResultRow extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 80,
|
||||
width: 120,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -1101,6 +1172,14 @@ class _ReceiptImportResultRow extends ConsumerWidget {
|
||||
: (isSuggested ? Colors.orange : theme.colorScheme.tertiary),
|
||||
size: 20,
|
||||
),
|
||||
if (hasProduct)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.drive_file_rename_outline, size: 18),
|
||||
onPressed: onAliasEditRequested,
|
||||
tooltip: 'Spara alias',
|
||||
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
||||
padding: const EdgeInsets.all(4),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete_outline, size: 18, color: theme.colorScheme.error),
|
||||
onPressed: onDeleteRequested,
|
||||
|
||||
@@ -93,6 +93,68 @@ class _UserAliasesScreenState extends ConsumerState<UserAliasesScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _editAlias(ReceiptAlias alias) async {
|
||||
if (!alias.isPrivate) return;
|
||||
|
||||
final controller = TextEditingController(text: alias.receiptName);
|
||||
final newAliasName = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Redigera alias'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Kvittonamn (alias)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onSubmitted: (value) => Navigator.pop(ctx, value.trim()),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('Avbryt'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, controller.text.trim()),
|
||||
child: const Text('Spara'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
controller.dispose();
|
||||
|
||||
if (!mounted || newAliasName == null) return;
|
||||
final normalizedNew = newAliasName.trim();
|
||||
if (normalizedNew.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Aliasnamn kan inte vara tomt.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (normalizedNew == alias.receiptName.trim()) return;
|
||||
|
||||
try {
|
||||
final repo = ref.read(adminRepositoryProvider);
|
||||
await repo.updateReceiptAlias(
|
||||
alias.id,
|
||||
receiptName: normalizedNew,
|
||||
productId: alias.productId,
|
||||
);
|
||||
await _load();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Alias uppdaterat.')),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Kunde inte uppdatera alias: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -242,11 +304,21 @@ class _UserAliasesScreenState extends ConsumerState<UserAliasesScreen> {
|
||||
],
|
||||
),
|
||||
trailing: alias.isPrivate
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
tooltip: 'Ta bort alias',
|
||||
color: theme.colorScheme.error,
|
||||
onPressed: () => _delete(alias),
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
tooltip: 'Redigera alias',
|
||||
onPressed: () => _editAlias(alias),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
tooltip: 'Ta bort alias',
|
||||
color: theme.colorScheme.error,
|
||||
onPressed: () => _delete(alias),
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user