refactor: Remove PantryProduct class and simplify category resolution in PantryScreen
Test Suite / test (24.15.0) (push) Has been cancelled
Test Suite / test (24.15.0) (push) Has been cancelled
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user