mirror of
				https://github.com/inventree/inventree-app.git
				synced 2025-10-31 21:35:42 +00:00 
			
		
		
		
	Stock display (#379)
* Display stock quantity more prominently * Cleanup search widget * Update for stock_detail widget * More tweaks * Change bottom bar icon * Display boolean parameters appropriately * Adds ability to edit part parameters * Bump icon size a bit * Improvements to filter options - Allow filtering by "option" type - To start with, filter stock by status code * Remove debug message * Remove getTriState method - No longer needed - Remove associated unit tests * Adjust filters based on server API version * Muted colors
This commit is contained in:
		
							
								
								
									
										15
									
								
								lib/api.dart
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								lib/api.dart
									
									
									
									
									
								
							| @@ -1332,7 +1332,20 @@ class InvenTreeAPI { | ||||
|  | ||||
|   static String get staticThumb => "/static/img/blank_image.thumbnail.png"; | ||||
|  | ||||
|   CachedNetworkImage getThumbnail(String imageUrl, {double size = 40}) => getImage(imageUrl, width: size, height: size); | ||||
|   CachedNetworkImage? getThumbnail(String imageUrl, {double size = 40, bool hideIfNull = false}) { | ||||
|  | ||||
|     if (hideIfNull) { | ||||
|       if (imageUrl.isEmpty) { | ||||
|         return null; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return getImage( | ||||
|         imageUrl, | ||||
|         width: size, | ||||
|         height: size | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Load image from the InvenTree server, | ||||
|   | ||||
| @@ -275,6 +275,7 @@ class APIFormField { | ||||
|  | ||||
|   // Construct a widget for this input | ||||
|   Widget constructField(BuildContext context) { | ||||
|  | ||||
|     switch (type) { | ||||
|       case "string": | ||||
|       case "url": | ||||
| @@ -696,6 +697,14 @@ class APIFormField { | ||||
|   // Construct a string input element | ||||
|   Widget _constructString() { | ||||
|  | ||||
|     if (readOnly) { | ||||
|       return ListTile( | ||||
|         title: Text(label), | ||||
|         subtitle: Text(helpText), | ||||
|         trailing: Text(value.toString()), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return TextFormField( | ||||
|       decoration: InputDecoration( | ||||
|         labelText: required ? label + "*" : label, | ||||
| @@ -724,12 +733,21 @@ class APIFormField { | ||||
|   // Construct a boolean input element | ||||
|   Widget _constructBoolean() { | ||||
|  | ||||
|     bool? initial_value; | ||||
|  | ||||
|     if (value is bool || value == null) { | ||||
|       initial_value = value as bool?; | ||||
|     } else { | ||||
|       String vs = value.toString().toLowerCase(); | ||||
|       initial_value = ["1", "true", "yes"].contains(vs); | ||||
|     } | ||||
|  | ||||
|     return CheckBoxField( | ||||
|       label: label, | ||||
|       labelStyle: _labelStyle(), | ||||
|       helperText: helpText, | ||||
|       helperStyle: _helperStyle(), | ||||
|       initial: value as bool?, | ||||
|       initial: initial_value, | ||||
|       tristate: (getParameter("tristate") ?? false) as bool, | ||||
|       onSaved: (val) { | ||||
|         data["value"] = val; | ||||
| @@ -1262,6 +1280,10 @@ class _APIFormWidgetState extends State<APIFormWidget> { | ||||
|  | ||||
|     for (var field in widget.fields) { | ||||
|  | ||||
|       if (field.readOnly) { | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       if (field.isSimple) { | ||||
|         // Simple top-level field data | ||||
|         data[field.name] = field.data["value"]; | ||||
|   | ||||
| @@ -16,6 +16,6 @@ Color get COLOR_ACTION { | ||||
| } | ||||
|  | ||||
| const Color COLOR_WARNING = Color.fromRGBO(250, 150, 50, 1); | ||||
| const Color COLOR_DANGER = Color.fromRGBO(250, 50, 50, 1); | ||||
| const Color COLOR_SUCCESS = Color.fromRGBO(50, 250, 50, 1); | ||||
| const Color COLOR_PROGRESS = Color.fromRGBO(50, 50, 250, 1); | ||||
| const Color COLOR_DANGER = Color.fromRGBO(200, 50, 75, 1); | ||||
| const Color COLOR_SUCCESS = Color.fromRGBO(100, 200, 75, 1); | ||||
| const Color COLOR_PROGRESS = Color.fromRGBO(50, 100, 200, 1); | ||||
| @@ -133,12 +133,29 @@ class InvenTreePartParameter extends InvenTreeModel { | ||||
|   @override | ||||
|   String get URL => "part/parameter/"; | ||||
|  | ||||
|   @override | ||||
|   List<String> get rolesRequired => ["part"]; | ||||
|  | ||||
|   @override | ||||
|   InvenTreeModel createFromJson(Map<String, dynamic> json) => InvenTreePartParameter.fromJson(json); | ||||
|  | ||||
|   @override | ||||
|   Map<String, dynamic> formFields() { | ||||
|     return {}; | ||||
|  | ||||
|     Map<String, dynamic> fields = { | ||||
|       "header": { | ||||
|         "type": "string", | ||||
|         "read_only": true, | ||||
|         "label": name, | ||||
|         "help_text": description, | ||||
|         "value": "", | ||||
|       }, | ||||
|       "data": { | ||||
|         "type": "string", | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     return fields; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -160,7 +177,11 @@ class InvenTreePartParameter extends InvenTreeModel { | ||||
|     return v; | ||||
|   } | ||||
|  | ||||
|   bool get as_bool => value.toLowerCase() == "true"; | ||||
|  | ||||
|   String get units => getString("units", subKey: "template_detail"); | ||||
|    | ||||
|   bool get is_checkbox => getBool("checkbox", subKey: "template_detail", backup: false); | ||||
| } | ||||
|  | ||||
| /* | ||||
|   | ||||
| @@ -23,6 +23,26 @@ class InvenTreeStatusCode { | ||||
|   // Internal status code data loaded from server | ||||
|   Map<String, dynamic> data = {}; | ||||
|  | ||||
|   /* | ||||
|    * Construct a list of "choices" suitable for a form | ||||
|    */ | ||||
|   List<dynamic> get choices { | ||||
|     List<dynamic> _choices = []; | ||||
|  | ||||
|     for (String key in data.keys) { | ||||
|       dynamic _entry = data[key]; | ||||
|  | ||||
|       if (_entry is Map<String, dynamic>) { | ||||
|         _choices.add({ | ||||
|           "value": _entry["key"], | ||||
|           "display_name": _entry["label"] | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return _choices; | ||||
|   } | ||||
|  | ||||
|   // Load status code information from the server | ||||
|   Future<void> load({bool forceReload = false}) async { | ||||
|  | ||||
|   | ||||
| @@ -399,7 +399,7 @@ class InvenTreeStockItem extends InvenTreeModel { | ||||
|  | ||||
|     double get quantity => getDouble("quantity"); | ||||
|  | ||||
|     String quantityString({bool includeUnits = false}){ | ||||
|     String quantityString({bool includeUnits = true}){ | ||||
|  | ||||
|       String q = ""; | ||||
|  | ||||
| @@ -467,7 +467,13 @@ class InvenTreeStockItem extends InvenTreeModel { | ||||
|       if (serialNumber.isNotEmpty) { | ||||
|         return "SN: $serialNumber"; | ||||
|       } else { | ||||
|         return simpleNumberString(quantity); | ||||
|         String q = simpleNumberString(quantity); | ||||
|  | ||||
|         if (units.isNotEmpty) { | ||||
|           q += " ${units}"; | ||||
|         } | ||||
|  | ||||
|         return q; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -278,6 +278,9 @@ | ||||
|   "editNotes": "Edit Notes", | ||||
|   "@editNotes": {}, | ||||
|  | ||||
|   "editParameter": "Edit Parameter", | ||||
|   "@editParameter": {}, | ||||
|  | ||||
|   "editPart": "Edit Part", | ||||
|   "@editPart": { | ||||
|     "description": "edit part" | ||||
|   | ||||
| @@ -126,7 +126,12 @@ class InvenTreeSettingsManager { | ||||
|  | ||||
|   Future<dynamic> getValue(String key, dynamic backup) async { | ||||
|  | ||||
|     final value = await store.record(key).get(await _db); | ||||
|     dynamic value = await store.record(key).get(await _db); | ||||
|  | ||||
|     // Retrieve value | ||||
|     if (value == "__null__") { | ||||
|       value = null; | ||||
|     } | ||||
|  | ||||
|     if (value == null) { | ||||
|       return backup; | ||||
| @@ -148,32 +153,11 @@ class InvenTreeSettingsManager { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Load a tristate (true / false / null) setting | ||||
|   Future<bool?> getTriState(String key, dynamic backup) async { | ||||
|     final dynamic value = await getValue(key, backup); | ||||
|  | ||||
|     if (value == null) { | ||||
|       return null; | ||||
|     } else if (value is bool) { | ||||
|       return value; | ||||
|     } else { | ||||
|       String s = value.toString().toLowerCase(); | ||||
|  | ||||
|       if (s.contains("t")) { | ||||
|         return true; | ||||
|       } else if (s.contains("f")) { | ||||
|         return false; | ||||
|       } else { | ||||
|         return null; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Store a key:value pair in the database | ||||
|   Future<void> setValue(String key, dynamic value) async { | ||||
|  | ||||
|     // Encode null values as strings | ||||
|     value ??= "null"; | ||||
|     value ??= "__null__"; | ||||
|  | ||||
|     await store.record(key).put(await _db, value); | ||||
|   } | ||||
|   | ||||
| @@ -154,8 +154,8 @@ class CheckBoxField extends FormField<bool> { | ||||
|         onSaved: onSaved, | ||||
|         initialValue: initial, | ||||
|         builder: (FormFieldState<bool> state) { | ||||
|  | ||||
|           return CheckboxListTile( | ||||
|             //dense: state.hasError, | ||||
|             title: label != null ? Text(label, style: labelStyle) : null, | ||||
|             value: state.value, | ||||
|             tristate: tristate, | ||||
|   | ||||
| @@ -38,44 +38,39 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta | ||||
|   // Override in implementing class | ||||
|   String get prefix => "prefix_"; | ||||
|  | ||||
|   // Return a map of boolean filtering options available for this list | ||||
|   // Should be overridden by an implementing subclass | ||||
|   Map<String, Map<String, dynamic>> get filterOptions => {}; | ||||
|  | ||||
|   // Return the boolean value of a particular boolean filter | ||||
|   Future<bool?> getBooleanFilterValue(String key) async { | ||||
|     key = "${prefix}bool_${key}"; | ||||
|   Future<dynamic> getFilterValue(String key) async { | ||||
|     key = "${prefix}filter_${key}"; | ||||
|  | ||||
|     Map<String, dynamic> opts = filterOptions[key] ?? {}; | ||||
|     dynamic backup = opts["default"]; | ||||
|     final result = await InvenTreeSettingsManager().getValue(key, backup); | ||||
|  | ||||
|     bool? backup; | ||||
|     dynamic v = opts["default"]; | ||||
|  | ||||
|     if (v is bool) { | ||||
|       backup = v; | ||||
|     } | ||||
|  | ||||
|     final result = await InvenTreeSettingsManager().getTriState(key, backup); | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   // Set the boolean value of a particular boolean filter | ||||
|   Future<void> setBooleanFilterValue(String key, bool? value) async { | ||||
|     key = "${prefix}bool_${key}"; | ||||
|   Future<void> setFilterValue(String key, dynamic value) async { | ||||
|     key = "${prefix}filter_${key}"; | ||||
|     await InvenTreeSettingsManager().setValue(key, value); | ||||
|   } | ||||
|  | ||||
|   // Construct the boolean filter options for this list | ||||
|   Future<Map<String, String>> constructBooleanFilters() async { | ||||
|   Future<Map<String, String>> constructFilters() async { | ||||
|  | ||||
|     Map<String, String> f = {}; | ||||
|  | ||||
|     for (String k in filterOptions.keys) { | ||||
|       bool? value = await getBooleanFilterValue(k); | ||||
|       dynamic value = await getFilterValue(k); | ||||
|  | ||||
|       if (value is bool) { | ||||
|         f[k] = value ? "true" : "false"; | ||||
|       // Skip null values | ||||
|       if (value == null) { | ||||
|         continue; | ||||
|       } | ||||
|       f[k] = value.toString(); | ||||
|     } | ||||
|  | ||||
|     return f; | ||||
| @@ -164,7 +159,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     // Add in boolean filter options | ||||
|     // Add in selected filter options | ||||
|     for (String key in filterOptions.keys) { | ||||
|       Map<String, dynamic> opts = filterOptions[key] ?? {}; | ||||
|  | ||||
| @@ -172,17 +167,18 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta | ||||
|       String label = (opts["label"] ?? key) as String; | ||||
|       String? help_text = opts["help_text"] as String?; | ||||
|  | ||||
|       List<dynamic> choices = (opts["choices"] ?? []) as List<dynamic>; | ||||
|  | ||||
|       bool tristate = (opts["tristate"] ?? true) as bool; | ||||
|  | ||||
|       bool? v = await getBooleanFilterValue(key); | ||||
|       dynamic v = await getFilterValue(key); | ||||
|  | ||||
|       // Prevent null value if not tristate | ||||
|       if (!tristate && v == null) { | ||||
|         v = false; | ||||
|       } | ||||
|  | ||||
|       // Add in the particular field | ||||
|       fields[key] = { | ||||
|       Map<String, dynamic> filter = { | ||||
|         "type": "boolean", | ||||
|         "display_name": label, | ||||
|         "label": label, | ||||
| @@ -190,6 +186,16 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta | ||||
|         "value": v, | ||||
|         "tristate": (opts["tristate"] ?? true) as bool, | ||||
|       }; | ||||
|  | ||||
|       if (choices.isNotEmpty) { | ||||
|         // Configure as a choice input | ||||
|         filter["type"] = "choice"; | ||||
|         filter["choices"] = choices; | ||||
|  | ||||
|         filter.remove("tristate"); | ||||
|       } | ||||
|  | ||||
|       fields[key] = filter; | ||||
|     } | ||||
|  | ||||
|     // Launch an interactive form for the user to select options | ||||
| @@ -211,16 +217,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta | ||||
|  | ||||
|         // Save boolean fields | ||||
|         for (String key in filterOptions.keys) { | ||||
|  | ||||
|           bool? v; | ||||
|  | ||||
|           dynamic value = data[key]; | ||||
|  | ||||
|           if (value is bool) { | ||||
|             v = value; | ||||
|           } | ||||
|  | ||||
|           await setBooleanFilterValue(key, v); | ||||
|           await setFilterValue(key, data[key]); | ||||
|         } | ||||
|  | ||||
|         // Refresh data from the server | ||||
| @@ -293,7 +290,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta | ||||
|         params["ordering"] = o; | ||||
|       } | ||||
|  | ||||
|       Map<String, String> f = await constructBooleanFilters(); | ||||
|       Map<String, String> f = await constructFilters(); | ||||
|  | ||||
|       if (f.isNotEmpty) { | ||||
|         params.addAll(f); | ||||
| @@ -348,6 +345,10 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta | ||||
|   void updateSearchTerm() { | ||||
|     searchTerm = searchController.text; | ||||
|     _pagingController.refresh(); | ||||
|  | ||||
|     if (mounted) { | ||||
|       setState(() {}); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Function to construct a single paginated item | ||||
| @@ -409,19 +410,19 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta | ||||
|    */ | ||||
|   Widget buildSearchInput(BuildContext context) { | ||||
|     return ListTile( | ||||
|       trailing: orderingOptions.isEmpty ? null : GestureDetector( | ||||
|         child: FaIcon(FontAwesomeIcons.sort, color: COLOR_ACTION), | ||||
|       leading: orderingOptions.isEmpty ? null : GestureDetector( | ||||
|         child: Icon(Icons.filter_list, color: COLOR_ACTION, size: 32), | ||||
|         onTap: () async { | ||||
|           _saveOrderingOptions(context); | ||||
|         }, | ||||
|       ), | ||||
|       leading: GestureDetector( | ||||
|       trailing: GestureDetector( | ||||
|         child: FaIcon( | ||||
|           searchController.text.isEmpty ? FontAwesomeIcons.magnifyingGlass : FontAwesomeIcons.deleteLeft, | ||||
|           color: searchController.text.isNotEmpty ? COLOR_DANGER : null, | ||||
|           color: searchController.text.isNotEmpty ? COLOR_DANGER : COLOR_ACTION, | ||||
|         ), | ||||
|         onTap: () { | ||||
|           if (searchController.text.isEmpty) { | ||||
|           if (searchController.text.isNotEmpty) { | ||||
|             searchController.clear(); | ||||
|           } | ||||
|           updateSearchTerm(); | ||||
|   | ||||
| @@ -228,20 +228,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /* | ||||
|    * Toggle the "star" status of this paricular part | ||||
|    */ | ||||
|   Future <void> _toggleStar(BuildContext context) async { | ||||
|  | ||||
|     if (InvenTreePart().canView) { | ||||
|       showLoadingOverlay(context); | ||||
|       await part.update(values: {"starred": "${!part.starred}"}); | ||||
|       hideLoadingOverlay(); | ||||
|       refresh(context); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _editPartDialog(BuildContext context) { | ||||
|  | ||||
|     part.editForm( | ||||
| @@ -259,13 +245,11 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> { | ||||
|         child: ListTile( | ||||
|           title: Text("${part.fullname}"), | ||||
|           subtitle: Text("${part.description}"), | ||||
|           trailing: IconButton( | ||||
|             icon: FaIcon(part.starred ? FontAwesomeIcons.solidStar : FontAwesomeIcons.star, | ||||
|               color: part.starred ? Colors.yellowAccent : null, | ||||
|             ), | ||||
|             onPressed: () { | ||||
|               _toggleStar(context); | ||||
|             }, | ||||
|           trailing: Text( | ||||
|             part.stockString(), | ||||
|             style: TextStyle( | ||||
|               fontSize: 20, | ||||
|             ) | ||||
|           ), | ||||
|           leading: GestureDetector( | ||||
|             child: api.getImage(part.thumbnail), | ||||
|   | ||||
| @@ -133,7 +133,13 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> { | ||||
|     return ListTile( | ||||
|       title: Text(part.fullname), | ||||
|       subtitle: Text(part.description), | ||||
|       trailing: Text(part.stockString()), | ||||
|       trailing: Text( | ||||
|         part.stockString(), | ||||
|         style: TextStyle( | ||||
|           fontSize: 16, | ||||
|           fontWeight: FontWeight.bold | ||||
|         ) | ||||
|       ), | ||||
|       leading: InvenTreeAPI().getThumbnail(part.thumbnail), | ||||
|       onTap: () { | ||||
|         Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import "package:inventree/inventree/model.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
| import "package:inventree/inventree/part.dart"; | ||||
| import "package:inventree/widget/paginator.dart"; | ||||
| import "package:inventree/widget/progress.dart"; | ||||
| import "package:inventree/widget/refreshable_state.dart"; | ||||
|  | ||||
| /* | ||||
| @@ -75,7 +76,7 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi | ||||
|  | ||||
|   @override | ||||
|   Map<String, String> get orderingOptions => { | ||||
|     // TODO | ||||
|  | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
| @@ -91,6 +92,22 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi | ||||
|     return page; | ||||
|   } | ||||
|  | ||||
|   Future<void> editParameter(InvenTreePartParameter parameter) async { | ||||
|  | ||||
|     // Checkbox values are handled separately | ||||
|     if (parameter.is_checkbox) { | ||||
|       return; | ||||
|     } else { | ||||
|       parameter.editForm( | ||||
|           context, | ||||
|           L10().editParameter, | ||||
|           onSuccess: (data) async { | ||||
|             updateSearchTerm(); | ||||
|           } | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget buildItem(BuildContext context, InvenTreeModel model) { | ||||
|  | ||||
| @@ -99,7 +116,28 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi | ||||
|     return ListTile( | ||||
|       title: Text(parameter.name), | ||||
|       subtitle: Text(parameter.description), | ||||
|       trailing: Text(parameter.valueString), | ||||
|       trailing: parameter.is_checkbox | ||||
|         ? Switch( | ||||
|           value: parameter.as_bool, | ||||
|           onChanged: (bool value) { | ||||
|             if (parameter.canEdit) { | ||||
|               showLoadingOverlay(context); | ||||
|               parameter.update( | ||||
|                 values: { | ||||
|                   "data": value.toString() | ||||
|                 } | ||||
|               ).then((value) async{ | ||||
|                 hideLoadingOverlay(); | ||||
|                 updateSearchTerm(); | ||||
|               }); | ||||
|             } | ||||
|           }, | ||||
|       ) : Text(parameter.valueString), | ||||
|       onTap: parameter.is_checkbox ? null : () async { | ||||
|         if (parameter.canEdit) { | ||||
|           editParameter(parameter); | ||||
|         } | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -71,7 +71,7 @@ mixin BaseWidgetProperties { | ||||
|    */ | ||||
|   BottomAppBar? buildBottomAppBar(BuildContext context, GlobalKey<ScaffoldState> key) { | ||||
|  | ||||
|     const double iconSize = 32; | ||||
|     const double iconSize = 40; | ||||
|  | ||||
|     List<Widget> icons = [ | ||||
|       IconButton( | ||||
| @@ -98,7 +98,7 @@ mixin BaseWidgetProperties { | ||||
|         }, | ||||
|       ), | ||||
|       IconButton( | ||||
|         icon: Icon(Icons.qr_code_scanner, color: COLOR_ACTION), | ||||
|         icon: Icon(Icons.barcode_reader, color: COLOR_ACTION), | ||||
|         iconSize: iconSize, | ||||
|         onPressed: () { | ||||
|           if (InvenTreeAPI().checkConnection()) { | ||||
|   | ||||
| @@ -577,8 +577,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | ||||
|         subtitle: Text("${widget.item.partDescription}"), | ||||
|         leading: InvenTreeAPI().getThumbnail(widget.item.partImage), | ||||
|         trailing: Text( | ||||
|           api.StockStatus.label(widget.item.status), | ||||
|           widget.item.quantityString(), | ||||
|           style: TextStyle( | ||||
|             fontSize: 20, | ||||
|             color: api.StockStatus.color(widget.item.status), | ||||
|           ) | ||||
|         ), | ||||
| @@ -615,6 +616,41 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | ||||
|       return tiles; | ||||
|     } | ||||
|  | ||||
|     // Location information | ||||
|     if ((widget.item.locationId > 0) && (widget.item.locationName.isNotEmpty)) { | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           title: Text(L10().stockLocation), | ||||
|           subtitle: Text("${widget.item.locationPathString}"), | ||||
|           leading: FaIcon( | ||||
|             FontAwesomeIcons.locationDot, | ||||
|             color: COLOR_ACTION, | ||||
|           ), | ||||
|           onTap: () async { | ||||
|             if (widget.item.locationId > 0) { | ||||
|  | ||||
|               showLoadingOverlay(context); | ||||
|               var loc = await InvenTreeStockLocation().get(widget.item.locationId); | ||||
|               hideLoadingOverlay(); | ||||
|  | ||||
|               if (loc is InvenTreeStockLocation) { | ||||
|                 Navigator.push(context, MaterialPageRoute( | ||||
|                     builder: (context) => LocationDisplayWidget(loc))); | ||||
|               } | ||||
|             } | ||||
|           }, | ||||
|         ), | ||||
|       ); | ||||
|     } else { | ||||
|       tiles.add( | ||||
|           ListTile( | ||||
|             title: Text(L10().stockLocation), | ||||
|             leading: FaIcon(FontAwesomeIcons.locationDot), | ||||
|             subtitle: Text(L10().locationNotSet), | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     // Quantity information | ||||
|     if (widget.item.isSerialized()) { | ||||
|       tiles.add( | ||||
| @@ -634,40 +670,19 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     // Location information | ||||
|     if ((widget.item.locationId > 0) && (widget.item.locationName.isNotEmpty)) { | ||||
|       tiles.add( | ||||
|           ListTile( | ||||
|             title: Text(L10().stockLocation), | ||||
|             subtitle: Text("${widget.item.locationPathString}"), | ||||
|             leading: FaIcon( | ||||
|               FontAwesomeIcons.locationDot, | ||||
|               color: COLOR_ACTION, | ||||
|             ), | ||||
|             onTap: () async { | ||||
|               if (widget.item.locationId > 0) { | ||||
|  | ||||
|                 showLoadingOverlay(context); | ||||
|                 var loc = await InvenTreeStockLocation().get(widget.item.locationId); | ||||
|                 hideLoadingOverlay(); | ||||
|  | ||||
|                 if (loc is InvenTreeStockLocation) { | ||||
|                   Navigator.push(context, MaterialPageRoute( | ||||
|                       builder: (context) => LocationDisplayWidget(loc))); | ||||
|                 } | ||||
|               } | ||||
|             }, | ||||
|           ), | ||||
|       ); | ||||
|     } else { | ||||
|       tiles.add( | ||||
|           ListTile( | ||||
|             title: Text(L10().stockLocation), | ||||
|             leading: FaIcon(FontAwesomeIcons.locationDot), | ||||
|             subtitle: Text(L10().locationNotSet), | ||||
|     // Stock item status information | ||||
|     tiles.add( | ||||
|       ListTile( | ||||
|         title: Text(L10().status), | ||||
|         leading: FaIcon(FontAwesomeIcons.circleInfo), | ||||
|         trailing: Text( | ||||
|           api.StockStatus.label(widget.item.status), | ||||
|           style: TextStyle( | ||||
|             color: api.StockStatus.color(widget.item.status), | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|         ) | ||||
|       ) | ||||
|     ); | ||||
|  | ||||
|     // Supplier part information (if available) | ||||
|     if (widget.item.supplierPartId > 0) { | ||||
| @@ -676,7 +691,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | ||||
|           title: Text(L10().supplierPart), | ||||
|           subtitle: Text(widget.item.supplierSKU), | ||||
|           leading: FaIcon(FontAwesomeIcons.building, color: COLOR_ACTION), | ||||
|           trailing: InvenTreeAPI().getThumbnail(widget.item.supplierImage), | ||||
|           trailing: InvenTreeAPI().getThumbnail(widget.item.supplierImage, hideIfNull: true), | ||||
|           onTap: () async { | ||||
|             showLoadingOverlay(context); | ||||
|             var sp = await InvenTreeSupplierPart().get( | ||||
|   | ||||
| @@ -79,24 +79,37 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Map<String, Map<String, dynamic>> get filterOptions => { | ||||
|     "in_stock": { | ||||
|       "default": true, | ||||
|       "label": L10().filterInStock, | ||||
|       "help_text": L10().filterInStockDetail, | ||||
|       "tristate": true, | ||||
|     }, | ||||
|     "cascade": { | ||||
|       "default": false, | ||||
|       "label": L10().includeSublocations, | ||||
|       "help_text": L10().includeSublocationsDetail, | ||||
|       "tristate": false, | ||||
|     }, | ||||
|     "serialized": { | ||||
|       "label": L10().filterSerialized, | ||||
|       "help_text": L10().filterSerializedDetail, | ||||
|   Map<String, Map<String, dynamic>> get filterOptions { | ||||
|     Map<String, Map<String, dynamic>> filters = { | ||||
|       "in_stock": { | ||||
|         "default": true, | ||||
|         "label": L10().filterInStock, | ||||
|         "help_text": L10().filterInStockDetail, | ||||
|         "tristate": true, | ||||
|       }, | ||||
|       "cascade": { | ||||
|         "default": false, | ||||
|         "label": L10().includeSublocations, | ||||
|         "help_text": L10().includeSublocationsDetail, | ||||
|         "tristate": false, | ||||
|       }, | ||||
|       "serialized": { | ||||
|         "label": L10().filterSerialized, | ||||
|         "help_text": L10().filterSerializedDetail, | ||||
|       }, | ||||
|       "status": { | ||||
|         "label": L10().status, | ||||
|         "help_text": L10().statusCode, | ||||
|         "choices": InvenTreeAPI().StockStatus.choices, | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     if (!InvenTreeAPI().supportsStatusLabelEndpoints) { | ||||
|       filters.remove("status"); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|     return filters; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { | ||||
| @@ -125,6 +138,7 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt | ||||
|       trailing: Text("${item.displayQuantity}", | ||||
|         style: TextStyle( | ||||
|           fontWeight: FontWeight.bold, | ||||
|           fontSize: 16, | ||||
|           color: InvenTreeAPI().StockStatus.color(item.status), | ||||
|         ), | ||||
|       ), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user