feat: Implement caching for selectable products and enhance product filtering in admin panels
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:
@@ -7,6 +7,7 @@ import '../../../core/l10n/l10n.dart';
|
||||
import '../../../core/ui/category_then_product_picker.dart';
|
||||
import '../../../core/ui/searchable_category_field.dart';
|
||||
import '../../../core/ui/product_picker_field.dart';
|
||||
import 'admin_form_shared.dart';
|
||||
import '../data/admin_repository.dart';
|
||||
import '../domain/admin_category_node.dart';
|
||||
import '../domain/admin_pantry_item.dart';
|
||||
@@ -30,7 +31,6 @@ class _AdminPantryPanelState extends ConsumerState<AdminPantryPanel> {
|
||||
List<AdminPantryItem> _items = [];
|
||||
List<AdminProduct> _products = [];
|
||||
List<AdminCategoryNode> _categories = [];
|
||||
List<CategorySelectOption> _categoryOptions = [];
|
||||
List<UserAdmin> _users = [];
|
||||
|
||||
@override
|
||||
@@ -48,7 +48,7 @@ class _AdminPantryPanelState extends ConsumerState<AdminPantryPanel> {
|
||||
try {
|
||||
final results = await Future.wait<dynamic>([
|
||||
ref.read(adminRepositoryProvider).listAdminPantry(userId: _selectedUserId),
|
||||
ref.read(adminRepositoryProvider).listGlobalProducts(),
|
||||
ref.read(adminRepositoryProvider).listSelectableProductsForAdmin(),
|
||||
ref.read(adminRepositoryProvider).listCategoryTree(),
|
||||
ref.read(adminRepositoryProvider).listUsers(),
|
||||
]);
|
||||
@@ -57,7 +57,6 @@ class _AdminPantryPanelState extends ConsumerState<AdminPantryPanel> {
|
||||
_items = results[0] as List<AdminPantryItem>;
|
||||
_products = results[1] as List<AdminProduct>;
|
||||
_categories = results[2] as List<AdminCategoryNode>;
|
||||
_categoryOptions = _flattenCategoryOptions(_categories);
|
||||
_users = results[3] as List<UserAdmin>;
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -80,18 +79,8 @@ class _AdminPantryPanelState extends ConsumerState<AdminPantryPanel> {
|
||||
}).toList();
|
||||
}
|
||||
|
||||
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<String> get _locationOptions {
|
||||
return buildLocationOptionsFromValues(_items.map((item) => item.location));
|
||||
}
|
||||
|
||||
Future<void> _addItem() async {
|
||||
@@ -152,6 +141,7 @@ class _AdminPantryPanelState extends ConsumerState<AdminPantryPanel> {
|
||||
users: _users,
|
||||
products: _products,
|
||||
categories: _categories,
|
||||
locationOptions: _locationOptions,
|
||||
initial: initial,
|
||||
initialOwnerUserId: initialOwnerUserId,
|
||||
),
|
||||
@@ -427,11 +417,15 @@ class _AdminPantryPanelState extends ConsumerState<AdminPantryPanel> {
|
||||
itemBuilder: (context, index) {
|
||||
final item = filtered[index];
|
||||
return ListTile(
|
||||
title: Text(item.displayName),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(child: Text(item.displayName)),
|
||||
buildCategoryPathChip(item.categoryPath),
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
'${item.username} (${item.userEmail})'
|
||||
'${item.location == null || item.location!.trim().isEmpty ? '' : ' · ${item.location}'}'
|
||||
'${item.categoryPath == null || item.categoryPath!.trim().isEmpty ? '' : ' · ${item.categoryPath}'}',
|
||||
'${item.location == null || item.location!.trim().isEmpty ? '' : ' · ${item.location}'}',
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -489,6 +483,7 @@ class _PantryFormDialog extends StatefulWidget {
|
||||
final List<UserAdmin> users;
|
||||
final List<AdminProduct> products;
|
||||
final List<AdminCategoryNode> categories;
|
||||
final List<String> locationOptions;
|
||||
final AdminPantryItem? initial;
|
||||
final int? initialOwnerUserId;
|
||||
|
||||
@@ -496,6 +491,7 @@ class _PantryFormDialog extends StatefulWidget {
|
||||
required this.users,
|
||||
required this.products,
|
||||
required this.categories,
|
||||
required this.locationOptions,
|
||||
this.initial,
|
||||
this.initialOwnerUserId,
|
||||
});
|
||||
@@ -505,6 +501,8 @@ class _PantryFormDialog extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PantryFormDialogState extends State<_PantryFormDialog> {
|
||||
static const String _manualLocationValue = '__manual_location__';
|
||||
|
||||
late final TextEditingController _locationController;
|
||||
late final TextEditingController _categorySearchController;
|
||||
late List<CategorySelectOption> _categoryOptions;
|
||||
@@ -514,6 +512,8 @@ class _PantryFormDialogState extends State<_PantryFormDialog> {
|
||||
int? _categoryId;
|
||||
String? _categoryPath;
|
||||
String? _productErrorText;
|
||||
bool _useManualLocation = false;
|
||||
ProductScopeFilter _productScopeFilter = ProductScopeFilter.globalOnly;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -527,6 +527,9 @@ class _PantryFormDialogState extends State<_PantryFormDialog> {
|
||||
_locationController = TextEditingController(text: initial?.location ?? '');
|
||||
_categorySearchController = TextEditingController(text: _categoryPath ?? '');
|
||||
_categoryOptions = _flattenCategoryOptions(widget.categories);
|
||||
final initialLocation = _locationController.text.trim();
|
||||
_useManualLocation = initialLocation.isNotEmpty &&
|
||||
!widget.locationOptions.contains(initialLocation);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -555,18 +558,39 @@ class _PantryFormDialogState extends State<_PantryFormDialog> {
|
||||
for (final product in widget.products) {
|
||||
if (product.id == id) return product;
|
||||
}
|
||||
final initial = widget.initial;
|
||||
if (initial != null && initial.productId == id) {
|
||||
return AdminProduct(
|
||||
id: initial.productId,
|
||||
name: initial.productName,
|
||||
canonicalName: initial.productCanonicalName,
|
||||
ownerId: initial.userId,
|
||||
categoryId: initial.categoryId,
|
||||
categoryPath: initial.categoryPath,
|
||||
status: 'private',
|
||||
);
|
||||
}
|
||||
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();
|
||||
final filtered = filterSelectableAdminProducts(
|
||||
products: widget.products,
|
||||
ownerUserId: _ownerUserId,
|
||||
categoryId: _categoryId,
|
||||
scopeFilter: _productScopeFilter,
|
||||
selectedProduct: _productById(_productId),
|
||||
);
|
||||
return toProductOptions(filtered);
|
||||
}
|
||||
|
||||
String? _locationDropdownValue() {
|
||||
return resolveLocationDropdownValue(
|
||||
useManualLocation: _useManualLocation,
|
||||
currentValue: _locationController.text,
|
||||
options: widget.locationOptions,
|
||||
manualLocationValue: _manualLocationValue,
|
||||
);
|
||||
}
|
||||
|
||||
List<CategorySelectOption> _flattenCategoryOptions(
|
||||
@@ -616,11 +640,25 @@ class _PantryFormDialogState extends State<_PantryFormDialog> {
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) => setState(() => _ownerUserId = value),
|
||||
onChanged: (value) => setState(() {
|
||||
_ownerUserId = value;
|
||||
if (value == null) {
|
||||
_productScopeFilter = ProductScopeFilter.globalOnly;
|
||||
final selected = _productById(_productId);
|
||||
if (selected?.ownerId != null) {
|
||||
_productId = null;
|
||||
}
|
||||
}
|
||||
}),
|
||||
decoration: const InputDecoration(labelText: 'Ägare (användare)'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
] else ...[
|
||||
Text(
|
||||
'Produkt: ${widget.initial!.displayName}',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Ägare: ${widget.initial!.username} (${widget.initial!.userEmail})',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
@@ -673,11 +711,73 @@ class _PantryFormDialogState extends State<_PantryFormDialog> {
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _locationController,
|
||||
if (_ownerUserId != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
SegmentedButton<ProductScopeFilter>(
|
||||
segments: const [
|
||||
ButtonSegment<ProductScopeFilter>(
|
||||
value: ProductScopeFilter.all,
|
||||
label: Text('Alla'),
|
||||
),
|
||||
ButtonSegment<ProductScopeFilter>(
|
||||
value: ProductScopeFilter.globalOnly,
|
||||
label: Text('Globala'),
|
||||
),
|
||||
ButtonSegment<ProductScopeFilter>(
|
||||
value: ProductScopeFilter.privateOnly,
|
||||
label: Text('Privata'),
|
||||
),
|
||||
],
|
||||
selected: {_productScopeFilter},
|
||||
onSelectionChanged: (selection) {
|
||||
if (selection.isEmpty) return;
|
||||
setState(() => _productScopeFilter = selection.first);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
] else ...[
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: _locationDropdownValue(),
|
||||
decoration: const InputDecoration(labelText: 'Plats (valfritt)'),
|
||||
items: [
|
||||
...widget.locationOptions.map(
|
||||
(option) => DropdownMenuItem<String>(
|
||||
value: option,
|
||||
child: Text(option),
|
||||
),
|
||||
),
|
||||
const DropdownMenuItem<String>(
|
||||
value: _manualLocationValue,
|
||||
child: Text('Ange manuellt...'),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value == null) {
|
||||
setState(() {
|
||||
_useManualLocation = false;
|
||||
_locationController.clear();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (value == _manualLocationValue) {
|
||||
setState(() => _useManualLocation = true);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_useManualLocation = false;
|
||||
_locationController.text = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (_useManualLocation) ...[
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _locationController,
|
||||
decoration: const InputDecoration(labelText: 'Plats (manuell)'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -714,4 +814,4 @@ class _PantryFormDialogState extends State<_PantryFormDialog> {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user