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
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/api/api_error_mapper.dart';
@@ -278,7 +279,13 @@ class _AdminInventoryPanelState extends ConsumerState<AdminInventoryPanel> {
width: 460,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Välj två poster för samma användare, produkt och enhet. Source tas bort och target behålls.',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 12),
DropdownButtonFormField<int>(
initialValue: sourceId,
items: _items
@@ -454,12 +461,58 @@ class _AdminInventoryPanelState extends ConsumerState<AdminInventoryPanel> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(child: Text(_error!));
final message = _error!;
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 720),
child: Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Kunde inte läsa inventory-data',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
SelectableText(message),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
FilledButton.icon(
onPressed: _load,
icon: const Icon(Icons.refresh),
label: const Text('Försök igen'),
),
OutlinedButton.icon(
onPressed: () {
Clipboard.setData(ClipboardData(text: message));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Felmeddelande kopierat.')),
);
},
icon: const Icon(Icons.copy_all),
label: const Text('Kopiera fel'),
),
],
),
],
),
),
),
),
);
}
final filtered = _filtered;
@@ -467,6 +520,33 @@ class _AdminInventoryPanelState extends ConsumerState<AdminInventoryPanel> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Inventory', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
Text(
'Här arbetar du på användarnas inventory-poster. Du kan filtrera per användare, justera mängder, flytta poster till baslager och slå ihop duplicerade rader.',
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 8),
const Wrap(
spacing: 8,
runSpacing: 8,
children: [
Chip(label: Text('User-scope')),
Chip(label: Text('Merge')),
Chip(label: Text('Flytta till baslager')),
],
),
],
),
),
),
const SizedBox(height: 12),
Row(
children: [
SizedBox(
@@ -551,35 +631,71 @@ class _AdminInventoryPanelState extends ConsumerState<AdminInventoryPanel> {
const SizedBox(height: 8),
Expanded(
child: Card(
child: ListView.separated(
itemCount: filtered.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final item = filtered[index];
return ListTile(
title: Text(item.displayName),
subtitle: Text(
'${item.quantity} ${item.unit} · ${item.username} (${item.userEmail})'
'${item.location == null || item.location!.isEmpty ? '' : ' · ${item.location}'}',
child: filtered.isEmpty
? Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Inventory', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
Text(
'Inga inventory-poster hittades med nuvarande filter.',
style: theme.textTheme.bodyMedium,
),
],
),
)
: ListView.separated(
itemCount: filtered.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final item = filtered[index];
return ListTile(
title: Text(item.displayName),
subtitle: Text(
'${item.quantity} ${item.unit} · ${item.username} (${item.userEmail})'
'${item.location == null || item.location!.isEmpty ? '' : ' · ${item.location}'}',
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: 'Flytta till baslager',
onPressed: () async {
try {
await ref.read(adminRepositoryProvider).moveAdminInventoryToPantry(item.id);
if (!mounted) return;
await _load();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Flyttade "${item.displayName}" till baslager.')),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)),
);
}
},
icon: const Icon(Icons.storefront_outlined),
),
IconButton(
tooltip: 'Ändra',
onPressed: () => _editItem(item),
icon: const Icon(Icons.edit_outlined),
),
IconButton(
tooltip: 'Ta bort',
onPressed: () => _deleteItem(item),
icon: const Icon(Icons.delete_outline),
),
],
),
);
},
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: 'Ändra',
onPressed: () => _editItem(item),
icon: const Icon(Icons.edit_outlined),
),
IconButton(
tooltip: 'Ta bort',
onPressed: () => _deleteItem(item),
icon: const Icon(Icons.delete_outline),
),
],
),
);
},
),
),
),
],
@@ -681,7 +797,15 @@ class _InventoryFormDialogState extends State<_InventoryFormDialog> {
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.initial == null
? 'Skapa en ny inventory-rad för en användare. Välj produkt, mängd, enhet och valfria metadata.'
: 'Ändra den valda inventory-raden. Produkt, mängd, enhet och metadata kan justeras utan att byta ägare.',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 12),
if (widget.initial == null) ...[
DropdownButtonFormField<int>(
initialValue: _ownerUserId,