feat: Enhance inventory screens with category selection and product loading improvements
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:
@@ -8,8 +8,11 @@ import '../../../core/api/api_paths.dart';
|
|||||||
import '../../../core/api/api_providers.dart';
|
import '../../../core/api/api_providers.dart';
|
||||||
import '../../../core/forms/form_options.dart';
|
import '../../../core/forms/form_options.dart';
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
|
import '../../../core/ui/category_then_product_picker.dart';
|
||||||
import '../../../core/ui/product_picker_field.dart';
|
import '../../../core/ui/product_picker_field.dart';
|
||||||
|
import '../../../core/ui/searchable_category_field.dart';
|
||||||
import '../../auth/data/auth_providers.dart';
|
import '../../auth/data/auth_providers.dart';
|
||||||
|
import '../../admin/domain/admin_category_node.dart';
|
||||||
import '../../pantry/data/pantry_providers.dart';
|
import '../../pantry/data/pantry_providers.dart';
|
||||||
import '../data/inventory_providers.dart';
|
import '../data/inventory_providers.dart';
|
||||||
import '../../import/data/receipt_import_session.dart' show ImportDestination;
|
import '../../import/data/receipt_import_session.dart' show ImportDestination;
|
||||||
@@ -34,7 +37,10 @@ class _CreateInventoryScreenState
|
|||||||
final _commentController = TextEditingController();
|
final _commentController = TextEditingController();
|
||||||
|
|
||||||
int? _selectedProductId;
|
int? _selectedProductId;
|
||||||
|
int? _selectedCategoryId;
|
||||||
List<Map<String, dynamic>> _products = [];
|
List<Map<String, dynamic>> _products = [];
|
||||||
|
List<AdminCategoryNode> _categoryTree = [];
|
||||||
|
List<CategorySelectOption> _categoryOptions = [];
|
||||||
bool _loadingProducts = false;
|
bool _loadingProducts = false;
|
||||||
DateTime? _purchaseDate;
|
DateTime? _purchaseDate;
|
||||||
DateTime? _bestBeforeDate;
|
DateTime? _bestBeforeDate;
|
||||||
@@ -48,7 +54,7 @@ class _CreateInventoryScreenState
|
|||||||
if (widget.initialDestination == 'pantry') {
|
if (widget.initialDestination == 'pantry') {
|
||||||
_destination = ImportDestination.pantry;
|
_destination = ImportDestination.pantry;
|
||||||
}
|
}
|
||||||
_loadProducts();
|
_loadProductsAndCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -61,22 +67,34 @@ class _CreateInventoryScreenState
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadProducts() async {
|
Future<void> _loadProductsAndCategories() async {
|
||||||
setState(() => _loadingProducts = true);
|
setState(() => _loadingProducts = true);
|
||||||
try {
|
try {
|
||||||
final token = await ref.read(authStateProvider.future);
|
final token = await ref.read(authStateProvider.future);
|
||||||
final api = ref.read(apiClientProvider);
|
final api = ref.read(apiClientProvider);
|
||||||
final data = await api.getJson(ProductApiPaths.mine, token: token);
|
final results = await Future.wait<dynamic>([
|
||||||
final list = data is List<dynamic>
|
api.getJson(ProductApiPaths.mine, token: token),
|
||||||
? data
|
api.getJson(CategoryApiPaths.tree, token: token),
|
||||||
: (data is Map<String, dynamic> && data['items'] is List<dynamic>)
|
]);
|
||||||
? data['items'] as List<dynamic>
|
final productData = results[0];
|
||||||
|
final categoryData = results[1];
|
||||||
|
final list = productData is List<dynamic>
|
||||||
|
? productData
|
||||||
|
: (productData is Map<String, dynamic> && productData['items'] is List<dynamic>)
|
||||||
|
? productData['items'] as List<dynamic>
|
||||||
|
: const <dynamic>[];
|
||||||
|
final categoryList = categoryData is List<dynamic>
|
||||||
|
? categoryData
|
||||||
|
: (categoryData is Map<String, dynamic> && categoryData['items'] is List<dynamic>)
|
||||||
|
? categoryData['items'] as List<dynamic>
|
||||||
: const <dynamic>[];
|
: const <dynamic>[];
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_products = list
|
_products = list.map((e) => e as Map<String, dynamic>).toList();
|
||||||
.map((e) => e as Map<String, dynamic>)
|
_categoryTree = categoryList
|
||||||
|
.map((e) => AdminCategoryNode.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||||
.toList();
|
.toList();
|
||||||
|
_categoryOptions = _flattenCategoryOptions(_categoryTree);
|
||||||
_loadingProducts = false;
|
_loadingProducts = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -90,6 +108,78 @@ class _CreateInventoryScreenState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<CategorySelectOption> _flattenCategoryOptions(
|
||||||
|
List<AdminCategoryNode> nodes, [
|
||||||
|
List<String> parents = const [],
|
||||||
|
]) {
|
||||||
|
final result = <CategorySelectOption>[];
|
||||||
|
for (final node in nodes) {
|
||||||
|
final pathParts = [...parents, node.name];
|
||||||
|
final path = pathParts.join(' > ');
|
||||||
|
result.add((value: node.id.toString(), label: path));
|
||||||
|
result.addAll(_flattenCategoryOptions(node.children, pathParts));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic>? _selectedProduct() {
|
||||||
|
if (_selectedProductId == null) return null;
|
||||||
|
for (final product in _products) {
|
||||||
|
if ((product['id'] as num).toInt() == _selectedProductId) {
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ProductOption> _productOptions() {
|
||||||
|
final source = _selectedCategoryId == null
|
||||||
|
? _products
|
||||||
|
: _products
|
||||||
|
.where((p) => (p['categoryId'] as num?)?.toInt() == _selectedCategoryId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final selected = _selectedProduct();
|
||||||
|
final withSelected = [...source];
|
||||||
|
if (selected != null &&
|
||||||
|
!withSelected.any((p) => (p['id'] as num).toInt() == _selectedProductId)) {
|
||||||
|
withSelected.add(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
withSelected.sort((a, b) {
|
||||||
|
final aName = (a['canonicalName'] ?? a['name'] ?? '').toString().toLowerCase();
|
||||||
|
final bName = (b['canonicalName'] ?? b['name'] ?? '').toString().toLowerCase();
|
||||||
|
return aName.compareTo(bName);
|
||||||
|
});
|
||||||
|
|
||||||
|
return withSelected
|
||||||
|
.map(
|
||||||
|
(p) => (
|
||||||
|
id: (p['id'] as num).toInt(),
|
||||||
|
name: (p['canonicalName'] ?? p['name'] ?? '').toString(),
|
||||||
|
categoryId: (p['categoryId'] as num?)?.toInt(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickCategory() async {
|
||||||
|
final selected = await CategoryThenProductPicker.showCategorySheet(
|
||||||
|
context,
|
||||||
|
categoryTree: _categoryTree,
|
||||||
|
preselectedCategoryId: _selectedCategoryId,
|
||||||
|
);
|
||||||
|
if (selected == null || !mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_selectedCategoryId = selected.id;
|
||||||
|
final current = _selectedProduct();
|
||||||
|
final currentCategoryId = (current?['categoryId'] as num?)?.toInt();
|
||||||
|
if (currentCategoryId != _selectedCategoryId) {
|
||||||
|
_selectedProductId = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _pickDate(bool isBestBefore) async {
|
Future<void> _pickDate(bool isBestBefore) async {
|
||||||
final picked = await showDatePicker(
|
final picked = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -182,21 +272,7 @@ class _CreateInventoryScreenState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sortedProducts = [..._products]
|
final productOptions = _productOptions();
|
||||||
..sort((a, b) {
|
|
||||||
final aName = (a['canonicalName'] ?? a['name'] ?? '').toString();
|
|
||||||
final bName = (b['canonicalName'] ?? b['name'] ?? '').toString();
|
|
||||||
return aName.toLowerCase().compareTo(bName.toLowerCase());
|
|
||||||
});
|
|
||||||
final productOptions = sortedProducts
|
|
||||||
.map(
|
|
||||||
(p) => (
|
|
||||||
id: (p['id'] as num).toInt(),
|
|
||||||
name: (p['canonicalName'] ?? p['name'] ?? '').toString(),
|
|
||||||
categoryId: (p['categoryId'] as num?)?.toInt(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final isInventory = _destination == ImportDestination.inventory;
|
final isInventory = _destination == ImportDestination.inventory;
|
||||||
|
|
||||||
@@ -226,6 +302,47 @@ class _CreateInventoryScreenState
|
|||||||
: (selected) => setState(() => _destination = selected.first),
|
: (selected) => setState(() => _destination = selected.first),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
SearchableCategoryField(
|
||||||
|
options: _categoryOptions,
|
||||||
|
value: _selectedCategoryId?.toString(),
|
||||||
|
label: 'Kategori (sökbar)',
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
setState(() {
|
||||||
|
_selectedCategoryId = int.tryParse(value);
|
||||||
|
final current = _selectedProduct();
|
||||||
|
final currentCategoryId = (current?['categoryId'] as num?)?.toInt();
|
||||||
|
if (currentCategoryId != _selectedCategoryId) {
|
||||||
|
_selectedProductId = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _loadingProducts || _saving || _categoryTree.isEmpty
|
||||||
|
? null
|
||||||
|
: _pickCategory,
|
||||||
|
icon: const Icon(Icons.category_outlined),
|
||||||
|
label: const Text('Välj kategori'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _saving
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
setState(() {
|
||||||
|
_selectedCategoryId = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
label: const Text('Rensa kategori'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
ProductPickerField(
|
ProductPickerField(
|
||||||
products: productOptions,
|
products: productOptions,
|
||||||
value: _selectedProductId,
|
value: _selectedProductId,
|
||||||
|
|||||||
@@ -3,11 +3,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../../core/api/api_error_mapper.dart';
|
import '../../../core/api/api_error_mapper.dart';
|
||||||
|
import '../../../core/api/api_paths.dart';
|
||||||
|
import '../../../core/api/api_providers.dart';
|
||||||
import '../../../core/utils/formatters.dart';
|
import '../../../core/utils/formatters.dart';
|
||||||
import '../../../core/forms/form_options.dart';
|
import '../../../core/forms/form_options.dart';
|
||||||
import '../../../core/l10n/l10n.dart';
|
import '../../../core/l10n/l10n.dart';
|
||||||
import '../../../core/ui/async_state_views.dart';
|
import '../../../core/ui/async_state_views.dart';
|
||||||
|
import '../../../core/ui/category_then_product_picker.dart';
|
||||||
|
import '../../../core/ui/product_picker_field.dart';
|
||||||
|
import '../../../core/ui/searchable_category_field.dart';
|
||||||
import '../../auth/data/auth_providers.dart';
|
import '../../auth/data/auth_providers.dart';
|
||||||
|
import '../../admin/domain/admin_category_node.dart';
|
||||||
import '../data/inventory_providers.dart';
|
import '../data/inventory_providers.dart';
|
||||||
import '../domain/inventory_item.dart';
|
import '../domain/inventory_item.dart';
|
||||||
|
|
||||||
@@ -30,10 +36,16 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
|
|||||||
final _commentController = TextEditingController();
|
final _commentController = TextEditingController();
|
||||||
|
|
||||||
bool _initialized = false;
|
bool _initialized = false;
|
||||||
|
bool _loadingProducts = false;
|
||||||
bool _opened = false;
|
bool _opened = false;
|
||||||
DateTime? _purchaseDate;
|
DateTime? _purchaseDate;
|
||||||
DateTime? _bestBeforeDate;
|
DateTime? _bestBeforeDate;
|
||||||
bool _saving = false;
|
bool _saving = false;
|
||||||
|
int? _selectedProductId;
|
||||||
|
int? _selectedCategoryId;
|
||||||
|
List<Map<String, dynamic>> _products = [];
|
||||||
|
List<AdminCategoryNode> _categoryTree = [];
|
||||||
|
List<CategorySelectOption> _categoryOptions = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -47,6 +59,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
|
|||||||
|
|
||||||
void _initControllers(InventoryItem item) {
|
void _initControllers(InventoryItem item) {
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
|
_selectedProductId = item.productId;
|
||||||
_quantityController.text = item.quantity.toString();
|
_quantityController.text = item.quantity.toString();
|
||||||
_unitController.text = item.unit;
|
_unitController.text = item.unit;
|
||||||
_locationController.text = item.location ?? '';
|
_locationController.text = item.location ?? '';
|
||||||
@@ -60,6 +73,132 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadProductsAndCategories();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadProductsAndCategories() async {
|
||||||
|
setState(() => _loadingProducts = true);
|
||||||
|
try {
|
||||||
|
final token = await ref.read(authStateProvider.future);
|
||||||
|
final api = ref.read(apiClientProvider);
|
||||||
|
final results = await Future.wait<dynamic>([
|
||||||
|
api.getJson(ProductApiPaths.mine, token: token),
|
||||||
|
api.getJson(CategoryApiPaths.tree, token: token),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final productData = results[0];
|
||||||
|
final categoryData = results[1];
|
||||||
|
final productList = productData is List<dynamic>
|
||||||
|
? productData
|
||||||
|
: (productData is Map<String, dynamic> && productData['items'] is List<dynamic>)
|
||||||
|
? productData['items'] as List<dynamic>
|
||||||
|
: const <dynamic>[];
|
||||||
|
final categoryList = categoryData is List<dynamic>
|
||||||
|
? categoryData
|
||||||
|
: (categoryData is Map<String, dynamic> && categoryData['items'] is List<dynamic>)
|
||||||
|
? categoryData['items'] as List<dynamic>
|
||||||
|
: const <dynamic>[];
|
||||||
|
|
||||||
|
final products = productList.map((e) => e as Map<String, dynamic>).toList();
|
||||||
|
final categoryTree = categoryList
|
||||||
|
.map((e) => AdminCategoryNode.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||||
|
.toList();
|
||||||
|
final categoryOptions = _flattenCategoryOptions(categoryTree);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_products = products;
|
||||||
|
_categoryTree = categoryTree;
|
||||||
|
_categoryOptions = categoryOptions;
|
||||||
|
|
||||||
|
final selected = _selectedProduct();
|
||||||
|
if (selected != null) {
|
||||||
|
_selectedCategoryId = (selected['categoryId'] as num?)?.toInt();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _loadingProducts = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CategorySelectOption> _flattenCategoryOptions(
|
||||||
|
List<AdminCategoryNode> nodes, [
|
||||||
|
List<String> parents = const [],
|
||||||
|
]) {
|
||||||
|
final result = <CategorySelectOption>[];
|
||||||
|
for (final node in nodes) {
|
||||||
|
final pathParts = [...parents, node.name];
|
||||||
|
final path = pathParts.join(' > ');
|
||||||
|
result.add((value: node.id.toString(), label: path));
|
||||||
|
result.addAll(_flattenCategoryOptions(node.children, pathParts));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic>? _selectedProduct() {
|
||||||
|
if (_selectedProductId == null) return null;
|
||||||
|
for (final product in _products) {
|
||||||
|
if ((product['id'] as num).toInt() == _selectedProductId) {
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ProductOption> _productOptions() {
|
||||||
|
final source = _selectedCategoryId == null
|
||||||
|
? _products
|
||||||
|
: _products.where((p) => (p['categoryId'] as num?)?.toInt() == _selectedCategoryId).toList();
|
||||||
|
|
||||||
|
final selected = _selectedProduct();
|
||||||
|
final withSelected = [...source];
|
||||||
|
if (selected != null && !withSelected.any((p) => (p['id'] as num).toInt() == _selectedProductId)) {
|
||||||
|
withSelected.add(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
withSelected.sort((a, b) {
|
||||||
|
final aName = (a['canonicalName'] ?? a['name'] ?? '').toString().toLowerCase();
|
||||||
|
final bName = (b['canonicalName'] ?? b['name'] ?? '').toString().toLowerCase();
|
||||||
|
return aName.compareTo(bName);
|
||||||
|
});
|
||||||
|
|
||||||
|
return withSelected
|
||||||
|
.map(
|
||||||
|
(p) => (
|
||||||
|
id: (p['id'] as num).toInt(),
|
||||||
|
name: (p['canonicalName'] ?? p['name'] ?? '').toString(),
|
||||||
|
categoryId: (p['categoryId'] as num?)?.toInt(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickCategory() async {
|
||||||
|
final selected = await CategoryThenProductPicker.showCategorySheet(
|
||||||
|
context,
|
||||||
|
categoryTree: _categoryTree,
|
||||||
|
preselectedCategoryId: _selectedCategoryId,
|
||||||
|
);
|
||||||
|
if (selected == null || !mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_selectedCategoryId = selected.id;
|
||||||
|
final current = _selectedProduct();
|
||||||
|
final currentCategoryId = (current?['categoryId'] as num?)?.toInt();
|
||||||
|
if (currentCategoryId != _selectedCategoryId) {
|
||||||
|
_selectedProductId = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _pickDate(bool isBestBefore) async {
|
Future<void> _pickDate(bool isBestBefore) async {
|
||||||
final picked = await showDatePicker(
|
final picked = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -84,6 +223,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
|
|||||||
try {
|
try {
|
||||||
final token = await ref.read(authStateProvider.future);
|
final token = await ref.read(authStateProvider.future);
|
||||||
final body = <String, dynamic>{
|
final body = <String, dynamic>{
|
||||||
|
'productId': _selectedProductId,
|
||||||
'quantity':
|
'quantity':
|
||||||
double.parse(_quantityController.text.trim().replaceAll(',', '.')),
|
double.parse(_quantityController.text.trim().replaceAll(',', '.')),
|
||||||
'unit': _unitController.text.trim(),
|
'unit': _unitController.text.trim(),
|
||||||
@@ -139,11 +279,61 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
|
|||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(item.displayName, style: Theme.of(context).textTheme.titleMedium),
|
||||||
item.productName,
|
if (item.categoryPath != null && item.categoryPath!.trim().isNotEmpty) ...[
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
const SizedBox(height: 4),
|
||||||
),
|
Text(item.categoryPath!, style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
],
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
SearchableCategoryField(
|
||||||
|
options: _categoryOptions,
|
||||||
|
value: _selectedCategoryId?.toString(),
|
||||||
|
label: 'Kategori (sökbar)',
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
setState(() {
|
||||||
|
_selectedCategoryId = int.tryParse(value);
|
||||||
|
final current = _selectedProduct();
|
||||||
|
final currentCategoryId = (current?['categoryId'] as num?)?.toInt();
|
||||||
|
if (currentCategoryId != _selectedCategoryId) {
|
||||||
|
_selectedProductId = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _loadingProducts || _saving || _categoryTree.isEmpty
|
||||||
|
? null
|
||||||
|
: _pickCategory,
|
||||||
|
icon: const Icon(Icons.category_outlined),
|
||||||
|
label: const Text('Välj kategori'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: _saving
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
setState(() {
|
||||||
|
_selectedCategoryId = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
label: const Text('Rensa kategori'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
ProductPickerField(
|
||||||
|
products: _productOptions(),
|
||||||
|
value: _selectedProductId,
|
||||||
|
isLoading: _loadingProducts,
|
||||||
|
enabled: !_saving,
|
||||||
|
label: context.l10n.inventoryProductLabel,
|
||||||
|
onChanged: (value) => setState(() => _selectedProductId = value),
|
||||||
|
),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class _InventoryScreenState extends ConsumerState<InventoryScreen> {
|
|||||||
final units = <String>[];
|
final units = <String>[];
|
||||||
final seenUnits = <String>{};
|
final seenUnits = <String>{};
|
||||||
for (final item in selectedItems) {
|
for (final item in selectedItems) {
|
||||||
final unit = (item.unit as String).trim();
|
final unit = item.unit.trim();
|
||||||
final key = unit.toLowerCase();
|
final key = unit.toLowerCase();
|
||||||
if (seenUnits.add(key)) {
|
if (seenUnits.add(key)) {
|
||||||
units.add(unit);
|
units.add(unit);
|
||||||
|
|||||||
Reference in New Issue
Block a user