feat: implement matchedVia tracking for receipt items and enhance user alias management
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-07 13:57:41 +02:00
parent f7446cc2df
commit d92272e554
9 changed files with 287 additions and 8 deletions
@@ -16,6 +16,8 @@ class ParsedReceiptItem {
final String? categorySuggestionName;
final String? categorySuggestionPath;
final int? categorySuggestionId;
// matchkälla för UI-visning: 'alias' | 'wordmatch' | 'ai' | 'none'
final String? matchedVia;
ParsedReceiptItem({
required this.rawName,
@@ -31,6 +33,7 @@ class ParsedReceiptItem {
this.categorySuggestionName,
this.categorySuggestionPath,
this.categorySuggestionId,
this.matchedVia,
});
factory ParsedReceiptItem.fromJson(Map<String, dynamic> json) {
@@ -49,6 +52,7 @@ class ParsedReceiptItem {
categorySuggestionName: cat?['categoryName'] as String?,
categorySuggestionPath: cat?['path'] as String?,
categorySuggestionId: (cat?['categoryId'] as num?)?.toInt(),
matchedVia: json['matchedVia'] as String?,
);
}
@@ -72,6 +76,7 @@ class ParsedReceiptItem {
'categoryName': categorySuggestionName,
'path': categorySuggestionPath,
},
if (matchedVia != null) 'matchedVia': matchedVia,
};
}
}
@@ -540,15 +540,16 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
}
final normalizedReceiptName = item.rawName.trim().toLowerCase();
final shouldLearnAlias =
canManageAliases &&
normalizedReceiptName.isNotEmpty &&
item.matchedProductId != pid;
// Spara alias för alla användare (user-scope) när raden inte redan matchades via alias,
// eller admin sparar global alias.
final alreadyAliasMatch = item.matchedVia == 'alias' && item.matchedProductId == pid;
final shouldLearnAlias = normalizedReceiptName.isNotEmpty && !alreadyAliasMatch;
if (shouldLearnAlias) {
try {
await adminRepo.upsertReceiptAlias(
receiptName: normalizedReceiptName,
productId: pid,
isGlobal: canManageAliases,
);
aliasesLearned++;
} catch (e, st) {
@@ -644,6 +645,31 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
);
}
Widget _buildMatchedViaBadge(ParsedReceiptItem item, ThemeData theme) {
final via = item.matchedVia;
if (via == null || via == 'none') return const SizedBox.shrink();
final (label, bg, fg) = switch (via) {
'alias' => ('Alias', Colors.teal.shade50, Colors.teal.shade800),
'wordmatch' => ('Ordmatch', Colors.blue.shade50, Colors.blue.shade800),
'ai' => ('AI-kategori', Colors.purple.shade50, Colors.purple.shade800),
_ => ('Matchad', theme.colorScheme.surfaceContainerHighest, theme.colorScheme.onSurfaceVariant),
};
return Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(999),
border: Border.all(color: fg.withOpacity(0.3)),
),
child: Text(
label,
style: theme.textTheme.labelSmall?.copyWith(color: fg, fontWeight: FontWeight.w600),
),
);
}
@override
Widget build(BuildContext context) {
final session = ref.watch(receiptImportSessionProvider);
@@ -797,6 +823,7 @@ class _ReceiptImportTabState extends ConsumerState<ReceiptImportTab> {
fontWeight: FontWeight.w500,
),
),
_buildMatchedViaBadge(item, theme),
if (edit.categorySource != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),