feat: enhance inventory and pantry features with filtering, sorting, and error handling improvements

This commit is contained in:
Nils-Johan Gynther
2026-04-22 18:14:19 +02:00
parent dd05fed279
commit 07ed164112
8 changed files with 142 additions and 11 deletions
@@ -7,15 +7,36 @@ import '../domain/inventory_item.dart';
import '../domain/inventory_consumption.dart';
import 'inventory_repository.dart';
class InventoryQuery {
final String location;
final String sort;
const InventoryQuery({required this.location, required this.sort});
}
final inventoryLocationFilterProvider = StateProvider<String>((ref) => '');
final inventorySortFilterProvider = StateProvider<String>((ref) => '');
final inventoryQueryProvider = Provider<InventoryQuery>((ref) {
final location = ref.watch(inventoryLocationFilterProvider);
final sort = ref.watch(inventorySortFilterProvider);
return InventoryQuery(location: location, sort: sort);
});
final inventoryRepositoryProvider = Provider<InventoryRepository>((ref) {
return InventoryRepository(ref.watch(apiClientProvider));
});
final inventoryProvider = FutureProvider<List<InventoryItem>>((ref) async {
final token = await ref.watch(authStateProvider.future);
final query = ref.watch(inventoryQueryProvider);
return guardedApiCall(
ref,
() => ref.read(inventoryRepositoryProvider).fetchInventory(token: token),
() => ref.read(inventoryRepositoryProvider).fetchInventory(
location: query.location,
sort: query.sort,
token: token,
),
);
});
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/api/api_error_mapper.dart';
import '../../../core/ui/async_state_views.dart';
import '../data/inventory_providers.dart';
import '../domain/inventory_consumption.dart';
@@ -25,7 +26,7 @@ class ConsumptionHistoryScreen extends ConsumerWidget {
body: historyAsync.when(
loading: () => const LoadingStateView(label: 'Laddar historik...'),
error: (e, _) => ErrorStateView(
message: e.toString(),
message: mapErrorToUserMessage(e),
onRetry: () => ref.invalidate(consumptionHistoryProvider(itemId)),
),
data: (history) {
@@ -36,7 +36,7 @@ class InventoryDetailScreen extends ConsumerWidget {
body: itemAsync.when(
loading: () => const LoadingStateView(label: 'Laddar...'),
error: (e, _) => ErrorStateView(
message: e.toString(),
message: mapErrorToUserMessage(e),
onRetry: () => ref.invalidate(inventoryDetailProvider(itemId)),
),
data: (item) => ListView(
@@ -130,7 +130,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
body: itemAsync.when(
loading: () => const LoadingStateView(label: 'Laddar...'),
error: (e, _) => ErrorStateView(
message: e.toString(),
message: mapErrorToUserMessage(e),
onRetry: () => ref.invalidate(inventoryDetailProvider(widget.itemId)),
),
data: (item) {
@@ -11,21 +11,87 @@ import '../domain/inventory_item.dart';
class InventoryScreen extends ConsumerWidget {
const InventoryScreen({super.key});
static const _locationOptions = <String>['', 'Kyl', 'Frys', 'Skafferi'];
static const _sortOptions = <({String value, String label})>[
(value: '', label: 'Senast tillagda'),
(value: 'nameAsc', label: 'Namn A-O'),
(value: 'bestBeforeAsc', label: 'Bast fore stigande'),
(value: 'bestBeforeDesc', label: 'Bast fore fallande'),
];
@override
Widget build(BuildContext context, WidgetRef ref) {
final location = ref.watch(inventoryLocationFilterProvider);
final sort = ref.watch(inventorySortFilterProvider);
final inventoryAsync = ref.watch(inventoryProvider);
return inventoryAsync.when(
loading: () => const LoadingStateView(label: 'Laddar inventarie...'),
error: (e, _) => ErrorStateView(
message: e.toString(),
message: mapErrorToUserMessage(e),
onRetry: () => ref.invalidate(inventoryProvider),
),
data: (items) {
final filterSection = Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Filter och sortering',
style: TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _locationOptions
.map(
(option) => ChoiceChip(
label: Text(option.isEmpty ? 'Alla' : option),
selected: location == option,
onSelected: (_) => ref
.read(inventoryLocationFilterProvider.notifier)
.state = option,
),
)
.toList(),
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: sort,
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Sortering',
border: OutlineInputBorder(),
),
items: _sortOptions
.map(
(option) => DropdownMenuItem<String>(
value: option.value,
child: Text(option.label),
),
)
.toList(),
onChanged: (value) {
ref.read(inventorySortFilterProvider.notifier).state =
value ?? '';
},
),
],
),
);
if (items.isEmpty) {
return Stack(
children: [
const EmptyStateView(title: 'Inventariet är tomt.'),
ListView(
padding: const EdgeInsets.only(bottom: 88),
children: [
filterSection,
const EmptyStateView(title: 'Inventariet ar tomt.'),
],
),
Positioned(
right: 16,
bottom: 16,
@@ -42,10 +108,11 @@ class InventoryScreen extends ConsumerWidget {
children: [
ListView.separated(
padding: const EdgeInsets.only(bottom: 88),
itemCount: items.length,
itemCount: items.length + 1,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final item = items[index];
if (index == 0) return filterSection;
final item = items[index - 1];
return _InventoryTile(item: item);
},
),