feat: implement dropdowns for unit and location selection in inventory forms; add product sorting functionality

This commit is contained in:
Nils-Johan Gynther
2026-04-22 10:04:57 +02:00
parent 296a89b165
commit 655adf66ae
4 changed files with 439 additions and 50 deletions
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import '../../../core/api/api_error_mapper.dart';
import '../../../core/api/api_providers.dart';
import '../../../core/forms/form_options.dart';
import '../../auth/data/auth_providers.dart';
import '../data/inventory_providers.dart';
@@ -135,6 +136,13 @@ class _CreateInventoryScreenState
@override
Widget build(BuildContext context) {
final sortedProducts = [..._products]
..sort((a, b) {
final aName = (a['canonicalName'] ?? a['name'] ?? '').toString();
final bName = (b['canonicalName'] ?? b['name'] ?? '').toString();
return aName.toLowerCase().compareTo(bName.toLowerCase());
});
return Scaffold(
appBar: AppBar(title: const Text('Lagg till inventariepost')),
body: Form(
@@ -142,41 +150,39 @@ class _CreateInventoryScreenState
child: ListView(
padding: const EdgeInsets.all(16),
children: [
Autocomplete<Map<String, dynamic>>(
optionsBuilder: (textEditingValue) {
if (textEditingValue.text.isEmpty) return const [];
final q = textEditingValue.text.toLowerCase();
return _products
.where((p) =>
(p['name'] as String).toLowerCase().contains(q))
.take(10);
},
displayStringForOption: (option) => option['name'] as String,
onSelected: (option) {
setState(() => _selectedProductId = option['id'] as int);
},
fieldViewBuilder:
(context, controller, focusNode, onFieldSubmitted) {
return TextFormField(
controller: controller,
focusNode: focusNode,
decoration: InputDecoration(
labelText: 'Produkt *',
border: const OutlineInputBorder(),
suffixIcon: _loadingProducts
? const Padding(
padding: EdgeInsets.all(12),
child: SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
)
: null,
),
enabled: !_saving,
);
},
DropdownButtonFormField<int>(
value: _selectedProductId,
isExpanded: true,
decoration: InputDecoration(
labelText: 'Produkt *',
border: const OutlineInputBorder(),
suffixIcon: _loadingProducts
? const Padding(
padding: EdgeInsets.all(12),
child: SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
)
: null,
),
items: sortedProducts
.map(
(product) => DropdownMenuItem<int>(
value: product['id'] as int,
child: Text(
((product['canonicalName'] ?? product['name']) as Object)
.toString(),
overflow: TextOverflow.ellipsis,
),
),
)
.toList(),
onChanged: (_loadingProducts || _saving)
? null
: (value) => setState(() => _selectedProductId = value),
validator: (value) => value == null ? 'Valj produkt' : null,
),
const SizedBox(height: 12),
Row(
@@ -205,27 +211,56 @@ class _CreateInventoryScreenState
),
const SizedBox(width: 8),
Expanded(
child: TextFormField(
controller: _unitController,
child: DropdownButtonFormField<String>(
value: _unitController.text.trim().isEmpty
? null
: _unitController.text.trim(),
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Enhet *',
border: OutlineInputBorder(),
),
enabled: !_saving,
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'Ange enhet' : null,
items: unitOptions
.map(
(option) => DropdownMenuItem<String>(
value: option.value,
child: Text(option.label, overflow: TextOverflow.ellipsis),
),
)
.toList(),
onChanged: _saving
? null
: (value) =>
setState(() => _unitController.text = value ?? ''),
validator: (value) =>
(value == null || value.trim().isEmpty) ? 'Ange enhet' : null,
),
),
],
),
const SizedBox(height: 12),
TextFormField(
controller: _locationController,
DropdownButtonFormField<String>(
value: _locationController.text.trim().isEmpty
? null
: _locationController.text.trim(),
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Plats (valfri)',
border: OutlineInputBorder(),
),
enabled: !_saving,
items: inventoryLocationOptions
.map(
(location) => DropdownMenuItem<String>(
value: location,
child: Text(location),
),
)
.toList(),
onChanged: _saving
? null
: (value) => setState(() {
_locationController.text = value ?? '';
}),
),
const SizedBox(height: 12),
TextFormField(
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/api/api_error_mapper.dart';
import '../../../core/forms/form_options.dart';
import '../../../core/ui/async_state_views.dart';
import '../../auth/data/auth_providers.dart';
import '../data/inventory_providers.dart';
@@ -173,13 +174,27 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
),
const SizedBox(width: 8),
Expanded(
child: TextFormField(
controller: _unitController,
child: DropdownButtonFormField<String>(
value: _unitController.text.trim().isEmpty
? null
: _unitController.text.trim(),
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Enhet *',
border: OutlineInputBorder(),
),
enabled: !_saving,
items: unitOptions
.map(
(option) => DropdownMenuItem<String>(
value: option.value,
child: Text(option.label, overflow: TextOverflow.ellipsis),
),
)
.toList(),
onChanged: _saving
? null
: (value) =>
setState(() => _unitController.text = value ?? ''),
validator: (v) => (v == null || v.trim().isEmpty)
? 'Ange enhet'
: null,
@@ -188,13 +203,27 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
],
),
const SizedBox(height: 12),
TextFormField(
controller: _locationController,
DropdownButtonFormField<String>(
value: _locationController.text.trim().isEmpty
? null
: _locationController.text.trim(),
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Plats',
border: OutlineInputBorder(),
),
enabled: !_saving,
items: inventoryLocationOptions
.map(
(location) => DropdownMenuItem<String>(
value: location,
child: Text(location),
),
)
.toList(),
onChanged: _saving
? null
: (value) =>
setState(() => _locationController.text = value ?? ''),
),
const SizedBox(height: 12),
TextFormField(