feat: Implement admin pantry item management with create and update functionality, including category selection and validation
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:
@@ -3,7 +3,10 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/api/api_error_mapper.dart';
|
||||
import '../../../core/ui/category_then_product_picker.dart';
|
||||
import '../../../core/ui/product_picker_field.dart';
|
||||
import '../data/admin_repository.dart';
|
||||
import '../domain/admin_category_node.dart';
|
||||
import '../domain/admin_inventory_item.dart';
|
||||
import '../domain/admin_product.dart';
|
||||
import '../domain/user_admin.dart';
|
||||
@@ -34,6 +37,7 @@ class _AdminInventoryPanelState extends ConsumerState<AdminInventoryPanel> {
|
||||
_InventorySort _sort = _InventorySort.newest;
|
||||
List<AdminInventoryItem> _items = [];
|
||||
List<AdminProduct> _products = [];
|
||||
List<AdminCategoryNode> _categories = [];
|
||||
List<UserAdmin> _users = [];
|
||||
|
||||
@override
|
||||
@@ -55,13 +59,15 @@ class _AdminInventoryPanelState extends ConsumerState<AdminInventoryPanel> {
|
||||
sort: _sortParam,
|
||||
),
|
||||
ref.read(adminRepositoryProvider).listGlobalProducts(),
|
||||
ref.read(adminRepositoryProvider).listCategoryTree(),
|
||||
ref.read(adminRepositoryProvider).listUsers(),
|
||||
]);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_items = results[0] as List<AdminInventoryItem>;
|
||||
_products = results[1] as List<AdminProduct>;
|
||||
_users = results[2] as List<UserAdmin>;
|
||||
_categories = results[2] as List<AdminCategoryNode>;
|
||||
_users = results[3] as List<UserAdmin>;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
@@ -94,7 +100,8 @@ class _AdminInventoryPanelState extends ConsumerState<AdminInventoryPanel> {
|
||||
return item.displayName.toLowerCase().contains(q) ||
|
||||
item.username.toLowerCase().contains(q) ||
|
||||
item.userEmail.toLowerCase().contains(q) ||
|
||||
(item.location ?? '').toLowerCase().contains(q);
|
||||
(item.location ?? '').toLowerCase().contains(q) ||
|
||||
(item.categoryPath ?? '').toLowerCase().contains(q);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -453,6 +460,7 @@ class _AdminInventoryPanelState extends ConsumerState<AdminInventoryPanel> {
|
||||
builder: (context) => _InventoryFormDialog(
|
||||
users: _users,
|
||||
products: _products,
|
||||
categories: _categories,
|
||||
initial: initial,
|
||||
initialOwnerUserId: initialOwnerUserId,
|
||||
),
|
||||
@@ -656,7 +664,8 @@ class _AdminInventoryPanelState extends ConsumerState<AdminInventoryPanel> {
|
||||
title: Text(item.displayName),
|
||||
subtitle: Text(
|
||||
'${item.quantity} ${item.unit} · ${item.username} (${item.userEmail})'
|
||||
'${item.location == null || item.location!.isEmpty ? '' : ' · ${item.location}'}',
|
||||
'${item.location == null || item.location!.isEmpty ? '' : ' · ${item.location}'}'
|
||||
'${item.categoryPath == null || item.categoryPath!.isEmpty ? '' : ' · ${item.categoryPath}'}',
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -730,12 +739,14 @@ class _InventoryFormValues {
|
||||
class _InventoryFormDialog extends StatefulWidget {
|
||||
final List<UserAdmin> users;
|
||||
final List<AdminProduct> products;
|
||||
final List<AdminCategoryNode> categories;
|
||||
final AdminInventoryItem? initial;
|
||||
final int? initialOwnerUserId;
|
||||
|
||||
const _InventoryFormDialog({
|
||||
required this.users,
|
||||
required this.products,
|
||||
required this.categories,
|
||||
this.initial,
|
||||
this.initialOwnerUserId,
|
||||
});
|
||||
@@ -756,6 +767,9 @@ class _InventoryFormDialogState extends State<_InventoryFormDialog> {
|
||||
|
||||
int? _ownerUserId;
|
||||
int? _productId;
|
||||
int? _categoryId;
|
||||
String? _categoryPath;
|
||||
String? _productErrorText;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -763,6 +777,9 @@ class _InventoryFormDialogState extends State<_InventoryFormDialog> {
|
||||
final initial = widget.initial;
|
||||
_ownerUserId = initial?.userId ?? widget.initialOwnerUserId;
|
||||
_productId = initial?.productId;
|
||||
final initialProduct = _productById(_productId);
|
||||
_categoryId = initialProduct?.categoryId;
|
||||
_categoryPath = initialProduct?.categoryPath;
|
||||
_quantityController = TextEditingController(
|
||||
text: initial == null ? '' : initial.quantity.toString(),
|
||||
);
|
||||
@@ -786,6 +803,44 @@ class _InventoryFormDialogState extends State<_InventoryFormDialog> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
AdminProduct? _productById(int? id) {
|
||||
if (id == null) return null;
|
||||
for (final product in widget.products) {
|
||||
if (product.id == id) return product;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<ProductOption> _productOptions() {
|
||||
final source = _categoryId == null
|
||||
? widget.products
|
||||
: widget.products.where((p) => p.categoryId == _categoryId).toList();
|
||||
final sorted = [...source]
|
||||
..sort((a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()));
|
||||
return sorted
|
||||
.map((p) => (id: p.id, name: p.displayName, categoryId: p.categoryId))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<void> _pickCategory() async {
|
||||
final selected = await CategoryThenProductPicker.showCategorySheet(
|
||||
context,
|
||||
categoryTree: widget.categories,
|
||||
preselectedCategoryId: _categoryId,
|
||||
);
|
||||
if (selected == null || !mounted) return;
|
||||
setState(() {
|
||||
_categoryId = selected.id;
|
||||
_categoryPath = selected.path;
|
||||
if (_productId != null) {
|
||||
final current = _productById(_productId);
|
||||
if (current?.categoryId != _categoryId) {
|
||||
_productId = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
@@ -833,20 +888,53 @@ class _InventoryFormDialogState extends State<_InventoryFormDialog> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
DropdownButtonFormField<int>(
|
||||
initialValue: _productId,
|
||||
items: widget.products
|
||||
.map((p) => DropdownMenuItem<int>(
|
||||
value: p.id,
|
||||
child: Text(
|
||||
p.displayName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) => setState(() => _productId = value),
|
||||
decoration: const InputDecoration(labelText: 'Produkt'),
|
||||
validator: (value) => value == null ? 'Välj en produkt' : null,
|
||||
GestureDetector(
|
||||
onTap: _pickCategory,
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Kategori',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
child: Text(
|
||||
_categoryPath == null || _categoryPath!.trim().isEmpty
|
||||
? 'Tryck för att välja kategori'
|
||||
: _categoryPath!,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: _pickCategory,
|
||||
icon: const Icon(Icons.category_outlined),
|
||||
label: const Text('Välj kategori'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_categoryId = null;
|
||||
_categoryPath = null;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.clear),
|
||||
label: const Text('Rensa kategori'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ProductPickerField(
|
||||
products: _productOptions(),
|
||||
value: _productId,
|
||||
label: 'Produkt',
|
||||
errorText: _productErrorText,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_productId = value;
|
||||
_productErrorText = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
@@ -905,6 +993,10 @@ class _InventoryFormDialogState extends State<_InventoryFormDialog> {
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
if (_productId == null) {
|
||||
setState(() => _productErrorText = 'Välj en produkt');
|
||||
return;
|
||||
}
|
||||
|
||||
final quantity =
|
||||
double.parse(_quantityController.text.trim().replaceAll(',', '.'));
|
||||
|
||||
Reference in New Issue
Block a user