diff --git a/lib/api.dart b/lib/api.dart index a02303f3..885fd145 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -72,8 +72,46 @@ class InvenTreeAPI { // Authentication token (initially empty, must be requested) String _token = ""; + bool isConnected() { + return _token.isNotEmpty; + } + + /* + * Check server connection and display messages if not connected. + * Useful as a precursor check before performing operations. + */ + bool checkConnection(BuildContext context) { + + // Firstly, is the server connected? + if (!isConnected()) { + showDialog( + context: context, + child: new SimpleDialog( + title: new Text("Not Connected"), + children: [ + ListTile( + title: Text("Server not connected"), + ) + ] + ) + ); + + return false; + } + + // Is the server version too old? + // TODO + + // Finally + return true; + + } + + // Server instance information + String instance = ''; + // Server version information - String _version; + String _version = ''; // Getter for server version information String get version => _version; @@ -174,6 +212,9 @@ class InvenTreeAPI { _version = data["version"]; + // Record the instance name of the server + instance = data['instance'] ?? ''; + // Request token from the server if we do not already have one if (_token.isNotEmpty) { print("Already have token - $_token"); diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 40ef716a..38680ba7 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -74,8 +74,13 @@ class InvenTreeModel { } */ + Map defaultListFilters() { return Map(); } + + // A map of "default" headers to use when performing a GET request + Map defaultGetFilters() { return Map(); } + // Return the detail view for the associated pk - Future get(int pk) async { + Future get(int pk, {Map filters}) async { // TODO - Add "timeout" // TODO - Add error catching @@ -86,7 +91,18 @@ class InvenTreeModel { addr += "/"; } - var response = await InvenTreeAPI().get(addr); + var params = defaultGetFilters(); + + if (filters != null) { + // Override any default values + for (String key in filters.keys) { + params[key] = filters[key]; + } + } + + print("GET: $addr ${params.toString()}"); + + var response = await InvenTreeAPI().get(addr, params: params); if (response.statusCode != 200) { print("Error retrieving data"); @@ -105,12 +121,20 @@ class InvenTreeModel { filters = {}; } - print("Listing endpoint: $URL"); + var params = defaultListFilters(); + + if (filters != null) { + for (String key in filters.keys) { + params[key] = filters[key]; + } + } + + print("LIST: $URL ${params.toString()}"); // TODO - Add "timeout" // TODO - Add error catching - var response = await InvenTreeAPI().get(URL, params:filters); + var response = await InvenTreeAPI().get(URL, params:params); // A list of "InvenTreeModel" items List results = new List(); diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index 6b09842e..3283a19c 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -12,6 +12,15 @@ class InvenTreePartCategory extends InvenTreeModel { @override String URL = "part/category/"; + @override + Map defaultListFilters() { + var filters = new Map(); + + filters["active"] = "true"; + + return filters; + } + String get pathstring => jsondata['pathstring'] ?? ''; String get parentpathstring { diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index 7bfe2bd2..60aaaecd 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'model.dart'; import 'package:InvenTree/api.dart'; @@ -6,15 +8,72 @@ class InvenTreeStockItem extends InvenTreeModel { @override String URL = "stock/"; + @override + Map defaultGetFilters() { + + var headers = new Map(); + + headers["part_detail"] = "true"; + headers["location_detail"] = "true"; + headers["supplier_detail"] = "true"; + + return headers; + } + InvenTreeStockItem() : super(); InvenTreeStockItem.fromJson(Map json) : super.fromJson(json) { // TODO } - String get partName => jsondata['part__name'] as String ?? ''; + int get partId => jsondata['part'] ?? -1; - String get partDescription => jsondata['part__description'] as String ?? ''; + int get trackingItemCount => jsondata['tracking_items'] as int ?? 0; + + String get partName { + + String nm = ''; + + // Use the detailed part information as priority + if (jsondata.containsKey('part_detail')) { + nm = jsondata['part_detail']['full_name'] ?? ''; + } + + if (nm.isEmpty) { + nm = jsondata['part__name'] ?? ''; + } + + return nm; + } + + String get partDescription { + String desc = ''; + + // Use the detailed part description as priority + if (jsondata.containsKey('part_detail')) { + desc = jsondata['part_detail']['description'] ?? ''; + } + + if (desc.isEmpty) { + desc = jsondata['part__description'] ?? ''; + } + + return desc; + } + + String get partImage { + String img = ''; + + if (jsondata.containsKey('part_detail')) { + img = jsondata['part_detail']['thumbnail'] ?? ''; + } + + if (img.isEmpty) { + img = jsondata['part__thumbnail'] ?? ''; + } + + return img; + } String get partThumbnail { String thumb = jsondata['part__thumbnail'] as String ?? ''; @@ -24,12 +83,58 @@ class InvenTreeStockItem extends InvenTreeModel { return thumb; } + int get supplierPartId => jsondata['supplier_part'] as int ?? -1; + + String get supplierImage { + String thumb = ''; + + if (jsondata.containsKey("supplier_detail")) { + thumb = jsondata['supplier_detail']['supplier_logo'] ?? ''; + } + + return thumb; + } + + String get supplierName { + String sname = ''; + + if (jsondata.containsKey("supplier_detail")) { + sname = jsondata["supplier_detail"]["supplier_name"] ?? ''; + } + + return sname; + } + + String get supplierSKU { + String sku = ''; + + if (jsondata.containsKey("supplier_detail")) { + sku = jsondata["supplier_detail"]["SKU"] ?? ''; + } + + return sku; + } + int get serialNumber => jsondata['serial'] as int ?? null; - double get quantity => jsondata['quantity'] as double ?? 0.0; + double get quantity => double.tryParse(jsondata['quantity'].toString() ?? '0'); int get locationId => jsondata['location'] as int ?? -1; + String get locationName { + String loc = ''; + + if (jsondata.containsKey('location_detail')) { + loc = jsondata['location_detail']['name'] ?? ''; + } + + if (loc.isEmpty) { + loc = jsondata['location__name'] ?? ''; + } + + return loc; + } + String get displayQuantity { // Display either quantity or serial number! diff --git a/lib/main.dart b/lib/main.dart index 121fa93a..0c6775fc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,7 +14,7 @@ import 'barcode.dart'; import 'dart:convert'; -import 'settings.dart'; +import 'settings/settings.dart'; import 'api.dart'; import 'preferences.dart'; @@ -151,6 +151,13 @@ class _MyHomePageState extends State { _serverAddress = prefs.getString("server"); + // Reset the connection status variables + _serverStatus = "Connecting to server"; + _serverMessage = ""; + _serverConnection = false; + _serverIcon = new FaIcon(FontAwesomeIcons.spinner); + _serverStatusColor = Color.fromARGB(255, 50, 50, 250); + InvenTreeAPI().connect().then((bool result) { if (result) { @@ -176,21 +183,33 @@ class _MyHomePageState extends State { onConnectFailure(fault); }); + + // Update widget state + setState(() {}); } void _search() { + if (!InvenTreeAPI().checkConnection(context)) return; + // TODO } void _scan() { + if (!InvenTreeAPI().checkConnection(context)) return; + scanQrCode(context); } void _parts() { + if (!InvenTreeAPI().checkConnection(context)) return; + Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))); } void _stock() { + + if (!InvenTreeAPI().checkConnection(context)) return; + Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); } @@ -349,6 +368,11 @@ class _MyHomePageState extends State { style: TextStyle(color: _serverStatusColor), ), leading: _serverIcon, + onTap: () { + if (!_serverConnection) { + _checkServerConnection(); + } + }, ), ), ], diff --git a/lib/settings/about.dart b/lib/settings/about.dart new file mode 100644 index 00000000..ad812183 --- /dev/null +++ b/lib/settings/about.dart @@ -0,0 +1,64 @@ +import 'package:InvenTree/api.dart'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:package_info/package_info.dart'; + +class InvenTreeAboutWidget extends StatelessWidget { + + final PackageInfo info; + + InvenTreeAboutWidget(this.info) : super(); + + @override + Widget build(BuildContext context) { + + return Scaffold( + appBar: AppBar( + title: Text("About InvenTree"), + ), + body: ListView( + children: [ + ListTile( + title: Text("Server Address"), + subtitle: Text(InvenTreeAPI().baseUrl.isNotEmpty ? InvenTreeAPI().baseUrl : "Not connected"), + ), + ListTile( + title: Text("Server Version"), + subtitle: Text(InvenTreeAPI().version.isNotEmpty ? InvenTreeAPI().version : "Not connected"), + ), + ListTile( + title: Text("Server Instance"), + subtitle: Text(InvenTreeAPI().instance.isNotEmpty ? InvenTreeAPI().instance : "Not connected"), + ), + Divider(), + ListTile( + title: Text("App Name"), + subtitle: Text("${info.appName}"), + ), + ListTile( + title: Text("Package Name"), + subtitle: Text("${info.packageName}"), + ), + ListTile( + title: Text("App Version"), + subtitle: Text("${info.version}"), + ), + ListTile( + title: Text("Build Number"), + subtitle: Text("${info.buildNumber}") + ), + Divider(), + ListTile( + title: Text("Submit Bug Report"), + subtitle: Text("https://github.com/inventree/inventree-app/issues/"), + onTap: () { + // TODO - Open the URL in an external webpage? + }, + ) + ], + ) + ); + } +} \ No newline at end of file diff --git a/lib/login_settings.dart b/lib/settings/login.dart similarity index 96% rename from lib/login_settings.dart rename to lib/settings/login.dart index aadbfffb..8415e2a4 100644 --- a/lib/login_settings.dart +++ b/lib/settings/login.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'api.dart'; -import 'preferences.dart'; +import '../api.dart'; +import '../preferences.dart'; class InvenTreeLoginSettingsWidget extends StatefulWidget { @@ -81,7 +81,6 @@ class _InvenTreeLoginSettingsState extends State { initialValue: _server, decoration: InputDecoration( hintText: "127.0.0.1:8000", - labelText: "Server:Port", ), validator: _validateServer, onSaved: (String value) { @@ -89,7 +88,7 @@ class _InvenTreeLoginSettingsState extends State { }, ), Divider(), - Text("Login Details"), + Text("Account Details"), TextFormField( initialValue: _username, decoration: InputDecoration( diff --git a/lib/settings.dart b/lib/settings/settings.dart similarity index 52% rename from lib/settings.dart rename to lib/settings/settings.dart index 48d8d363..a3651e88 100644 --- a/lib/settings.dart +++ b/lib/settings/settings.dart @@ -1,8 +1,12 @@ +import 'package:InvenTree/settings/about.dart'; +import 'package:InvenTree/settings/login.dart'; + import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:InvenTree/api.dart'; -import 'login_settings.dart'; +import 'login.dart'; import 'package:package_info/package_info.dart'; @@ -28,12 +32,14 @@ class _InvenTreeSettingsState extends State { ListTile( title: Text("Server Settings"), subtitle: Text("Configure server and login settings"), + leading: FaIcon(FontAwesomeIcons.server), onTap: _editServerSettings, ), Divider(), ListTile( title: Text("About"), subtitle: Text("App details"), + leading: FaIcon(FontAwesomeIcons.infoCircle), onTap: _about, ), ], @@ -52,40 +58,8 @@ class _InvenTreeSettingsState extends State { void _about() async { PackageInfo.fromPlatform().then((PackageInfo info) { - showDialog( - context: context, - child: new SimpleDialog( - title: new Text("About InvenTree"), - children: [ - ListTile( - title: Text("Server Version"), - subtitle: Text(InvenTreeAPI().version.isNotEmpty ? InvenTreeAPI().version : "Not connected"), - ), - Divider(), - ListTile( - title: Text("App Name"), - subtitle: Text("${info.appName}"), - ), - ListTile( - title: Text("Package Name"), - subtitle: Text("${info.packageName}"), - ), - ListTile( - title: Text("App Version"), - subtitle: Text("${info.version}"), - ), - ListTile( - title: Text("Build Number"), - subtitle: Text("${info.buildNumber}") - ), - Divider(), - ListTile( - title: Text("Submit Bug Report"), - subtitle: Text("Submit a bug report or feature request at:\n https://github.com/inventree/inventree-app/issues/"), - ) - ] - ), - ); + Navigator.push(context, + MaterialPageRoute(builder: (context) => InvenTreeAboutWidget(info))); }); } } \ No newline at end of file diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index 0f68703f..f1f02823 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -169,7 +169,7 @@ class _CategoryDisplayState extends State { ); }, body: SubcategoryList(_subcategories), - isExpanded: _subcategoriesExpanded, + isExpanded: _subcategoriesExpanded && _subcategories.length > 0, ), ExpansionPanel( headerBuilder: (BuildContext context, bool isExpanded) { @@ -185,7 +185,7 @@ class _CategoryDisplayState extends State { ); }, body: PartList(_parts), - isExpanded: _partListExpanded, + isExpanded: _partListExpanded && _parts.length > 0, ) ], ), diff --git a/lib/widget/drawer.dart b/lib/widget/drawer.dart index 03095ce1..f60b5497 100644 --- a/lib/widget/drawer.dart +++ b/lib/widget/drawer.dart @@ -1,10 +1,13 @@ +import 'package:InvenTree/api.dart'; import 'package:InvenTree/barcode.dart'; import 'package:flutter/material.dart'; +import 'package:InvenTree/api.dart'; + import 'package:InvenTree/widget/category_display.dart'; import 'package:InvenTree/widget/location_display.dart'; -import 'package:InvenTree/settings.dart'; +import 'package:InvenTree/settings/settings.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class InvenTreeDrawer extends StatelessWidget { @@ -33,6 +36,7 @@ class InvenTreeDrawer extends StatelessWidget { * Upon successful scan, data are passed off to be decoded. */ void _scan() async { + if (!InvenTreeAPI().checkConnection(context)) return; _closeDrawer(); scanQrCode(context); @@ -42,6 +46,7 @@ class InvenTreeDrawer extends StatelessWidget { * Display the top-level PartCategory list */ void _showParts() { + if (!InvenTreeAPI().checkConnection(context)) return; _closeDrawer(); Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))); @@ -51,6 +56,7 @@ class InvenTreeDrawer extends StatelessWidget { * Display the top-level StockLocation list */ void _showStock() { + if (!InvenTreeAPI().checkConnection(context)) return; _closeDrawer(); Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); } @@ -72,6 +78,7 @@ class InvenTreeDrawer extends StatelessWidget { leading: new Image.asset( "assets/image/icon.png", fit: BoxFit.scaleDown, + width: 40, ), title: new Text("InvenTree"), onTap: _home, diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index 49654ffb..8cfd141c 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -176,7 +176,7 @@ class _LocationDisplayState extends State { ); }, body: SublocationList(_sublocations), - isExpanded: _locationListExpanded, + isExpanded: _locationListExpanded && _sublocations.length > 0, ), ExpansionPanel( headerBuilder: (BuildContext context, bool isExpanded) { @@ -192,7 +192,7 @@ class _LocationDisplayState extends State { ); }, body: StockList(_items), - isExpanded: _stockListExpanded, + isExpanded: _stockListExpanded && _items.length > 0, ) ] ), diff --git a/lib/widget/part_display.dart b/lib/widget/part_display.dart index 712069e6..d987d91b 100644 --- a/lib/widget/part_display.dart +++ b/lib/widget/part_display.dart @@ -29,158 +29,126 @@ class _PartDisplayState extends State { InvenTreePart part; - /* - * Construct a list of detail elements about this part. - * Not all elements are set for each part, so only add the ones that are important. - */ - List partDetails() { - List widgets = [ - - // Image / name / description - ListTile( - title: Text("${part.fullname}"), - subtitle: Text("${part.description}"), - leading: Image( - image: InvenTreeAPI().getImage(part.image) - ), - trailing: IconButton( - icon: FaIcon(FontAwesomeIcons.edit), - onPressed: null, - ), - ) - ]; - - return widgets; - } - /* * Build a list of tiles to display under the part description */ List partTiles() { - List tiles = [ + List tiles = []; + + // Image / name / description + tiles.add( Card( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: partDetails(), - ) - ), - ]; + child: ListTile( + title: Text("${part.fullname}"), + subtitle: Text("${part.description}"), + leading: Image( + image: InvenTreeAPI().getImage(part.image) + ), + trailing: IconButton( + icon: FaIcon(FontAwesomeIcons.edit), + onPressed: null, + ), + ) + ) + ); // Category information if (part.categoryName.isNotEmpty) { tiles.add( - Card( - child: ListTile( + ListTile( title: Text("Part Category"), subtitle: Text("${part.categoryName}"), leading: FaIcon(FontAwesomeIcons.stream), onTap: () { - InvenTreePartCategory().get(part.categoryId).then((var cat) { - Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); - }); + if (part.categoryId > 0) { + InvenTreePartCategory().get(part.categoryId).then((var cat) { + Navigator.push(context, MaterialPageRoute( + builder: (context) => CategoryDisplayWidget(cat))); + }); + } }, ) - ) ); } // External link? if (part.link.isNotEmpty) { tiles.add( - Card( - child: ListTile( + ListTile( title: Text("${part.link}"), leading: FaIcon(FontAwesomeIcons.link), trailing: Text(""), onTap: null, ) - ) ); } // Stock information tiles.add( - Card( - child: ListTile( + ListTile( title: Text("Stock"), leading: FaIcon(FontAwesomeIcons.boxes), trailing: Text("${part.inStock}"), onTap: null, ), - ) ); // Parts on order if (part.isPurchaseable) { tiles.add( - Card( - child: ListTile( + ListTile( title: Text("On Order"), leading: FaIcon(FontAwesomeIcons.shoppingCart), trailing: Text("${part.onOrder}"), onTap: null, ) - ) ); } // Parts being built if (part.isAssembly) { - tiles.add( - Card( - child: ListTile( + tiles.add(ListTile( title: Text("Bill of Materials"), leading: FaIcon(FontAwesomeIcons.thList), trailing: Text("${part.bomItemCount}"), onTap: null, ) - ) ); tiles.add( - Card( - child: ListTile( + ListTile( title: Text("Building"), leading: FaIcon(FontAwesomeIcons.tools), trailing: Text("${part.building}"), onTap: null, ) - ) ); } if (part.isComponent) { - tiles.add( - Card( - child: ListTile( + tiles.add(ListTile( title: Text("Used In"), leading: FaIcon(FontAwesomeIcons.sitemap), trailing: Text("${part.usedInCount}"), onTap: null, ) - ) ); } // Notes field? if (part.notes.isNotEmpty) { tiles.add( - Card( - child: ListTile( + ListTile( title: Text("Notes"), leading: FaIcon(FontAwesomeIcons.stickyNote), trailing: Text(""), onTap: null, ) - ) ); } - tiles.add(Spacer()); - return tiles; } @@ -193,9 +161,7 @@ class _PartDisplayState extends State { ), drawer: new InvenTreeDrawer(context), body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, + child: ListView( children: partTiles(), ), ) diff --git a/lib/widget/stock_display.dart b/lib/widget/stock_display.dart index 45c9e041..6a4c5938 100644 --- a/lib/widget/stock_display.dart +++ b/lib/widget/stock_display.dart @@ -1,10 +1,16 @@ import 'package:InvenTree/inventree/stock.dart'; +import 'package:InvenTree/inventree/part.dart'; +import 'package:InvenTree/widget/location_display.dart'; +import 'package:InvenTree/widget/part_display.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:InvenTree/api.dart'; + import 'package:InvenTree/widget/drawer.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class StockItemDisplayWidget extends StatefulWidget { @@ -25,27 +31,137 @@ class _StockItemDisplayState extends State { final InvenTreeStockItem item; - String get _title { - if (item == null) { - return "Stock Item"; - } else { - return "Item: x ${item.partName}"; + /* + * Construct a list of detail elements about this StockItem. + * The number of elements may vary depending on the StockItem details + */ + List stockTiles() { + List tiles = []; + + // Image / name / description + tiles.add( + Card( + child: ListTile( + title: Text("${item.partName}"), + subtitle: Text("${item.partDescription}"), + leading: Image( + image: InvenTreeAPI().getImage(item.partImage), + ), + trailing: IconButton( + icon: FaIcon(FontAwesomeIcons.edit), + onPressed: null, + ) + ) + ) + ); + + tiles.add( + ListTile( + title: Text("Part"), + subtitle: Text("${item.partName}"), + leading: FaIcon(FontAwesomeIcons.shapes), + onTap: () { + if (item.partId > 0) { + InvenTreePart().get(item.partId).then((var part) { + if (part is InvenTreePart) { + Navigator.push(context, MaterialPageRoute(builder: (context) => PartDisplayWidget(part))); + } + }); + } + }, + ) + ); + + // Quantity information + tiles.add( + ListTile( + title: Text("Quantity"), + leading: FaIcon(FontAwesomeIcons.cubes), + trailing: Text("${item.quantity}"), + ) + ); + + // Location information + if (item.locationName.isNotEmpty) { + tiles.add( + ListTile( + title: Text("Stock Location"), + subtitle: Text("${item.locationName}"), + leading: FaIcon(FontAwesomeIcons.mapMarkerAlt), + onTap: () { + if (item.locationId > 0) { + InvenTreeStockLocation().get(item.locationId).then((var loc) { + Navigator.push(context, MaterialPageRoute( + builder: (context) => LocationDisplayWidget(loc))); + }); + } + }, + ) + ); } + + // Supplier part? + if (item.supplierPartId > 0) { + tiles.add( + ListTile( + title: Text("${item.supplierName}"), + subtitle: Text("${item.supplierSKU}"), + leading: FaIcon(FontAwesomeIcons.industry), + trailing: Image( + image: InvenTreeAPI().getImage(item.supplierImage), + height: 32, + ), + onTap: null, + ) + ); + } + + if (item.link.isNotEmpty) { + tiles.add( + ListTile( + title: Text("${item.link}"), + leading: FaIcon(FontAwesomeIcons.link), + trailing: Text(""), + onTap: null, + ) + ); + } + + if (item.trackingItemCount > 0) { + tiles.add( + ListTile( + title: Text("History"), + leading: FaIcon(FontAwesomeIcons.history), + trailing: Text("${item.trackingItemCount}"), + onTap: null, + ) + ); + } + + if (item.notes.isNotEmpty) { + tiles.add( + ListTile( + title: Text("Notes"), + leading: FaIcon(FontAwesomeIcons.stickyNote), + trailing: Text(""), + onTap: null, + ) + ); + } + + return tiles; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(_title), + title: Text("Stock Item"), ), drawer: new InvenTreeDrawer(context), body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text("Stock Item: hello"), - ], + child: ListView( + children: stockTiles(), ) ) );