feat: Add functionality to move inventory items to pantry and enhance pantry management
Test Suite / test (24.15.0) (push) Has been cancelled

- Implemented moveInventoryItemToPantry method in InventoryRepository to facilitate moving items from inventory to pantry.
- Enhanced InventoryScreen with a new header section providing context about the inventory.
- Added a button in SwipeableInventoryTile to move items to pantry with appropriate error handling.
- Introduced movePantryItemToInventory method in PantryRepository to support moving items back to inventory.
- Refactored PantryScreen to rename _addToInventory to _moveToInventory for clarity and updated UI to reflect changes.
- Added AdminPantryItem model to represent pantry items in the admin panel.
- Created AdminPantryPanel for managing pantry items, including moving items to inventory and listing users.
- Developed AdminPrivateProductsPanel for managing private products, allowing promotion to global products.
This commit is contained in:
Nils-Johan Gynther
2026-05-11 09:06:30 +02:00
parent edf9c74e75
commit 84ccabe2fe
27 changed files with 1851 additions and 376 deletions
@@ -71,4 +71,18 @@ class PantryRepository {
rethrow;
}
}
Future<void> movePantryItemToInventory(
int id, {
required Map<String, dynamic> body,
String? token,
}) async {
try {
await _api.postJson(PantryApiPaths.moveToInventory(id), body: body, token: token);
_logger.info('Moved pantry item with ID: $id to inventory');
} catch (error) {
_logger.severe('Failed to move pantry item to inventory: $error');
rethrow;
}
}
}
@@ -40,7 +40,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
_logger.info('Initializing PantryScreen');
}
Future<void> _addToInventory(PantryItem item) async {
Future<void> _moveToInventory(PantryItem item) async {
final quantityController = TextEditingController(text: '1');
String selectedUnit = 'st';
String? selectedLocation;
@@ -155,8 +155,9 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
try {
final token = await ref.read(authStateProvider.future);
await ref.read(inventoryRepositoryProvider).createInventoryItem(
{
await ref.read(pantryRepositoryProvider).movePantryItemToInventory(
item.id,
body: {
'productId': item.productId,
'quantity': payload['quantity'] as double,
'unit': payload['unit'] as String,
@@ -164,10 +165,11 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
},
token: token,
);
ref.invalidate(pantryProvider);
ref.invalidate(inventoryProvider);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.pantryItemAdded(item.displayName))),
SnackBar(content: Text('Flyttade "${item.displayName}" till inventarie.')),
);
} catch (error) {
_logger.severe('Failed to add item to inventory: $error');
@@ -235,12 +237,14 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
if (pantryAsync.hasError || productsAsync.hasError) {
final error = pantryAsync.error ?? productsAsync.error;
_logger.severe('Error loading pantry or products: $error');
return ErrorStateView(
return buildCopyableErrorPanel(
context: context,
message: mapErrorToUserMessage(error ?? 'Okänt fel', context),
onRetry: () {
ref.invalidate(pantryProvider);
ref.invalidate(pantryProductsProvider);
},
title: 'Kunde inte läsa baslagret',
);
}
@@ -323,11 +327,42 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
),
);
final headerSection = Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 4),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Baslager', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text(
'Det här är ditt user-scope baslager. Här lagrar du sådant du vill ha lätt åtkomligt och kan flytta poster vidare till inventarie när det behövs.',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8),
const Wrap(
spacing: 8,
runSpacing: 8,
children: [
Chip(label: Text('User-scope')),
Chip(label: Text('Flytta till inventarie')),
Chip(label: Text('Plats + kategori')),
],
),
],
),
),
),
);
final content = filteredItems.isEmpty
? ListView(
key: const PageStorageKey<String>('pantry-empty-list'),
padding: const EdgeInsets.fromLTRB(12, 0, 12, 96),
children: [
headerSection,
filterSection,
const EmptyStateView(
title: 'Baslagret är tomt',
@@ -338,11 +373,12 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
: ListView.separated(
key: const PageStorageKey<String>('pantry-main-list'),
padding: const EdgeInsets.fromLTRB(12, 0, 12, 96),
itemCount: filteredItems.length + 1,
itemCount: filteredItems.length + 2,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
if (index == 0) return filterSection;
final item = filteredItems[index - 1];
if (index == 1) return headerSection;
final item = filteredItems[index - 2];
final l1Category = _resolveL1Category(item, productById);
return ListTile(
@@ -358,9 +394,9 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: 'Lägg i inventarie',
tooltip: 'Flytta till inventarie',
icon: const Icon(Icons.inventory_2_outlined),
onPressed: () => _addToInventory(item),
onPressed: () => _moveToInventory(item),
),
IconButton(
tooltip: 'Ta bort',