feat: enhance product picker and improve error handling in inventory screen

This commit is contained in:
Nils-Johan Gynther
2026-04-25 07:47:35 +02:00
parent 5a85bd4526
commit 53afcc98a9
2 changed files with 31 additions and 9 deletions
+18 -6
View File
@@ -11,6 +11,8 @@ typedef ProductOption = ({int id, String name});
/// Replaces a long [DropdownButtonFormField] when the product list is large.
/// Works both inside and outside a [Form].
class ProductPickerField extends StatelessWidget {
static const _clearSelectionToken = '__clear_selection__';
final List<ProductOption> products;
/// Currently selected product id, or null if nothing is selected.
@@ -51,7 +53,7 @@ class ProductPickerField extends StatelessWidget {
orElse: () => null,
);
final interactive = enabled && !isLoading;
final interactive = enabled && !isLoading && products.isNotEmpty;
return MouseRegion(
cursor: interactive ? SystemMouseCursors.click : MouseCursor.defer,
@@ -73,7 +75,11 @@ class ProductPickerField extends StatelessWidget {
),
)
: selected == null
? const Icon(Icons.search)
? Text(
products.isEmpty
? 'Inga produkter tillgängliga'
: 'Tryck för att välja produkt',
)
: Text(selected.name),
),
),
@@ -81,7 +87,7 @@ class ProductPickerField extends StatelessWidget {
}
Future<void> _openPicker(BuildContext context) async {
final selectedId = await showModalBottomSheet<int?>(
final result = await showModalBottomSheet<Object?>(
context: context,
isScrollControlled: true,
useSafeArea: true,
@@ -114,7 +120,8 @@ class ProductPickerField extends StatelessWidget {
),
),
TextButton.icon(
onPressed: () => Navigator.pop(context, null),
onPressed: () =>
Navigator.pop(context, _clearSelectionToken),
icon: const Icon(Icons.clear),
label: const Text('Rensa'),
),
@@ -169,10 +176,15 @@ class ProductPickerField extends StatelessWidget {
);
if (!context.mounted) return;
if (selectedId == null) {
if (result == null) {
return;
}
if (result == _clearSelectionToken) {
onChanged?.call(null);
return;
}
onChanged?.call(selectedId);
if (result is int) {
onChanged?.call(result);
}
}
}
@@ -57,16 +57,26 @@ class _CreateInventoryScreenState
final token = await ref.read(authStateProvider.future);
final api = ref.read(apiClientProvider);
final data = await api.getJson(ProductApiPaths.list, token: token);
final list = data is List<dynamic>
? data
: (data is Map<String, dynamic> && data['items'] is List<dynamic>)
? data['items'] as List<dynamic>
: const <dynamic>[];
if (mounted) {
setState(() {
_products = (data as List<dynamic>)
_products = list
.map((e) => e as Map<String, dynamic>)
.toList();
_loadingProducts = false;
});
}
} catch (_) {
} catch (e) {
if (mounted) setState(() => _loadingProducts = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(e, context))),
);
}
}
}
@@ -147,7 +157,7 @@ class _CreateInventoryScreenState
final productOptions = sortedProducts
.map(
(p) => (
id: p['id'] as int,
id: (p['id'] as num).toInt(),
name: (p['canonicalName'] ?? p['name'] ?? '').toString(),
),
)