feat: Introduce SearchableCategoryField component and integrate it into admin panels for enhanced category selection
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-11 12:20:57 +02:00
parent f132983b75
commit 3ad6cfee50
5 changed files with 233 additions and 103 deletions
@@ -4,6 +4,7 @@ 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/searchable_category_field.dart';
import '../../../core/ui/product_picker_field.dart';
import '../data/admin_repository.dart';
import '../domain/admin_category_node.dart';
@@ -38,6 +39,7 @@ class _AdminInventoryPanelState extends ConsumerState<AdminInventoryPanel> {
List<AdminInventoryItem> _items = [];
List<AdminProduct> _products = [];
List<AdminCategoryNode> _categories = [];
List<CategorySelectOption> _categoryOptions = [];
List<UserAdmin> _users = [];
@override
@@ -67,6 +69,7 @@ class _AdminInventoryPanelState extends ConsumerState<AdminInventoryPanel> {
_items = results[0] as List<AdminInventoryItem>;
_products = results[1] as List<AdminProduct>;
_categories = results[2] as List<AdminCategoryNode>;
_categoryOptions = _flattenCategoryOptions(_categories);
_users = results[3] as List<UserAdmin>;
});
} catch (e) {
@@ -93,6 +96,20 @@ class _AdminInventoryPanelState extends ConsumerState<AdminInventoryPanel> {
_InventorySort.quantityDesc => 'Mängd fallande',
};
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;
}
List<AdminInventoryItem> get _filtered {
final q = _search.trim().toLowerCase();
if (q.isEmpty) return _items;
@@ -803,6 +820,20 @@ class _InventoryFormDialogState extends State<_InventoryFormDialog> {
super.dispose();
}
void _applyCategorySelection(int id, String path) {
setState(() {
_categoryId = id;
_categoryPath = path;
_categorySearchController.text = path;
if (_productId != null) {
final current = _productById(_productId);
if (current?.categoryId != _categoryId) {
_productId = null;
}
}
});
}
AdminProduct? _productById(int? id) {
if (id == null) return null;
for (final product in widget.products) {
@@ -829,16 +860,7 @@ class _InventoryFormDialogState extends State<_InventoryFormDialog> {
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;
}
}
});
_applyCategorySelection(selected.id, selected.path);
}
@override
@@ -888,19 +910,17 @@ class _InventoryFormDialogState extends State<_InventoryFormDialog> {
),
const SizedBox(height: 12),
],
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!,
),
),
SearchableCategoryField(
options: _categoryOptions,
value: _categoryId?.toString(),
label: 'Kategori (sökbar)',
onChanged: (value) {
if (value == null) return;
final label = _categoryOptions
.firstWhere((option) => option.value == value)
.label;
_applyCategorySelection(int.parse(value), label);
},
),
const SizedBox(height: 12),
Row(