feat: enhance product picker and improve error handling in inventory screen
This commit is contained in:
@@ -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(),
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user