import 'package:InvenTree/barcode.dart'; import 'package:InvenTree/inventree/stock.dart'; import 'package:InvenTree/inventree/part.dart'; import 'package:InvenTree/widget/dialogs.dart'; import 'package:InvenTree/widget/fields.dart'; import 'package:InvenTree/widget/location_display.dart'; import 'package:InvenTree/widget/part_detail.dart'; import 'package:InvenTree/widget/refreshable_state.dart'; import 'package:InvenTree/widget/stock_item_test_results.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:InvenTree/api.dart'; import 'package:InvenTree/widget/drawer.dart'; import 'package:InvenTree/widget/refreshable_state.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; class StockDetailWidget extends StatefulWidget { StockDetailWidget(this.item, {Key key}) : super(key: key); final InvenTreeStockItem item; @override _StockItemDisplayState createState() => _StockItemDisplayState(item); } class _StockItemDisplayState extends RefreshableState { @override String getAppBarTitle(BuildContext context) => I18N.of(context).stockItem; final TextEditingController _quantityController = TextEditingController(); final TextEditingController _notesController = TextEditingController(); final _addStockKey = GlobalKey(); final _removeStockKey = GlobalKey(); final _countStockKey = GlobalKey(); final _moveStockKey = GlobalKey(); final _editStockKey = GlobalKey(); _StockItemDisplayState(this.item) { // TODO } final InvenTreeStockItem item; @override Future request(BuildContext context) async { await item.reload(context); await item.getTestResults(context); } void _addStock() async { Navigator.of(context).pop(); double quantity = double.parse(_quantityController.text); _quantityController.clear(); // Await response to prevent the button from being pressed multiple times var response = await item.addStock(quantity, notes: _notesController.text); _notesController.clear(); // TODO - Handle error cases refresh(); // TODO - Display a snackbar here indicating the action was successful (or otherwise) } void _addStockDialog() async { _quantityController.clear(); _notesController.clear(); showFormDialog(context, I18N.of(context).addStock, key: _addStockKey, actions: [ FlatButton( child: Text(I18N.of(context).add), onPressed: () { if (_addStockKey.currentState.validate()) _addStock(); }, ) ], fields: [ Text("Current stock: ${item.quantity}"), QuantityField( label: I18N.of(context).addStock, controller: _quantityController, ), TextFormField( decoration: InputDecoration( labelText: I18N.of(context).notes, ), controller: _notesController, ) ], ); } void _removeStock() async { Navigator.of(context).pop(); double quantity = double.parse(_quantityController.text); _quantityController.clear(); var response = await item.removeStock(quantity, notes: _notesController.text); _notesController.clear(); // TODO - Handle error cases refresh(); // TODO - Display a snackbar here indicating the action was successful (or otherwise) } void _removeStockDialog() { _quantityController.clear(); _notesController.clear(); showFormDialog(context, I18N.of(context).removeStock, key: _removeStockKey, actions: [ FlatButton( child: Text(I18N.of(context).remove), onPressed: () { if (_removeStockKey.currentState.validate()) _removeStock(); }, ) ], fields: [ Text("Current stock: ${item.quantity}"), QuantityField( label: I18N.of(context).removeStock, controller: _quantityController, max: item.quantity, ), TextFormField( decoration: InputDecoration( labelText: I18N.of(context).notes, ), controller: _notesController, ), ], ); } void _countStock() async { Navigator.of(context).pop(); double quantity = double.parse(_quantityController.text); _quantityController.clear(); var response = await item.countStock(quantity, notes: _notesController.text); _notesController.clear(); // TODO - Handle error cases, timeout, etc refresh(); // TODO - Display a snackbar here indicating the action was successful (or otherwise) } void _countStockDialog() async { _quantityController.text = item.quantity.toString(); _notesController.clear(); showFormDialog(context, I18N.of(context).countStock, key: _countStockKey, actions: [ FlatButton( child: Text(I18N.of(context).count), onPressed: () { if (_countStockKey.currentState.validate()) _countStock(); }, ) ], fields: [ QuantityField( label: I18N.of(context).countStock, hint: "${item.quantity}", controller: _quantityController, ), TextFormField( decoration: InputDecoration( labelText: I18N.of(context).notes, ), controller: _notesController, ) ] ); } void _transferStock(BuildContext context, InvenTreeStockLocation location) async { Navigator.of(context).pop(); double quantity = double.parse(_quantityController.text); String notes = _notesController.text; _quantityController.clear(); _notesController.clear(); var response = await item.transferStock(location.pk, quantity: quantity, notes: notes); // TODO - Error handling (potentially return false?) refresh(); // TODO - Display a snackbar here indicating the action was successful (or otherwise) } void _transferStockDialog() async { var locations = await InvenTreeStockLocation().list(context); final _selectedController = TextEditingController(); InvenTreeStockLocation selectedLocation; _quantityController.text = "${item.quantity}"; showFormDialog(context, I18N.of(context).transferStock, key: _moveStockKey, actions: [ FlatButton( child: Text(I18N.of(context).transfer), onPressed: () { if (_moveStockKey.currentState.validate()) { _moveStockKey.currentState.save(); } }, ) ], fields: [ QuantityField( label: I18N.of(context).quantity, controller: _quantityController, max: item.quantity, ), TypeAheadFormField( textFieldConfiguration: TextFieldConfiguration( controller: _selectedController, autofocus: true, decoration: InputDecoration( hintText: "Search for location", border: OutlineInputBorder() ) ), suggestionsCallback: (pattern) async { var suggestions = List(); for (var loc in locations) { if (loc.matchAgainstString(pattern)) { suggestions.add(loc as InvenTreeStockLocation); } } return suggestions; }, validator: (value) { if (selectedLocation == null) { return "Select a location"; } return null; }, onSuggestionSelected: (suggestion) { selectedLocation = suggestion as InvenTreeStockLocation; _selectedController.text = selectedLocation.pathstring; }, onSaved: (value) { _transferStock(context, selectedLocation); }, itemBuilder: (context, suggestion) { var location = suggestion as InvenTreeStockLocation; return ListTile( title: Text("${location.pathstring}"), subtitle: Text("${location.description}"), ); } ), ], ); } Widget headerTile() { return Card( child: ListTile( title: Text("${item.partName}"), subtitle: Text("${item.partDescription}"), leading: InvenTreeAPI().getImage(item.partImage), ) ); } /* * Construct a list of detail elements about this StockItem. * The number of elements may vary depending on the StockItem details */ List detailTiles() { List tiles = []; // Image / name / description tiles.add(headerTile()); tiles.add( ListTile( title: Text(I18N.of(context).part), subtitle: Text("${item.partName}"), leading: FaIcon(FontAwesomeIcons.shapes), onTap: () { if (item.partId > 0) { InvenTreePart().get(context, item.partId).then((var part) { if (part is InvenTreePart) { Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); } }); } }, ) ); // Location information if ((item.locationId > 0) && (item.locationName != null) && (item.locationName.isNotEmpty)) { tiles.add( ListTile( title: Text(I18N.of(context).stockLocation), subtitle: Text("${item.locationPathString}"), leading: FaIcon(FontAwesomeIcons.mapMarkerAlt), onTap: () { if (item.locationId > 0) { InvenTreeStockLocation().get(context, item.locationId).then((var loc) { Navigator.push(context, MaterialPageRoute( builder: (context) => LocationDisplayWidget(loc))); }); } }, ) ); } else { tiles.add( ListTile( title: Text(I18N.of(context).stockLocation), leading: FaIcon(FontAwesomeIcons.mapMarkerAlt), subtitle: Text("No location set"), ) ); } // Quantity information if (item.isSerialized()) { tiles.add( ListTile( title: Text("Serial Number"), leading: FaIcon(FontAwesomeIcons.hashtag), trailing: Text("${item.serialNumber}"), ) ); } else { tiles.add( ListTile( title: Text(I18N.of(context).quantity), leading: FaIcon(FontAwesomeIcons.cubes), trailing: Text("${item.quantity}"), ) ); } // Supplier part? if (item.supplierPartId > 0) { tiles.add( ListTile( title: Text("${item.supplierName}"), subtitle: Text("${item.supplierSKU}"), leading: FaIcon(FontAwesomeIcons.industry), trailing: InvenTreeAPI().getImage(item.supplierImage), onTap: null, ) ); } if (item.link.isNotEmpty) { tiles.add( ListTile( title: Text("${item.link}"), leading: FaIcon(FontAwesomeIcons.link), trailing: Text(""), onTap: null, ) ); } tiles.add( ListTile( title: Text("Test Results"), leading: FaIcon(FontAwesomeIcons.tasks), trailing: Text("${item.testResultCount}"), onTap: () { Navigator.push(context, MaterialPageRoute(builder: (context) => StockItemTestResultsWidget(item))); } ) ); if (item.trackingItemCount > 0) { tiles.add( ListTile( title: Text(I18N.of(context).history), leading: FaIcon(FontAwesomeIcons.history), trailing: Text("${item.trackingItemCount}"), onTap: null, ) ); } if (item.notes.isNotEmpty) { tiles.add( ListTile( title: Text(I18N.of(context).notes), leading: FaIcon(FontAwesomeIcons.stickyNote), trailing: Text(""), onTap: null, ) ); } return tiles; } List actionTiles() { List tiles = []; tiles.add(headerTile()); if (!item.isSerialized()) { tiles.add( ListTile( title: Text(I18N.of(context).countStock), leading: FaIcon(FontAwesomeIcons.checkCircle), onTap: _countStockDialog, ) ); tiles.add( ListTile( title: Text(I18N.of(context).removeStock), leading: FaIcon(FontAwesomeIcons.minusCircle), onTap: _removeStockDialog, ) ); tiles.add( ListTile( title: Text(I18N.of(context).addStock), leading: FaIcon(FontAwesomeIcons.plusCircle), onTap: _addStockDialog, ) ); } tiles.add( ListTile( title: Text(I18N.of(context).transferStock), leading: FaIcon(FontAwesomeIcons.exchangeAlt), onTap: _transferStockDialog, ) ); // Scan item into a location tiles.add( ListTile( title: Text("Scan Into Location"), leading: FaIcon(FontAwesomeIcons.exchangeAlt), trailing: FaIcon(FontAwesomeIcons.qrcode), onTap: () { Navigator.push( context, MaterialPageRoute(builder: (context) => InvenTreeQRView(StockItemScanIntoLocationHandler(item))) ); }, ) ); // Add or remove custom barcode if (item.uid.isEmpty) { tiles.add( ListTile( title: Text("Assign Barcode"), leading: FaIcon(FontAwesomeIcons.barcode), trailing: FaIcon(FontAwesomeIcons.qrcode), onTap: () { Navigator.push( context, MaterialPageRoute(builder: (context) => InvenTreeQRView(StockItemBarcodeAssignmentHandler(item))) ); } ) ); } return tiles; } /* * Return a list of context-sensitive action buttons. * Not all buttons will be avaialable for a given StockItem, * depending on the properties of that StockItem */ /* List actionButtons() { var buttons = List(); // The following actions only apply if the StockItem is not serialized if (!item.isSerialized()) { buttons.add(SpeedDialChild( child: Icon(FontAwesomeIcons.plusCircle), label: "Add Stock", onTap: _addStockDialog, ) ); buttons.add(SpeedDialChild( child: Icon(FontAwesomeIcons.minusCircle), label: "Remove Stock", onTap: _removeStockDialog, ), ); buttons.add(SpeedDialChild( child: Icon(FontAwesomeIcons.checkCircle), label: "Count Stock", onTap: _countStockDialog, )); } buttons.add(SpeedDialChild( child: Icon(FontAwesomeIcons.exchangeAlt), label: "Transfer Stock", onTap: _transferStockDialog, )); return buttons; } */ @override Widget getBottomNavBar(BuildContext context) { return BottomNavigationBar( currentIndex: tabIndex, onTap: onTabSelectionChanged, items: [ BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.infoCircle), title: Text(I18N.of(context).details), ), BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.wrench), title: Text(I18N.of(context).actions), ), ] ); } Widget getSelectedWidget(int index) { switch (index) { case 0: return ListView( children: detailTiles(), ); case 1: return ListView( children: actionTiles(), ); default: return null; } } @override Widget getBody(BuildContext context) { return getSelectedWidget(tabIndex); } /* @override Widget getFab(BuildContext context) { return SpeedDial( visible: true, animatedIcon: AnimatedIcons.menu_close, heroTag: 'stock-item-fab', children: actionButtons(), ); } */ }