refactor: Remove PantryProduct class and simplify category resolution in PantryScreen
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-11 20:01:00 +02:00
parent a635f1002a
commit 68476142c1
5 changed files with 43 additions and 101 deletions
@@ -4,7 +4,6 @@ import '../../../core/api/api_providers.dart';
import '../../../core/api/guarded_api_call.dart'; import '../../../core/api/guarded_api_call.dart';
import '../../auth/data/auth_providers.dart'; import '../../auth/data/auth_providers.dart';
import '../domain/pantry_item.dart'; import '../domain/pantry_item.dart';
import '../domain/pantry_product.dart';
import 'pantry_repository.dart'; import 'pantry_repository.dart';
final pantryRepositoryProvider = Provider<PantryRepository>((ref) { final pantryRepositoryProvider = Provider<PantryRepository>((ref) {
@@ -18,11 +17,3 @@ final pantryProvider = FutureProvider<List<PantryItem>>((ref) async {
() => ref.read(pantryRepositoryProvider).fetchPantry(token: token), () => ref.read(pantryRepositoryProvider).fetchPantry(token: token),
); );
}); });
final pantryProductsProvider = FutureProvider<List<PantryProduct>>((ref) async {
final token = await ref.watch(authStateProvider.future);
return guardedApiCall(
ref,
() => ref.read(pantryRepositoryProvider).fetchProducts(token: token),
);
});
@@ -2,7 +2,6 @@ import 'package:logging/logging.dart';
import '../../../core/api/api_client.dart'; import '../../../core/api/api_client.dart';
import '../../../core/api/api_paths.dart'; import '../../../core/api/api_paths.dart';
import '../domain/pantry_item.dart'; import '../domain/pantry_item.dart';
import '../domain/pantry_product.dart';
final _logger = Logger('PantryRepository'); final _logger = Logger('PantryRepository');
@@ -25,20 +24,6 @@ class PantryRepository {
} }
} }
Future<List<PantryProduct>> fetchProducts({String? token}) async {
try {
final data = await _api.getJson(ProductApiPaths.mine, token: token);
final list = data as List<dynamic>;
_logger.info('Fetched ${list.length} products');
return list
.map((e) => PantryProduct.fromJson(e as Map<String, dynamic>))
.toList();
} catch (error) {
_logger.severe('Failed to fetch products: $error');
rethrow;
}
}
Future<PantryItem> createPantryItem( Future<PantryItem> createPantryItem(
int productId, { int productId, {
String? token, String? token,
@@ -5,6 +5,7 @@ class PantryItem {
final String? canonicalName; final String? canonicalName;
final String? category; final String? category;
final int? categoryId; final int? categoryId;
final String? categoryPath;
final String? location; final String? location;
const PantryItem({ const PantryItem({
@@ -14,6 +15,7 @@ class PantryItem {
this.canonicalName, this.canonicalName,
this.category, this.category,
this.categoryId, this.categoryId,
this.categoryPath,
this.location, this.location,
}); });
@@ -24,6 +26,17 @@ class PantryItem {
return productName; return productName;
} }
String? get l1CategoryOrNull {
final path = categoryPath?.trim();
if (path != null && path.isNotEmpty) {
return path.split('>').first.trim();
}
if (category != null && category!.trim().isNotEmpty) {
return category!.trim();
}
return null;
}
factory PantryItem.fromJson(Map<String, dynamic> json) { factory PantryItem.fromJson(Map<String, dynamic> json) {
final product = json['product'] as Map<String, dynamic>? ?? {}; final product = json['product'] as Map<String, dynamic>? ?? {};
return PantryItem( return PantryItem(
@@ -33,7 +46,25 @@ class PantryItem {
canonicalName: product['canonicalName']?.toString(), canonicalName: product['canonicalName']?.toString(),
category: product['category']?.toString(), category: product['category']?.toString(),
categoryId: (product['categoryId'] as num?)?.toInt(), categoryId: (product['categoryId'] as num?)?.toInt(),
categoryPath: _buildCategoryPath(product['categoryRef']),
location: json['location']?.toString(), location: json['location']?.toString(),
); );
} }
static String? _buildCategoryPath(dynamic rawCategoryRef) {
if (rawCategoryRef is! Map<String, dynamic>) return null;
final names = <String>[];
dynamic current = rawCategoryRef;
while (current is Map<String, dynamic>) {
final name = current['name']?.toString().trim();
if (name != null && name.isNotEmpty) {
names.add(name);
}
current = current['parent'];
}
if (names.isEmpty) return null;
return names.reversed.join(' > ');
}
} }
@@ -1,55 +0,0 @@
class PantryProduct {
final int id;
final String name;
final String? canonicalName;
final String? category;
final int? categoryId;
final String? categoryPath;
const PantryProduct({
required this.id,
required this.name,
this.canonicalName,
this.category,
this.categoryId,
this.categoryPath,
});
String get displayName {
if (canonicalName != null && canonicalName!.trim().isNotEmpty) {
return canonicalName!;
}
return name;
}
factory PantryProduct.fromJson(Map<String, dynamic> json) {
final categoryRef = json['categoryRef'];
final path = _buildCategoryPath(categoryRef);
return PantryProduct(
id: (json['id'] as num).toInt(),
name: (json['name'] ?? '').toString(),
canonicalName: json['canonicalName']?.toString(),
category: json['category']?.toString(),
categoryId: (json['categoryId'] as num?)?.toInt(),
categoryPath: path,
);
}
static String? _buildCategoryPath(dynamic rawCategoryRef) {
if (rawCategoryRef is! Map<String, dynamic>) return null;
final names = <String>[];
dynamic current = rawCategoryRef;
while (current is Map<String, dynamic>) {
final name = current['name']?.toString().trim();
if (name != null && name.isNotEmpty) {
names.insert(0, name);
}
current = current['parent'];
}
if (names.isEmpty) return null;
return names.join(' > ');
}
}
@@ -11,7 +11,6 @@ import '../../auth/data/auth_providers.dart';
import '../../inventory/data/inventory_providers.dart'; import '../../inventory/data/inventory_providers.dart';
import '../data/pantry_providers.dart'; import '../data/pantry_providers.dart';
import '../domain/pantry_item.dart'; import '../domain/pantry_item.dart';
import '../domain/pantry_product.dart';
final _logger = Logger('PantryScreen'); final _logger = Logger('PantryScreen');
@@ -214,35 +213,26 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
} }
} }
String _resolveL1Category(PantryItem item, Map<int, PantryProduct> productById) { String _resolveL1Category(PantryItem item) {
final path = productById[item.productId]?.categoryPath?.trim(); return item.l1CategoryOrNull ?? context.l10n.pantryOtherCategory;
if (path != null && path.isNotEmpty) {
return path.split('>').first.trim();
}
if (item.category != null && item.category!.trim().isNotEmpty) {
return item.category!.trim();
}
return context.l10n.pantryOtherCategory;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final pantryAsync = ref.watch(pantryProvider); final pantryAsync = ref.watch(pantryProvider);
final productsAsync = ref.watch(pantryProductsProvider);
if (pantryAsync.isLoading || productsAsync.isLoading) { if (pantryAsync.isLoading) {
return LoadingStateView(label: context.l10n.pantryLoading); return LoadingStateView(label: context.l10n.pantryLoading);
} }
if (pantryAsync.hasError || productsAsync.hasError) { if (pantryAsync.hasError) {
final error = pantryAsync.error ?? productsAsync.error; final error = pantryAsync.error;
_logger.severe('Error loading pantry or products: $error'); _logger.severe('Error loading pantry or products: $error');
return buildCopyableErrorPanel( return buildCopyableErrorPanel(
context: context, context: context,
message: mapErrorToUserMessage(error ?? 'Okänt fel', context), message: mapErrorToUserMessage(error ?? 'Okänt fel', context),
onRetry: () { onRetry: () {
ref.invalidate(pantryProvider); ref.invalidate(pantryProvider);
ref.invalidate(pantryProductsProvider);
}, },
title: 'Kunde inte läsa baslagret', title: 'Kunde inte läsa baslagret',
); );
@@ -250,15 +240,17 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
final pantryItems = final pantryItems =
pantryAsync.maybeWhen(data: (d) => d, orElse: () => null) ?? const []; pantryAsync.maybeWhen(data: (d) => d, orElse: () => null) ?? const [];
final products =
productsAsync.maybeWhen(data: (d) => d, orElse: () => null) ?? const [];
final productById = {for (final product in products) product.id: product};
final filteredItems = pantryItems.where((item) { final filteredItems = pantryItems.where((item) {
if (_locationFilter.isEmpty) return true; if (_locationFilter.isEmpty) return true;
return (item.location ?? '').trim() == _locationFilter; return (item.location ?? '').trim() == _locationFilter;
}).toList(); }).toList();
final l1LowerByItemId = {
for (final item in filteredItems)
item.id: _resolveL1Category(item).toLowerCase(),
};
filteredItems.sort((a, b) { filteredItems.sort((a, b) {
if (_sort == 'nameDesc') { if (_sort == 'nameDesc') {
return b.displayName.toLowerCase().compareTo(a.displayName.toLowerCase()); return b.displayName.toLowerCase().compareTo(a.displayName.toLowerCase());
@@ -271,9 +263,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
return a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()); return a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase());
} }
if (_sort == 'l1CategoryAsc') { if (_sort == 'l1CategoryAsc') {
final byL1 = _resolveL1Category(a, productById).toLowerCase().compareTo( final byL1 = (l1LowerByItemId[a.id] ?? '').compareTo(l1LowerByItemId[b.id] ?? '');
_resolveL1Category(b, productById).toLowerCase(),
);
if (byL1 != 0) return byL1; if (byL1 != 0) return byL1;
return a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()); return a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase());
} }
@@ -379,7 +369,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
if (index == 0) return filterSection; if (index == 0) return filterSection;
if (index == 1) return headerSection; if (index == 1) return headerSection;
final item = filteredItems[index - 2]; final item = filteredItems[index - 2];
final l1Category = _resolveL1Category(item, productById); final l1Category = _resolveL1Category(item);
return ListTile( return ListTile(
title: Text(item.displayName), title: Text(item.displayName),