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
@@ -173,6 +173,9 @@ class AdminRepository {
Future<List<AdminProduct>> listProducts() =>
_getList(ProductApiPaths.mine, AdminProduct.fromJson);
Future<List<AdminProduct>> listGlobalProducts() =>
_getList(ProductApiPaths.list, AdminProduct.fromJson, requiresAuth: false);
Future<List<AdminProduct>> listDeletedProducts() =>
_getList(ProductApiPaths.deleted, AdminProduct.fromJson);
@@ -2,6 +2,8 @@ class ReceiptAlias {
final int id;
final String receiptName;
final int productId;
final int? ownerId;
final bool isGlobal;
final String? productName;
final String? productCanonicalName;
@@ -9,10 +11,14 @@ class ReceiptAlias {
required this.id,
required this.receiptName,
required this.productId,
required this.ownerId,
required this.isGlobal,
this.productName,
this.productCanonicalName,
});
bool get isPrivate => !isGlobal;
String get displayProductName {
final canonical = productCanonicalName?.trim();
if (canonical != null && canonical.isNotEmpty) return canonical;
@@ -33,6 +39,8 @@ class ReceiptAlias {
productId: (json['productId'] as num?)?.toInt() ??
(productMap['id'] as num?)?.toInt() ??
0,
ownerId: (json['ownerId'] as num?)?.toInt(),
isGlobal: json['isGlobal'] == true,
productName: productMap['name']?.toString(),
productCanonicalName: productMap['canonicalName']?.toString(),
);
@@ -46,7 +46,7 @@ class _AdminAliasesPanelState extends ConsumerState<AdminAliasesPanel> {
try {
final results = await Future.wait<dynamic>([
ref.read(adminRepositoryProvider).listReceiptAliases(),
ref.read(adminRepositoryProvider).listProducts(),
ref.read(adminRepositoryProvider).listGlobalProducts(),
]);
if (!mounted) return;
setState(() {
@@ -67,7 +67,7 @@ class _AdminAliasesPanelState extends ConsumerState<AdminAliasesPanel> {
}
Future<void> _upsertAlias() async {
final rawAlias = _aliasController.text.trim().toLowerCase();
final rawAlias = _aliasController.text.trim();
final productId = _selectedProductId;
if (rawAlias.isEmpty || productId == null) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -186,6 +186,11 @@ class _AdminAliasesPanelState extends ConsumerState<AdminAliasesPanel> {
color: Theme.of(context).colorScheme.outline,
),
),
const SizedBox(height: 4),
Chip(
visualDensity: VisualDensity.compact,
label: Text(alias.isGlobal ? 'Global' : 'Privat'),
),
],
),
trailing: IconButton(
@@ -198,17 +203,6 @@ class _AdminAliasesPanelState extends ConsumerState<AdminAliasesPanel> {
);
}
Widget buildAliasList({EdgeInsetsGeometry padding = EdgeInsets.zero}) {
return ListView.builder(
padding: padding,
itemCount: filteredAliases.length,
itemBuilder: (context, index) {
final alias = filteredAliases[index];
return buildAliasCard(alias);
},
);
}
final content = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [