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 '../../auth/data/auth_providers.dart';
import '../domain/pantry_item.dart';
import '../domain/pantry_product.dart';
import 'pantry_repository.dart';
final pantryRepositoryProvider = Provider<PantryRepository>((ref) {
@@ -18,11 +17,3 @@ final pantryProvider = FutureProvider<List<PantryItem>>((ref) async {
() => 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_paths.dart';
import '../domain/pantry_item.dart';
import '../domain/pantry_product.dart';
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(
int productId, {
String? token,
@@ -5,6 +5,7 @@ class PantryItem {
final String? canonicalName;
final String? category;
final int? categoryId;
final String? categoryPath;
final String? location;
const PantryItem({
@@ -14,6 +15,7 @@ class PantryItem {
this.canonicalName,
this.category,
this.categoryId,
this.categoryPath,
this.location,
});
@@ -24,6 +26,17 @@ class PantryItem {
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) {
final product = json['product'] as Map<String, dynamic>? ?? {};
return PantryItem(
@@ -33,7 +46,25 @@ class PantryItem {
canonicalName: product['canonicalName']?.toString(),
category: product['category']?.toString(),
categoryId: (product['categoryId'] as num?)?.toInt(),
categoryPath: _buildCategoryPath(product['categoryRef']),
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 '../data/pantry_providers.dart';
import '../domain/pantry_item.dart';
import '../domain/pantry_product.dart';
final _logger = Logger('PantryScreen');
@@ -214,35 +213,26 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
}
}
String _resolveL1Category(PantryItem item, Map<int, PantryProduct> productById) {
final path = productById[item.productId]?.categoryPath?.trim();
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;
String _resolveL1Category(PantryItem item) {
return item.l1CategoryOrNull ?? context.l10n.pantryOtherCategory;
}
@override
Widget build(BuildContext context) {
final pantryAsync = ref.watch(pantryProvider);
final productsAsync = ref.watch(pantryProductsProvider);
if (pantryAsync.isLoading || productsAsync.isLoading) {
if (pantryAsync.isLoading) {
return LoadingStateView(label: context.l10n.pantryLoading);
}
if (pantryAsync.hasError || productsAsync.hasError) {
final error = pantryAsync.error ?? productsAsync.error;
if (pantryAsync.hasError) {
final error = pantryAsync.error;
_logger.severe('Error loading pantry or products: $error');
return buildCopyableErrorPanel(
context: context,
message: mapErrorToUserMessage(error ?? 'Okänt fel', context),
onRetry: () {
ref.invalidate(pantryProvider);
ref.invalidate(pantryProductsProvider);
},
title: 'Kunde inte läsa baslagret',
);
@@ -250,15 +240,17 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
final pantryItems =
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) {
if (_locationFilter.isEmpty) return true;
return (item.location ?? '').trim() == _locationFilter;
}).toList();
final l1LowerByItemId = {
for (final item in filteredItems)
item.id: _resolveL1Category(item).toLowerCase(),
};
filteredItems.sort((a, b) {
if (_sort == 'nameDesc') {
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());
}
if (_sort == 'l1CategoryAsc') {
final byL1 = _resolveL1Category(a, productById).toLowerCase().compareTo(
_resolveL1Category(b, productById).toLowerCase(),
);
final byL1 = (l1LowerByItemId[a.id] ?? '').compareTo(l1LowerByItemId[b.id] ?? '');
if (byL1 != 0) return byL1;
return a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase());
}
@@ -379,7 +369,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
if (index == 0) return filterSection;
if (index == 1) return headerSection;
final item = filteredItems[index - 2];
final l1Category = _resolveL1Category(item, productById);
final l1Category = _resolveL1Category(item);
return ListTile(
title: Text(item.displayName),