feat: implement alias strategy for receipt import with user-scoped and global fallback, enhance validation and normalization, and update UI components
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-09 23:41:42 +02:00
parent b342de906e
commit 65137b41fb
17 changed files with 388 additions and 67 deletions
@@ -19,6 +19,7 @@ class EditDialog extends StatefulWidget {
final List<AdminCategoryNode> categoryTree;
final Future<ProductOption?> Function(String name, int? categoryId)? onCreate;
final ImportProductEntryMode? initialEntryMode;
final bool canLearnGlobalAlias;
const EditDialog({
super.key,
@@ -28,6 +29,7 @@ class EditDialog extends StatefulWidget {
required this.categoryTree,
this.onCreate,
this.initialEntryMode,
this.canLearnGlobalAlias = false,
});
@override
@@ -53,6 +55,8 @@ class _EditDialogState extends State<EditDialog> {
_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<ProductOption> _localProducts;
@@ -68,6 +72,8 @@ class _EditDialogState extends State<EditDialog> {
_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
@@ -273,6 +279,8 @@ class _EditDialogState extends State<EditDialog> {
ItemEdit(
productId: _productId,
productName: _productName,
learnAlias: _learnAlias,
learnAliasGlobally: _learnAlias && widget.canLearnGlobalAlias && _learnAliasGlobally,
categoryId: _productCategoryId,
categoryPath: _productCategoryPath,
categorySource: _productCategorySource,
@@ -358,6 +366,8 @@ class _EditDialogState extends State<EditDialog> {
else
_buildCreateProductSection(theme, aiLabel),
const SizedBox(height: 12),
_buildAliasSection(theme, item),
const SizedBox(height: 12),
if (_destination == _Destination.inventory)
_buildQuantitySection(theme, totalPreview, currentUnit)
else
@@ -429,6 +439,70 @@ class _EditDialogState extends State<EditDialog> {
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<bool>(
segments: const [
ButtonSegment<bool>(
value: false,
label: Text('Privat alias'),
icon: Icon(Icons.lock_outline, size: 16),
),
ButtonSegment<bool>(
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,