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.
|
/// Replaces a long [DropdownButtonFormField] when the product list is large.
|
||||||
/// Works both inside and outside a [Form].
|
/// Works both inside and outside a [Form].
|
||||||
class ProductPickerField extends StatelessWidget {
|
class ProductPickerField extends StatelessWidget {
|
||||||
|
static const _clearSelectionToken = '__clear_selection__';
|
||||||
|
|
||||||
final List<ProductOption> products;
|
final List<ProductOption> products;
|
||||||
|
|
||||||
/// Currently selected product id, or null if nothing is selected.
|
/// Currently selected product id, or null if nothing is selected.
|
||||||
@@ -51,7 +53,7 @@ class ProductPickerField extends StatelessWidget {
|
|||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
|
|
||||||
final interactive = enabled && !isLoading;
|
final interactive = enabled && !isLoading && products.isNotEmpty;
|
||||||
|
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
cursor: interactive ? SystemMouseCursors.click : MouseCursor.defer,
|
cursor: interactive ? SystemMouseCursors.click : MouseCursor.defer,
|
||||||
@@ -73,7 +75,11 @@ class ProductPickerField extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: selected == null
|
: selected == null
|
||||||
? const Icon(Icons.search)
|
? Text(
|
||||||
|
products.isEmpty
|
||||||
|
? 'Inga produkter tillgängliga'
|
||||||
|
: 'Tryck för att välja produkt',
|
||||||
|
)
|
||||||
: Text(selected.name),
|
: Text(selected.name),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -81,7 +87,7 @@ class ProductPickerField extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openPicker(BuildContext context) async {
|
Future<void> _openPicker(BuildContext context) async {
|
||||||
final selectedId = await showModalBottomSheet<int?>(
|
final result = await showModalBottomSheet<Object?>(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
useSafeArea: true,
|
useSafeArea: true,
|
||||||
@@ -114,7 +120,8 @@ class ProductPickerField extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () => Navigator.pop(context, null),
|
onPressed: () =>
|
||||||
|
Navigator.pop(context, _clearSelectionToken),
|
||||||
icon: const Icon(Icons.clear),
|
icon: const Icon(Icons.clear),
|
||||||
label: const Text('Rensa'),
|
label: const Text('Rensa'),
|
||||||
),
|
),
|
||||||
@@ -169,10 +176,15 @@ class ProductPickerField extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (selectedId == null) {
|
if (result == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result == _clearSelectionToken) {
|
||||||
onChanged?.call(null);
|
onChanged?.call(null);
|
||||||
return;
|
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 token = await ref.read(authStateProvider.future);
|
||||||
final api = ref.read(apiClientProvider);
|
final api = ref.read(apiClientProvider);
|
||||||
final data = await api.getJson(ProductApiPaths.list, token: token);
|
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) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_products = (data as List<dynamic>)
|
_products = list
|
||||||
.map((e) => e as Map<String, dynamic>)
|
.map((e) => e as Map<String, dynamic>)
|
||||||
.toList();
|
.toList();
|
||||||
_loadingProducts = false;
|
_loadingProducts = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (e) {
|
||||||
if (mounted) setState(() => _loadingProducts = false);
|
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
|
final productOptions = sortedProducts
|
||||||
.map(
|
.map(
|
||||||
(p) => (
|
(p) => (
|
||||||
id: p['id'] as int,
|
id: (p['id'] as num).toInt(),
|
||||||
name: (p['canonicalName'] ?? p['name'] ?? '').toString(),
|
name: (p['canonicalName'] ?? p['name'] ?? '').toString(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user