mirror of
				https://github.com/inventree/inventree-app.git
				synced 2025-10-31 13:25:40 +00:00 
			
		
		
		
	UX Overhaul (#300)
* Add "global actions" to title bar * Implement actions * Add "speed dial" action buttons * tweak global action icons * Refactor actions for "stock item" display * Refactor "part" detail * part category * SupplierPart * More updates * Add BottomAppBar * Add a global bottom app bar * Move "edit" buttons back to the app bar * tweaks * Updates to drawer navigation menu * home screen improvements * text tweaks * Fix appBarTitle for notifications widget * Update "tabs" for category display * Fix for attachment widget * Update tabs for purchaseorder view * Update part display * Cleanup * Add "BOM" tab to part detail widget * Paginated list search cleanup * Update release notes * Update old function * linting * linting * Tweaks to bottomappbar - Increase icon size slightly - Adjust "actions" icon
This commit is contained in:
		| @@ -1,12 +1,12 @@ | ||||
| import "dart:io"; | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:flutter_speed_dial/flutter_speed_dial.dart"; | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
| import "package:inventree/inventree/purchase_order.dart"; | ||||
| import "package:inventree/widget/purchase_order_detail.dart"; | ||||
| import "package:one_context/one_context.dart"; | ||||
| import "package:qr_code_scanner/qr_code_scanner.dart"; | ||||
|  | ||||
| import "package:inventree/app_colors.dart"; | ||||
| import "package:inventree/api.dart"; | ||||
| import "package:inventree/helpers.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
| @@ -805,17 +805,12 @@ Future<void> scanQrCode(BuildContext context) async { | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Construct a generic ListTile widget to link or un-link a custom barcode from a model. | ||||
|  */ | ||||
| Widget customBarcodeActionTile(BuildContext context, RefreshableState state, String barcode, String model, int pk) { | ||||
| SpeedDialChild customBarcodeAction(BuildContext context, RefreshableState state, String barcode, String model, int pk) { | ||||
|  | ||||
|   if (barcode.isEmpty) { | ||||
|     return ListTile( | ||||
|       title: Text(L10().barcodeAssign), | ||||
|       subtitle: Text(L10().barcodeAssignDetail), | ||||
|       leading: Icon(Icons.qr_code, color: COLOR_CLICK), | ||||
|       trailing: Icon(Icons.qr_code_scanner), | ||||
|     return SpeedDialChild( | ||||
|       label: L10().barcodeAssign, | ||||
|       child: Icon(Icons.barcode_reader), | ||||
|       onTap: () { | ||||
|         var handler = UniqueBarcodeHandler((String barcode) { | ||||
|           InvenTreeAPI().linkBarcode({ | ||||
| @@ -823,8 +818,8 @@ Widget customBarcodeActionTile(BuildContext context, RefreshableState state, Str | ||||
|             "barcode": barcode, | ||||
|           }).then((bool result) { | ||||
|             showSnackIcon( | ||||
|               result ? L10().barcodeAssigned : L10().barcodeNotAssigned, | ||||
|               success: result | ||||
|                 result ? L10().barcodeAssigned : L10().barcodeNotAssigned, | ||||
|                 success: result | ||||
|             ); | ||||
|  | ||||
|             state.refresh(context); | ||||
| @@ -832,18 +827,18 @@ Widget customBarcodeActionTile(BuildContext context, RefreshableState state, Str | ||||
|         }); | ||||
|  | ||||
|         Navigator.push( | ||||
|           context, | ||||
|           MaterialPageRoute( | ||||
|             builder: (context) => InvenTreeQRView(handler) | ||||
|           ) | ||||
|             context, | ||||
|             MaterialPageRoute( | ||||
|                 builder: (context) => InvenTreeQRView(handler) | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|     ); | ||||
|   } else { | ||||
|     return ListTile( | ||||
|       title: Text(L10().barcodeUnassign), | ||||
|       leading: Icon(Icons.qr_code, color: COLOR_CLICK), | ||||
|       onTap: () async { | ||||
|     return SpeedDialChild( | ||||
|       child: Icon(Icons.barcode_reader), | ||||
|       label: L10().barcodeUnassign, | ||||
|       onTap: () { | ||||
|         InvenTreeAPI().unlinkBarcode({ | ||||
|           model: pk.toString() | ||||
|         }).then((bool result) { | ||||
| @@ -854,7 +849,7 @@ Widget customBarcodeActionTile(BuildContext context, RefreshableState state, Str | ||||
|  | ||||
|           state.refresh(context); | ||||
|         }); | ||||
|       }, | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
| import "package:package_info_plus/package_info_plus.dart"; | ||||
|  | ||||
| import "package:inventree/app_colors.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
| import "package:inventree/settings/about.dart"; | ||||
| import "package:inventree/settings/app_settings.dart"; | ||||
| import "package:inventree/settings/home_settings.dart"; | ||||
| import "package:inventree/settings/login.dart"; | ||||
| @@ -22,6 +24,16 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> { | ||||
|  | ||||
|   final _scaffoldKey = GlobalKey<ScaffoldState>(); | ||||
|  | ||||
|   /* | ||||
|    * Load "About" widget | ||||
|    */ | ||||
|   Future<void> _about() async { | ||||
|     PackageInfo.fromPlatform().then((PackageInfo info) { | ||||
|       Navigator.push(context, | ||||
|           MaterialPageRoute(builder: (context) => InvenTreeAboutWidget(info))); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
| @@ -31,9 +43,7 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> { | ||||
|       ), | ||||
|       body: Center( | ||||
|         child: ListView( | ||||
|           children: ListTile.divideTiles( | ||||
|             context: context, | ||||
|             tiles: <Widget>[ | ||||
|           children: [ | ||||
|               ListTile( | ||||
|                   title: Text(L10().server), | ||||
|                   subtitle: Text(L10().configureServer), | ||||
| @@ -65,9 +75,14 @@ class _InvenTreeSettingsState extends State<InvenTreeSettingsWidget> { | ||||
|                 onTap: () { | ||||
|                   Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreePartSettingsWidget())); | ||||
|                 } | ||||
|               ), | ||||
|               Divider(), | ||||
|               ListTile( | ||||
|                 title: Text(L10().about), | ||||
|                 leading: FaIcon(FontAwesomeIcons.circleInfo), | ||||
|                 onTap: _about, | ||||
|               ) | ||||
|             ] | ||||
|           ).toList() | ||||
|         ) | ||||
|       ) | ||||
|     ); | ||||
|   | ||||
| @@ -43,33 +43,35 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> { | ||||
|   List<InvenTreeAttachment> attachments = []; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().attachments; | ||||
|   String getAppBarTitle() => L10().attachments; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) { | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|     if (!widget.hasUploadPermission) return []; | ||||
|  | ||||
|     List<Widget> actions = []; | ||||
|  | ||||
|     if (widget.hasUploadPermission) { | ||||
|       // File upload | ||||
|       actions.add( | ||||
|           IconButton( | ||||
|             icon: FaIcon(FontAwesomeIcons.circlePlus), | ||||
|             onPressed: () async { | ||||
|               FilePickerDialog.pickFile( | ||||
|                   onPicked: (File file) async { | ||||
|                     await upload(context, file); | ||||
|                   } | ||||
|               ); | ||||
|             }, | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return actions; | ||||
|     return [ | ||||
|       IconButton( | ||||
|         icon: FaIcon(FontAwesomeIcons.camera), | ||||
|         onPressed: () async { | ||||
|           FilePickerDialog.pickImageFromCamera().then((File? file) { | ||||
|             upload(context, file); | ||||
|           }); | ||||
|         } | ||||
|       ), | ||||
|       IconButton( | ||||
|         icon: FaIcon(FontAwesomeIcons.fileArrowUp), | ||||
|         onPressed: () async { | ||||
|           FilePickerDialog.pickFileFromDevice().then((File? file) { | ||||
|             upload(context, file); | ||||
|           }); | ||||
|         } | ||||
|       ) | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   Future<void> upload(BuildContext context, File file) async { | ||||
|   Future<void> upload(BuildContext context, File? file) async { | ||||
|  | ||||
|     if (file == null) return; | ||||
|  | ||||
|     showLoadingOverlay(context); | ||||
|     final bool result = await widget.attachment.uploadAttachment(file, widget.referenceId); | ||||
|   | ||||
| @@ -37,7 +37,7 @@ class _BillOfMaterialsState extends RefreshableState<BillOfMaterialsWidget> { | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) { | ||||
|   String getAppBarTitle() { | ||||
|     if (widget.isParentComponent) { | ||||
|       return L10().billOfMaterials; | ||||
|     } else { | ||||
| @@ -46,7 +46,7 @@ class _BillOfMaterialsState extends RefreshableState<BillOfMaterialsWidget> { | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) => [ | ||||
|   List<Widget> appBarActions(BuildContext context) => [ | ||||
|     IconButton( | ||||
|       icon: FaIcon(FontAwesomeIcons.filter), | ||||
|       onPressed: () async { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:flutter_speed_dial/flutter_speed_dial.dart"; | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| import "package:inventree/api.dart"; | ||||
| import "package:inventree/app_colors.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
| @@ -33,27 +33,56 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> { | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().partCategory; | ||||
|   String getAppBarTitle() => L10().partCategory; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) { | ||||
|  | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|     List<Widget> actions = []; | ||||
|  | ||||
|     if ((widget.category != null) && InvenTreeAPI().checkPermission("part_category", "change")) { | ||||
|     if (widget.category != null) { | ||||
|       if (api.checkPermission("part_category", "change")) { | ||||
|         actions.add( | ||||
|           IconButton( | ||||
|             icon:  Icon(Icons.edit_square), | ||||
|             tooltip: L10().editCategory, | ||||
|             onPressed: () { | ||||
|               _editCategoryDialog(context); | ||||
|             }, | ||||
|           ) | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<SpeedDialChild> actionButtons(BuildContext context) { | ||||
|     List<SpeedDialChild> actions = []; | ||||
|  | ||||
|     if (api.checkPermission("part", "add")) { | ||||
|      actions.add( | ||||
|        SpeedDialChild( | ||||
|          child: FaIcon(FontAwesomeIcons.shapes), | ||||
|          label: L10().partCreateDetail, | ||||
|          onTap: _newPart, | ||||
|        ) | ||||
|      ); | ||||
|     } | ||||
|  | ||||
|     if (api.checkPermission("part_category", "add")) { | ||||
|       actions.add( | ||||
|         IconButton( | ||||
|           icon: FaIcon(FontAwesomeIcons.penToSquare), | ||||
|           tooltip: L10().edit, | ||||
|           onPressed: () { | ||||
|             _editCategoryDialog(context); | ||||
|           }, | ||||
|         SpeedDialChild( | ||||
|           child: FaIcon(FontAwesomeIcons.sitemap), | ||||
|           label: L10().categoryCreateDetail, | ||||
|           onTap: () { | ||||
|             _newCategory(context); | ||||
|           } | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return actions; | ||||
|  | ||||
|   } | ||||
|  | ||||
|   void _editCategoryDialog(BuildContext context) { | ||||
| @@ -154,25 +183,20 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> { | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget getBottomNavBar(BuildContext context) { | ||||
|     return BottomNavigationBar( | ||||
|       currentIndex: tabIndex, | ||||
|       onTap: onTabSelectionChanged, | ||||
|       items: <BottomNavigationBarItem>[ | ||||
|         BottomNavigationBarItem( | ||||
|           icon: FaIcon(FontAwesomeIcons.sitemap), | ||||
|           label: L10().details, | ||||
|         ), | ||||
|         BottomNavigationBarItem( | ||||
|           icon: FaIcon(FontAwesomeIcons.shapes), | ||||
|           label: L10().parts, | ||||
|         ), | ||||
|         BottomNavigationBarItem( | ||||
|           icon: FaIcon(FontAwesomeIcons.wrench), | ||||
|           label: L10().actions | ||||
|         ), | ||||
|       ] | ||||
|     ); | ||||
|   List<Widget> getTabIcons(BuildContext context) { | ||||
|  | ||||
|     return [ | ||||
|       Tab(text: L10().details), | ||||
|       Tab(text: L10().parts), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getTabs(BuildContext context) { | ||||
|     return [ | ||||
|       Column(children: detailTiles()), | ||||
|       Column(children: partsTiles()), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   // Construct the "details" panel | ||||
| @@ -216,7 +240,6 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> { | ||||
|     }; | ||||
|  | ||||
|     return [ | ||||
|       getCategoryDescriptionCard(extra: false), | ||||
|       ListTile( | ||||
|         title: Text( | ||||
|           L10().parts, | ||||
| @@ -298,74 +321,4 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> { | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   List<Widget> actionTiles(BuildContext context) { | ||||
|  | ||||
|     List<Widget> tiles = [ | ||||
|       getCategoryDescriptionCard(extra: false), | ||||
|     ]; | ||||
|  | ||||
|     if (InvenTreeAPI().checkPermission("part", "add")) { | ||||
|       tiles.add( | ||||
|           ListTile( | ||||
|             title: Text(L10().categoryCreate), | ||||
|             subtitle: Text(L10().categoryCreateDetail), | ||||
|             leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK), | ||||
|             onTap: () async { | ||||
|               _newCategory(context); | ||||
|             }, | ||||
|           ) | ||||
|       ); | ||||
|  | ||||
|       if (widget.category != null) { | ||||
|         tiles.add( | ||||
|             ListTile( | ||||
|               title: Text(L10().partCreate), | ||||
|               subtitle: Text(L10().partCreateDetail), | ||||
|               leading: FaIcon(FontAwesomeIcons.shapes, color: COLOR_CLICK), | ||||
|               onTap: _newPart, | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (tiles.isEmpty) { | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           title: Text( | ||||
|             L10().actionsNone | ||||
|           ), | ||||
|           subtitle: Text( | ||||
|             L10().permissionAccountDenied, | ||||
|           ), | ||||
|           leading: FaIcon(FontAwesomeIcons.userXmark), | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return tiles; | ||||
|   } | ||||
|  | ||||
|   int partCount = 0; | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|  | ||||
|     switch (tabIndex) { | ||||
|       case 0: | ||||
|         return Column( | ||||
|             children: detailTiles() | ||||
|         ); | ||||
|       case 1: | ||||
|         return Column( | ||||
|           children: partsTiles() | ||||
|         ); | ||||
|       case 2: | ||||
|         return ListView( | ||||
|           children: actionTiles(context) | ||||
|         ); | ||||
|       default: | ||||
|         return ListView(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -29,7 +29,7 @@ class _PartCategoryListState extends RefreshableState<PartCategoryList> { | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) => [ | ||||
|   List<Widget> appBarActions(BuildContext context) => [ | ||||
|     IconButton( | ||||
|       icon: FaIcon(FontAwesomeIcons.filter), | ||||
|       onPressed: () async { | ||||
| @@ -41,7 +41,7 @@ class _PartCategoryListState extends RefreshableState<PartCategoryList> { | ||||
|   ]; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().partCategories; | ||||
|   String getAppBarTitle() => L10().partCategories; | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:flutter_speed_dial/flutter_speed_dial.dart"; | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| import "package:inventree/l10.dart"; | ||||
| @@ -40,34 +41,36 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> { | ||||
|   int attachmentCount = 0; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().company; | ||||
|   String getAppBarTitle() => L10().company; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) { | ||||
|  | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|     List<Widget> actions = []; | ||||
|  | ||||
|     actions.add( | ||||
|       IconButton( | ||||
|         icon: FaIcon(FontAwesomeIcons.globe), | ||||
|         onPressed: () async { | ||||
|           widget.company.goToInvenTreePage(); | ||||
|         }, | ||||
|       ) | ||||
|     ); | ||||
|     if (api.checkPermission("purchase_order", "change") || | ||||
|         api.checkPermission("sales_order", "change") || | ||||
|         api.checkPermission("return_order", "change")) { | ||||
|       actions.add( | ||||
|           IconButton( | ||||
|               icon: Icon(Icons.edit_square), | ||||
|               tooltip: L10().companyEdit, | ||||
|               onPressed: () { | ||||
|                 editCompany(context); | ||||
|               } | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|      | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|     actions.add( | ||||
|       IconButton( | ||||
|         icon: FaIcon(FontAwesomeIcons.penToSquare), | ||||
|         tooltip: L10().edit, | ||||
|         onPressed: () { | ||||
|           editCompany(context); | ||||
|         } | ||||
|       ) | ||||
|     ); | ||||
|   @override | ||||
|   List<SpeedDialChild> actionButtons(BuildContext context) { | ||||
|     List<SpeedDialChild> actions = []; | ||||
|  | ||||
|     // TODO | ||||
|  | ||||
|     return actions; | ||||
|  | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   | ||||
| @@ -32,7 +32,7 @@ class _CompanyListWidgetState extends RefreshableState<CompanyListWidget> { | ||||
|   _CompanyListWidgetState(); | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => widget.title; | ||||
|   String getAppBarTitle() => widget.title; | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|   | ||||
| @@ -1,23 +1,17 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
| import "package:package_info_plus/package_info_plus.dart"; | ||||
|  | ||||
| import "package:inventree/api.dart"; | ||||
| import "package:inventree/barcode.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
| import "package:inventree/settings/about.dart"; | ||||
| import "package:inventree/settings/settings.dart"; | ||||
|  | ||||
| import "package:inventree/widget/search.dart"; | ||||
| import "package:inventree/widget/category_display.dart"; | ||||
| import "package:inventree/widget/notifications.dart"; | ||||
| import "package:inventree/widget/purchase_order_list.dart"; | ||||
| import "package:inventree/widget/location_display.dart"; | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Custom "drawer" widget for the InvenTree app. | ||||
|  * | ||||
|  * - Provides a "home" button which completely unwinds the widget stack | ||||
|  * - Global search | ||||
|  * - Barcode scan | ||||
|  */ | ||||
| class InvenTreeDrawer extends StatelessWidget { | ||||
|  | ||||
| @@ -42,90 +36,126 @@ class InvenTreeDrawer extends StatelessWidget { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _search() { | ||||
|  | ||||
|     if (!InvenTreeAPI().checkConnection()) return; | ||||
|   // Load "parts" page | ||||
|   void _parts() { | ||||
|     _closeDrawer(); | ||||
|     Navigator.push( | ||||
|       context, | ||||
|       MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   // Load "stock" page | ||||
|   void _stock() { | ||||
|     _closeDrawer(); | ||||
|     Navigator.push( | ||||
|       context, | ||||
|       MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)) | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   // Load "purchase orders" page | ||||
|   void _purchaseOrders() { | ||||
|     _closeDrawer(); | ||||
|  | ||||
|     Navigator.push( | ||||
|         context, | ||||
|         MaterialPageRoute( | ||||
|             builder: (context) => SearchWidget(true) | ||||
|             builder: (context) => PurchaseOrderListWidget(filters: {}) | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Launch the camera to scan a QR code. | ||||
|    * Upon successful scan, data are passed off to be decoded. | ||||
|    */ | ||||
|   Future <void> _scan() async { | ||||
|     if (!InvenTreeAPI().checkConnection()) return; | ||||
|  | ||||
|   // Load notifications screen | ||||
|   void _notifications() { | ||||
|     _closeDrawer(); | ||||
|     scanQrCode(context); | ||||
|     Navigator.push(context, MaterialPageRoute(builder: (context) => NotificationWidget())); | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Load settings widget | ||||
|    */ | ||||
|   // Load settings widget | ||||
|   void _settings() { | ||||
|     _closeDrawer(); | ||||
|     Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeSettingsWidget())); | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Load "About" widget | ||||
|    */ | ||||
|   Future<void> _about() async { | ||||
|     _closeDrawer(); | ||||
|   // Construct list of tiles to display in the "drawer" menu | ||||
|   List<Widget> drawerTiles(BuildContext context) { | ||||
|     List<Widget> tiles = []; | ||||
|  | ||||
|     PackageInfo.fromPlatform().then((PackageInfo info) { | ||||
|       Navigator.push(context, | ||||
|           MaterialPageRoute(builder: (context) => InvenTreeAboutWidget(info))); | ||||
|     }); | ||||
|     // "Home" access | ||||
|     tiles.add(ListTile( | ||||
|       leading: FaIcon(FontAwesomeIcons.house), | ||||
|       title: Text( | ||||
|         L10().appTitle, | ||||
|         style: TextStyle(fontWeight: FontWeight.bold), | ||||
|       ), | ||||
|       onTap: _home, | ||||
|     )); | ||||
|  | ||||
|     tiles.add(Divider()); | ||||
|  | ||||
|     if (InvenTreeAPI().checkPermission("part_category", "view")) { | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           title: Text(L10().parts), | ||||
|           leading: FaIcon(FontAwesomeIcons.shapes), | ||||
|           onTap: _parts, | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (InvenTreeAPI().checkPermission("stock_location", "view")) { | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           title: Text(L10().stock), | ||||
|           leading: FaIcon(FontAwesomeIcons.boxesStacked), | ||||
|           onTap: _stock, | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (InvenTreeAPI().checkPermission("purchase_order", "view")) { | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           title: Text(L10().purchaseOrders), | ||||
|           leading: FaIcon(FontAwesomeIcons.cartShopping), | ||||
|           onTap: _purchaseOrders, | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (tiles.length > 2) { | ||||
|       tiles.add(Divider()); | ||||
|     } | ||||
|  | ||||
|     if (InvenTreeAPI().supportsNotifications) { | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           leading: FaIcon(FontAwesomeIcons.bell), | ||||
|           title: Text(L10().notifications), | ||||
|           onTap: _notifications, | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     tiles.add( | ||||
|       ListTile( | ||||
|         title: Text(L10().settings), | ||||
|         leading: Icon(Icons.settings), | ||||
|         onTap: _settings, | ||||
|       ) | ||||
|     ); | ||||
|  | ||||
|     return tiles; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|  | ||||
|     return Drawer( | ||||
|     return  Drawer( | ||||
|         child: ListView( | ||||
|             children: ListTile.divideTiles( | ||||
|               context: context, | ||||
|               tiles: <Widget>[ | ||||
|                 ListTile( | ||||
|                   leading: FaIcon(FontAwesomeIcons.house), | ||||
|                   title: Text( | ||||
|                     L10().appTitle, | ||||
|                     style: TextStyle(fontWeight: FontWeight.bold), | ||||
|                   ), | ||||
|                   onTap: _home, | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   title: Text(L10().scanBarcode), | ||||
|                   onTap: _scan, | ||||
|                   leading: Icon(Icons.qr_code_scanner), | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   title: Text(L10().search), | ||||
|                   leading: FaIcon(FontAwesomeIcons.magnifyingGlass), | ||||
|                   onTap: _search, | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   title: Text(L10().settings), | ||||
|                   leading: Icon(Icons.settings), | ||||
|                   onTap: _settings, | ||||
|                 ), | ||||
|                 ListTile( | ||||
|                   title: Text(L10().about), | ||||
|                   leading: FaIcon(FontAwesomeIcons.circleInfo), | ||||
|                   onTap: _about, | ||||
|                 ) | ||||
|               ] | ||||
|             ).toList(), | ||||
|           children: drawerTiles(context), | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -13,15 +13,12 @@ import "package:inventree/settings/login.dart"; | ||||
| import "package:inventree/settings/settings.dart"; | ||||
| import "package:inventree/user_profile.dart"; | ||||
|  | ||||
| import "package:inventree/inventree/notification.dart"; | ||||
|  | ||||
| import "package:inventree/widget/category_display.dart"; | ||||
| import "package:inventree/widget/drawer.dart"; | ||||
| import "package:inventree/widget/location_display.dart"; | ||||
| import "package:inventree/widget/notifications.dart"; | ||||
| import "package:inventree/widget/part_list.dart"; | ||||
| import "package:inventree/widget/purchase_order_list.dart"; | ||||
| import "package:inventree/widget/search.dart"; | ||||
| import "package:inventree/widget/refreshable_state.dart"; | ||||
| import "package:inventree/widget/snacks.dart"; | ||||
| import "package:inventree/widget/spinner.dart"; | ||||
| import "package:inventree/widget/company_list.dart"; | ||||
| @@ -35,7 +32,8 @@ class InvenTreeHomePage extends StatefulWidget { | ||||
|   _InvenTreeHomePageState createState() => _InvenTreeHomePageState(); | ||||
| } | ||||
|  | ||||
| class _InvenTreeHomePageState extends State<InvenTreeHomePage> { | ||||
|  | ||||
| class _InvenTreeHomePageState extends State<InvenTreeHomePage> with BaseWidgetProperties { | ||||
|  | ||||
|   _InvenTreeHomePageState() : super() { | ||||
|     // Load display settings | ||||
| @@ -64,11 +62,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   // Index of bottom navigation bar | ||||
|   int _tabIndex = 0; | ||||
|  | ||||
|   // Number of outstanding notifications | ||||
|   int _notificationCounter = 0; | ||||
|   final homeKey = GlobalKey<ScaffoldState>(); | ||||
|  | ||||
|   bool homeShowPo = false; | ||||
|   bool homeShowSubscribed = false; | ||||
| @@ -76,8 +70,6 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> { | ||||
|   bool homeShowCustomers = false; | ||||
|   bool homeShowSuppliers = false; | ||||
|  | ||||
|   final GlobalKey<_InvenTreeHomePageState> _homeKey = GlobalKey<_InvenTreeHomePageState>(); | ||||
|  | ||||
|   // Selected user profile | ||||
|   UserProfile? _profile; | ||||
|  | ||||
| @@ -202,10 +194,10 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     final notifications = await InvenTreeNotification().list(); | ||||
|     // final notifications = await InvenTreeNotification().list(); | ||||
|  | ||||
|     setState(() { | ||||
|       _notificationCounter = notifications.length; | ||||
|       // _notificationCounter = notifications.length; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| @@ -416,78 +408,17 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> { | ||||
|    * Return the main body widget for display. | ||||
|    * This depends on the current value of _tabIndex | ||||
|    */ | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|  | ||||
|     if (!InvenTreeAPI().isConnected()) { | ||||
|       return _connectionStatusWidget(context); | ||||
|     } | ||||
|  | ||||
|     switch (_tabIndex) { | ||||
|       case 1: // Search widget | ||||
|         return SearchWidget(false); | ||||
|       case 2: // Notification widget | ||||
|         return NotificationWidget(); | ||||
|       case 0: // Home widget | ||||
|       default: | ||||
|         return ListView( | ||||
|           scrollDirection: Axis.vertical, | ||||
|           children: getListTiles(context), | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Construct the bottom navigation bar | ||||
|    */ | ||||
|   List<BottomNavigationBarItem> getNavBarItems(BuildContext context) { | ||||
|  | ||||
|     List<BottomNavigationBarItem> items = <BottomNavigationBarItem>[ | ||||
|       BottomNavigationBarItem( | ||||
|         icon: FaIcon(FontAwesomeIcons.house), | ||||
|         label: L10().home, | ||||
|       ), | ||||
|       BottomNavigationBarItem( | ||||
|         icon: FaIcon(FontAwesomeIcons.magnifyingGlass), | ||||
|         label: L10().search, | ||||
|       ), | ||||
|     ]; | ||||
|  | ||||
|     if (InvenTreeAPI().supportsNotifications) { | ||||
|       items.add( | ||||
|           BottomNavigationBarItem( | ||||
|             icon: _notificationCounter == 0 ? FaIcon(FontAwesomeIcons.bell) : Stack( | ||||
|               children: <Widget>[ | ||||
|                 FaIcon(FontAwesomeIcons.bell), | ||||
|                 Positioned( | ||||
|                   right: 0, | ||||
|                   child: Container( | ||||
|                     padding: EdgeInsets.all(2), | ||||
|                     decoration: BoxDecoration( | ||||
|                       color: Colors.red, | ||||
|                       borderRadius: BorderRadius.circular(20), | ||||
|                     ), | ||||
|                     constraints: BoxConstraints( | ||||
|                       minWidth: 12, | ||||
|                       minHeight: 12, | ||||
|                     ), | ||||
|                     child: Text( | ||||
|                       "${_notificationCounter}", | ||||
|                       style: TextStyle( | ||||
|                         color: Colors.white, | ||||
|                         fontSize: 9, | ||||
|                       ), | ||||
|                       textAlign: TextAlign.center, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ) | ||||
|               ], | ||||
|             ), | ||||
|             label: L10().notifications, | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return items; | ||||
|     return ListView( | ||||
|         scrollDirection: Axis.vertical, | ||||
|         children: getListTiles(context), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -497,7 +428,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> { | ||||
|     var connecting = !connected && InvenTreeAPI().isConnecting(); | ||||
|  | ||||
|     return Scaffold( | ||||
|       key: _homeKey, | ||||
|       key: homeKey, | ||||
|       appBar: AppBar( | ||||
|         title: Text(L10().appTitle), | ||||
|         actions: [ | ||||
| @@ -512,17 +443,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> { | ||||
|       ), | ||||
|       drawer: InvenTreeDrawer(context), | ||||
|       body: getBody(context), | ||||
|       bottomNavigationBar: connected ? BottomNavigationBar( | ||||
|         currentIndex: _tabIndex, | ||||
|         onTap: (int index) { | ||||
|           setState(() { | ||||
|             _tabIndex = index; | ||||
|           }); | ||||
|  | ||||
|           _refreshNotifications(); | ||||
|         }, | ||||
|         items: getNavBarItems(context), | ||||
|       ) : null, | ||||
|       bottomNavigationBar: InvenTreeAPI().isConnected() ? buildBottomAppBar(context, homeKey) : null, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:flutter_speed_dial/flutter_speed_dial.dart"; | ||||
|  | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| @@ -40,35 +41,96 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) { return L10().stockLocation; } | ||||
|   String getAppBarTitle() { | ||||
|     return L10().stockLocation; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) { | ||||
|  | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|     List<Widget> actions = []; | ||||
|  | ||||
|     if (location != null) { | ||||
|  | ||||
|       // Add "locate" button | ||||
|       if (api.supportsMixin("locate")) { | ||||
|         actions.add( | ||||
|     // Add "locate" button | ||||
|     if (location != null && api.supportsMixin("locate")) { | ||||
|       actions.add( | ||||
|           IconButton( | ||||
|             icon: FaIcon(FontAwesomeIcons.magnifyingGlassLocation), | ||||
|             tooltip: L10().locateLocation, | ||||
|             onPressed: () async { | ||||
|               _locateStockLocation(context); | ||||
|             }, | ||||
|               icon: Icon(Icons.travel_explore), | ||||
|               tooltip: L10().locateLocation, | ||||
|               onPressed: () async { | ||||
|                 api.locateItemOrLocation(context, location: location!.pk); | ||||
|               } | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     // Add "edit" button | ||||
|     if (location != null && api.checkPermission("stock_location", "change")) { | ||||
|       actions.add( | ||||
|           IconButton( | ||||
|               icon: Icon(Icons.edit_square), | ||||
|               tooltip: L10().editLocation, | ||||
|               onPressed: () { | ||||
|                 _editLocationDialog(context); | ||||
|               } | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<SpeedDialChild> barcodeButtons(BuildContext context) { | ||||
|     List<SpeedDialChild> actions = []; | ||||
|  | ||||
|     if (location != null) { | ||||
|       // Scan items into this location | ||||
|       if (api.checkPermission("stock", "change")) { | ||||
|         actions.add( | ||||
|             SpeedDialChild( | ||||
|                 child: FaIcon(FontAwesomeIcons.qrcode), | ||||
|                 label: L10().barcodeScanItem, | ||||
|                 onTap: () { | ||||
|                   Navigator.push( | ||||
|                       context, | ||||
|                       MaterialPageRoute(builder: (context) => | ||||
|                           InvenTreeQRView( | ||||
|                               StockLocationScanInItemsHandler(location!))) | ||||
|                   ).then((value) { | ||||
|                     refresh(context); | ||||
|                   }); | ||||
|                 } | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       // Add "edit" button | ||||
|       // Scan this location into another one | ||||
|       if (api.checkPermission("stock_location", "change")) { | ||||
|         actions.add( | ||||
|             IconButton( | ||||
|               icon: FaIcon(FontAwesomeIcons.penToSquare), | ||||
|               tooltip: L10().edit, | ||||
|               onPressed: () { _editLocationDialog(context); }, | ||||
|             SpeedDialChild( | ||||
|                 child: FaIcon(FontAwesomeIcons.qrcode), | ||||
|                 label: L10().transferStockLocation, | ||||
|                 onTap: () { | ||||
|                   Navigator.push( | ||||
|                       context, | ||||
|                       MaterialPageRoute(builder: (context) => | ||||
|                           InvenTreeQRView( | ||||
|                               ScanParentLocationHandler(location!))) | ||||
|                   ).then((value) { | ||||
|                     refresh(context); | ||||
|                   }); | ||||
|                 } | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       // Assign or un-assign barcodes | ||||
|       if (api.supportModernBarcodes) { | ||||
|         actions.add( | ||||
|             customBarcodeAction( | ||||
|                 context, this, | ||||
|                 location!.customBarcode, "stocklocation", | ||||
|                 location!.pk | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
| @@ -77,23 +139,43 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Request identification of this location | ||||
|    */ | ||||
|   Future<void> _locateStockLocation(BuildContext context) async { | ||||
|   @override | ||||
|   List<SpeedDialChild> actionButtons(BuildContext context) { | ||||
|     List<SpeedDialChild> actions = []; | ||||
|  | ||||
|     final _loc = location; | ||||
|  | ||||
|     if (_loc != null) { | ||||
|       api.locateItemOrLocation(context, location: _loc.pk); | ||||
|     // Create new location | ||||
|     if (api.checkPermission("stock_location", "add")) { | ||||
|       actions.add( | ||||
|           SpeedDialChild( | ||||
|               child: FaIcon(FontAwesomeIcons.sitemap), | ||||
|               label: L10().locationCreate, | ||||
|               onTap: () async { | ||||
|                 _newLocation(context); | ||||
|               } | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     // Create new item | ||||
|     if (location != null && api.checkPermission("stock", "add")) { | ||||
|       actions.add( | ||||
|           SpeedDialChild( | ||||
|               child: FaIcon(FontAwesomeIcons.boxesStacked), | ||||
|               label: L10().stockItemCreate, | ||||
|               onTap: () async { | ||||
|                 _newStockItem(context); | ||||
|               } | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Launch a dialog form to edit this stock location | ||||
|    */ | ||||
|   void _editLocationDialog(BuildContext context) { | ||||
|  | ||||
|     final _loc = location; | ||||
|  | ||||
|     if (_loc == null) { | ||||
| @@ -101,12 +183,12 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|     } | ||||
|  | ||||
|     _loc.editForm( | ||||
|       context, | ||||
|       L10().editLocation, | ||||
|       onSuccess: (data) async { | ||||
|         refresh(context); | ||||
|         showSnackIcon(L10().locationUpdated, success: true); | ||||
|       } | ||||
|         context, | ||||
|         L10().editLocation, | ||||
|         onSuccess: (data) async { | ||||
|           refresh(context); | ||||
|           showSnackIcon(L10().locationUpdated, success: true); | ||||
|         } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -117,7 +199,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|  | ||||
|   @override | ||||
|   Future<void> request(BuildContext context) async { | ||||
|  | ||||
|     // Reload location information | ||||
|     if (location != null) { | ||||
|       final bool result = await location!.reload(); | ||||
| @@ -133,35 +214,32 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|   } | ||||
|  | ||||
|   Future<void> _newLocation(BuildContext context) async { | ||||
|  | ||||
|     int pk = location?.pk ?? -1; | ||||
|  | ||||
|     InvenTreeStockLocation().createForm( | ||||
|       context, | ||||
|       L10().locationCreate, | ||||
|       data: { | ||||
|         "parent": (pk > 0) ? pk : null, | ||||
|       }, | ||||
|       onSuccess: (result) async { | ||||
|         context, | ||||
|         L10().locationCreate, | ||||
|         data: { | ||||
|           "parent": (pk > 0) ? pk : null, | ||||
|         }, | ||||
|         onSuccess: (result) async { | ||||
|           Map<String, dynamic> data = result as Map<String, dynamic>; | ||||
|  | ||||
|         Map<String, dynamic> data = result as Map<String, dynamic>; | ||||
|           if (data.containsKey("pk")) { | ||||
|             var loc = InvenTreeStockLocation.fromJson(data); | ||||
|  | ||||
|         if (data.containsKey("pk")) { | ||||
|           var loc = InvenTreeStockLocation.fromJson(data); | ||||
|  | ||||
|           Navigator.push( | ||||
|             context, | ||||
|             MaterialPageRoute( | ||||
|               builder: (context) => LocationDisplayWidget(loc) | ||||
|             ) | ||||
|           ); | ||||
|             Navigator.push( | ||||
|                 context, | ||||
|                 MaterialPageRoute( | ||||
|                     builder: (context) => LocationDisplayWidget(loc) | ||||
|                 ) | ||||
|             ); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<void> _newStockItem(BuildContext context) async { | ||||
|  | ||||
|     int pk = location?.pk ?? -1; | ||||
|  | ||||
|     if (location != null && pk <= 0) { | ||||
| @@ -169,48 +247,46 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|     } | ||||
|  | ||||
|     InvenTreeStockItem().createForm( | ||||
|       context, | ||||
|       L10().stockItemCreate, | ||||
|       data: { | ||||
|         "location": location != null ? pk : null, | ||||
|       }, | ||||
|       onSuccess: (result) async { | ||||
|         context, | ||||
|         L10().stockItemCreate, | ||||
|         data: { | ||||
|           "location": location != null ? pk : null, | ||||
|         }, | ||||
|         onSuccess: (result) async { | ||||
|           Map<String, dynamic> data = result as Map<String, dynamic>; | ||||
|  | ||||
|         Map<String, dynamic> data = result as Map<String, dynamic>; | ||||
|           if (data.containsKey("pk")) { | ||||
|             var item = InvenTreeStockItem.fromJson(data); | ||||
|  | ||||
|         if (data.containsKey("pk")) { | ||||
|           var item = InvenTreeStockItem.fromJson(data); | ||||
|  | ||||
|           Navigator.push( | ||||
|             context, | ||||
|             MaterialPageRoute( | ||||
|               builder: (context) => StockDetailWidget(item) | ||||
|             ) | ||||
|           ); | ||||
|             Navigator.push( | ||||
|                 context, | ||||
|                 MaterialPageRoute( | ||||
|                     builder: (context) => StockDetailWidget(item) | ||||
|                 ) | ||||
|             ); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
|  | ||||
|   } | ||||
|  | ||||
|   Widget locationDescriptionCard({bool includeActions = true}) { | ||||
|     if (location == null) { | ||||
|       return Card( | ||||
|         child: ListTile( | ||||
|           title: Text( | ||||
|             L10().stockTopLevel, | ||||
|             style: TextStyle(fontStyle: FontStyle.italic) | ||||
|           ), | ||||
|           leading: FaIcon(FontAwesomeIcons.boxesStacked), | ||||
|         ) | ||||
|           child: ListTile( | ||||
|             title: Text( | ||||
|                 L10().stockTopLevel, | ||||
|                 style: TextStyle(fontStyle: FontStyle.italic) | ||||
|             ), | ||||
|             leading: FaIcon(FontAwesomeIcons.boxesStacked), | ||||
|           ) | ||||
|       ); | ||||
|     } else { | ||||
|  | ||||
|       List<Widget> children = [ | ||||
|         ListTile( | ||||
|           title: Text("${location!.name}"), | ||||
|           subtitle: Text("${location!.description}"), | ||||
|           leading: location!.customIcon ?? FaIcon(FontAwesomeIcons.boxesStacked), | ||||
|           leading: location!.customIcon ?? | ||||
|               FaIcon(FontAwesomeIcons.boxesStacked), | ||||
|         ), | ||||
|       ]; | ||||
|  | ||||
| @@ -221,19 +297,19 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|               subtitle: Text("${location!.parentPathString}"), | ||||
|               leading: FaIcon(FontAwesomeIcons.turnUp, color: COLOR_CLICK), | ||||
|               onTap: () async { | ||||
|  | ||||
|                 int parentId = location?.parentId ?? -1; | ||||
|  | ||||
|                 if (parentId < 0) { | ||||
|                   Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); | ||||
|                   Navigator.push(context, MaterialPageRoute( | ||||
|                       builder: (context) => LocationDisplayWidget(null))); | ||||
|                 } else { | ||||
|  | ||||
|                   showLoadingOverlay(context); | ||||
|                   var loc = await InvenTreeStockLocation().get(parentId); | ||||
|                   hideLoadingOverlay(); | ||||
|  | ||||
|                   if (loc is InvenTreeStockLocation) { | ||||
|                     Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); | ||||
|                     Navigator.push(context, MaterialPageRoute( | ||||
|                         builder: (context) => LocationDisplayWidget(loc))); | ||||
|                   } | ||||
|                 } | ||||
|               }, | ||||
| @@ -242,63 +318,27 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|       } | ||||
|  | ||||
|       return Card( | ||||
|         child: Column( | ||||
|           children: children, | ||||
|         ) | ||||
|           child: Column( | ||||
|             children: children, | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget getBottomNavBar(BuildContext context) { | ||||
|     return BottomNavigationBar( | ||||
|         currentIndex: tabIndex, | ||||
|         onTap: onTabSelectionChanged, | ||||
|         items: <BottomNavigationBarItem> [ | ||||
|           BottomNavigationBarItem( | ||||
|             icon: FaIcon(FontAwesomeIcons.sitemap), | ||||
|             label: L10().details, | ||||
|           ), | ||||
|           BottomNavigationBarItem( | ||||
|             icon: FaIcon(FontAwesomeIcons.boxesStacked), | ||||
|             label: L10().stock, | ||||
|           ), | ||||
|           BottomNavigationBarItem( | ||||
|             icon: FaIcon(FontAwesomeIcons.wrench), | ||||
|             label: L10().actions, | ||||
|           ) | ||||
|         ] | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   int stockItemCount = 0; | ||||
|  | ||||
|   Widget getSelectedWidget(int index) { | ||||
|  | ||||
|     switch (index) { | ||||
|       case 0: | ||||
|         return Column( | ||||
|           children: detailTiles(), | ||||
|         ); | ||||
|       case 1: | ||||
|         return Column( | ||||
|           children: stockTiles(), | ||||
|         ); | ||||
|       case 2: | ||||
|         return ListView( | ||||
|           children: ListTile.divideTiles( | ||||
|             context: context, | ||||
|             tiles: actionTiles() | ||||
|           ).toList() | ||||
|         ); | ||||
|       default: | ||||
|         return ListView(); | ||||
|     } | ||||
|   List<Widget> getTabIcons(BuildContext context) { | ||||
|     return [ | ||||
|       Tab(text: L10().details), | ||||
|       Tab(text: L10().stockItems), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|     return getSelectedWidget(tabIndex); | ||||
|   List<Widget> getTabs(BuildContext context) { | ||||
|     return [ | ||||
|       Column(children: detailTiles()), | ||||
|       Column(children: stockTiles()), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   // Construct the "details" panel | ||||
| @@ -306,18 +346,18 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|     List<Widget> tiles = [ | ||||
|       locationDescriptionCard(), | ||||
|       ListTile( | ||||
|         title: Text( | ||||
|           L10().sublocations, | ||||
|           style: TextStyle(fontWeight: FontWeight.bold), | ||||
|         ), | ||||
|         trailing: GestureDetector( | ||||
|           child: FaIcon(FontAwesomeIcons.filter), | ||||
|           onTap: () async { | ||||
|             setState(() { | ||||
|               showFilterOptions = !showFilterOptions; | ||||
|             }); | ||||
|           }, | ||||
|         ) | ||||
|           title: Text( | ||||
|             L10().sublocations, | ||||
|             style: TextStyle(fontWeight: FontWeight.bold), | ||||
|           ), | ||||
|           trailing: GestureDetector( | ||||
|             child: FaIcon(FontAwesomeIcons.filter), | ||||
|             onTap: () async { | ||||
|               setState(() { | ||||
|                 showFilterOptions = !showFilterOptions; | ||||
|               }); | ||||
|             }, | ||||
|           ) | ||||
|       ), | ||||
|       Expanded( | ||||
|         child: PaginatedStockLocationList( | ||||
| @@ -335,13 +375,11 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|  | ||||
|   // Construct the "stock" panel | ||||
|   List<Widget> stockTiles() { | ||||
|  | ||||
|     Map<String, String> filters = { | ||||
|       "location": location?.pk.toString() ?? "null", | ||||
|     }; | ||||
|  | ||||
|     return [ | ||||
|       locationDescriptionCard(includeActions: false), | ||||
|       ListTile( | ||||
|         title: Text( | ||||
|           L10().stock, | ||||
| @@ -365,115 +403,4 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|       ) | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   List<Widget> actionTiles() { | ||||
|     List<Widget> tiles = []; | ||||
|  | ||||
|     tiles.add(locationDescriptionCard(includeActions: false)); | ||||
|  | ||||
|     if (api.checkPermission("stock", "add")) { | ||||
|  | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           title: Text(L10().locationCreate), | ||||
|           subtitle: Text(L10().locationCreateDetail), | ||||
|           leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK), | ||||
|           trailing: FaIcon(FontAwesomeIcons.circlePlus, color: COLOR_CLICK), | ||||
|           onTap: () async { | ||||
|             _newLocation(context); | ||||
|           }, | ||||
|         ) | ||||
|       ); | ||||
|  | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           title: Text(L10().stockItemCreate), | ||||
|           subtitle: Text(L10().stockItemCreateDetail), | ||||
|           leading: FaIcon(FontAwesomeIcons.boxesStacked, color: COLOR_CLICK), | ||||
|           trailing: FaIcon(FontAwesomeIcons.circlePlus, color: COLOR_CLICK), | ||||
|           onTap: () async { | ||||
|             _newStockItem(context); | ||||
|           }, | ||||
|         ) | ||||
|       ); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     if (location != null) { | ||||
|  | ||||
|       // Scan stock item into location | ||||
|       if (api.checkPermission("stock", "change")) { | ||||
|         tiles.add( | ||||
|             ListTile( | ||||
|               title: Text(L10().barcodeScanItem), | ||||
|               subtitle: Text(L10().barcodeScanInItems), | ||||
|               leading: FaIcon(FontAwesomeIcons.rightLeft, color: COLOR_CLICK), | ||||
|               trailing: Icon(Icons.qr_code, color: COLOR_CLICK), | ||||
|               onTap: () { | ||||
|  | ||||
|                 var _loc = location; | ||||
|  | ||||
|                 if (_loc != null) { | ||||
|                   Navigator.push( | ||||
|                       context, | ||||
|                       MaterialPageRoute(builder: (context) => | ||||
|                           InvenTreeQRView( | ||||
|                               StockLocationScanInItemsHandler(_loc))) | ||||
|                   ).then((value) { | ||||
|                     refresh(context); | ||||
|                   }); | ||||
|                 } | ||||
|               }, | ||||
|             ) | ||||
|         ); | ||||
|  | ||||
|         // Scan this location into another one | ||||
|         if (api.checkPermission("stock_location", "change")) { | ||||
|           tiles.add( | ||||
|             ListTile( | ||||
|               title: Text(L10().transferStockLocation), | ||||
|               subtitle: Text(L10().transferStockLocationDetail), | ||||
|               leading: FaIcon(FontAwesomeIcons.rightToBracket, color: COLOR_CLICK), | ||||
|               trailing: Icon(Icons.qr_code, color: COLOR_CLICK), | ||||
|               onTap: () { | ||||
|                 var _loc = location; | ||||
|  | ||||
|                 if (_loc != null) { | ||||
|                   Navigator.push( | ||||
|                       context, | ||||
|                       MaterialPageRoute(builder: (context) => | ||||
|                           InvenTreeQRView( | ||||
|                               ScanParentLocationHandler(_loc))) | ||||
|                   ).then((value) { | ||||
|                     refresh(context); | ||||
|                   }); | ||||
|                 } | ||||
|               } | ||||
|             ) | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         if (api.supportModernBarcodes) { | ||||
|           tiles.add( | ||||
|             customBarcodeActionTile(context, this, location!.customBarcode, "stocklocation", location!.pk) | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (tiles.length <= 1) { | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           title: Text( | ||||
|               L10().actionsNone, | ||||
|             style: TextStyle( | ||||
|               fontStyle: FontStyle.italic | ||||
|             ), | ||||
|           ) | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return tiles; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -30,7 +30,7 @@ class _StockLocationListState extends RefreshableState<StockLocationList> { | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) => [ | ||||
|   List<Widget> appBarActions(BuildContext context) => [ | ||||
|     IconButton( | ||||
|       icon: FaIcon(FontAwesomeIcons.filter), | ||||
|       onPressed: () async { | ||||
| @@ -42,7 +42,7 @@ class _StockLocationListState extends RefreshableState<StockLocationList> { | ||||
|   ]; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().stockLocations; | ||||
|   String getAppBarTitle() => L10().stockLocations; | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|   | ||||
| @@ -24,10 +24,7 @@ class _NotificationState extends RefreshableState<NotificationWidget> { | ||||
|   List<InvenTreeNotification> notifications = []; | ||||
|  | ||||
|   @override | ||||
|   AppBar? buildAppBar(BuildContext context, GlobalKey<ScaffoldState> key) { | ||||
|     // No app bar for the notification widget | ||||
|     return null; | ||||
|   } | ||||
|   String getAppBarTitle() => L10().notifications; | ||||
|  | ||||
|   @override | ||||
|   Future<void> request (BuildContext context) async { | ||||
|   | ||||
| @@ -409,19 +409,21 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta | ||||
|    */ | ||||
|   Widget buildSearchInput(BuildContext context) { | ||||
|     return ListTile( | ||||
|       leading: orderingOptions.isEmpty ? null : GestureDetector( | ||||
|       trailing: orderingOptions.isEmpty ? null : GestureDetector( | ||||
|         child: FaIcon(FontAwesomeIcons.sort, color: COLOR_CLICK), | ||||
|         onTap: () async { | ||||
|           _saveOrderingOptions(context); | ||||
|         }, | ||||
|       ), | ||||
|       trailing: GestureDetector( | ||||
|       leading: GestureDetector( | ||||
|         child: FaIcon( | ||||
|           searchController.text.isEmpty ? FontAwesomeIcons.magnifyingGlass : FontAwesomeIcons.deleteLeft, | ||||
|           color: searchController.text.isNotEmpty ? COLOR_DANGER : COLOR_CLICK, | ||||
|           color: searchController.text.isNotEmpty ? COLOR_DANGER : null, | ||||
|         ), | ||||
|         onTap: () { | ||||
|           searchController.clear(); | ||||
|           if (searchController.text.isEmpty) { | ||||
|             searchController.clear(); | ||||
|           } | ||||
|           updateSearchTerm(); | ||||
|         }, | ||||
|       ), | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:flutter_speed_dial/flutter_speed_dial.dart"; | ||||
|  | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| @@ -65,39 +66,62 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> { | ||||
|   int variantCount = 0; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().partDetails; | ||||
|   String getAppBarTitle() => L10().partDetails; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) { | ||||
|  | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|     List<Widget> actions = []; | ||||
|  | ||||
|     if (api.checkPermission("part", "view")) { | ||||
|       actions.add( | ||||
|         IconButton( | ||||
|           icon: FaIcon(FontAwesomeIcons.globe), | ||||
|           onPressed: _openInvenTreePage, | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (api.checkPermission("part", "change")) { | ||||
|       actions.add( | ||||
|         IconButton( | ||||
|           icon: FaIcon(FontAwesomeIcons.penToSquare), | ||||
|           tooltip: L10().edit, | ||||
|           onPressed: () { | ||||
|             _editPartDialog(context); | ||||
|           }, | ||||
|         ) | ||||
|           IconButton( | ||||
|               icon: Icon(Icons.edit_square), | ||||
|               tooltip: L10().editPart, | ||||
|               onPressed: () { | ||||
|                 _editPartDialog(context); | ||||
|               } | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<SpeedDialChild> barcodeButtons(BuildContext context) { | ||||
|     List<SpeedDialChild> actions = []; | ||||
|  | ||||
|     if (api.checkPermission("part", "change")) { | ||||
|       if (api.supportModernBarcodes) { | ||||
|         actions.add( | ||||
|             customBarcodeAction( | ||||
|                 context, this, | ||||
|                 widget.part.customBarcode, "part", | ||||
|                 widget.part.pk | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   Future<void> _openInvenTreePage() async { | ||||
|     part.goToInvenTreePage(); | ||||
|   @override | ||||
|   List<SpeedDialChild> actionButtons(BuildContext context) { | ||||
|     List<SpeedDialChild> actions = []; | ||||
|  | ||||
|     if (api.checkPermission("stock", "add")) { | ||||
|       actions.add( | ||||
|         SpeedDialChild( | ||||
|           child: FaIcon(FontAwesomeIcons.box), | ||||
|           label: L10().stockItemCreate, | ||||
|           onTap: () { | ||||
|             _newStockItem(context); | ||||
|           } | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -144,20 +168,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> { | ||||
|  | ||||
|     // Request the number of parameters for this part | ||||
|     if (api.supportsPartParameters) { | ||||
|  | ||||
|       showParameters = await InvenTreeSettingsManager().getValue(INV_PART_SHOW_PARAMETERS, true) as bool; | ||||
|  | ||||
|       InvenTreePartParameter().count( | ||||
|           filters: { | ||||
|             "part": part.pk.toString(), | ||||
|           } | ||||
|       ).then((int value) { | ||||
|         if (mounted) { | ||||
|           setState(() { | ||||
|             parameterCount = value; | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|     } else { | ||||
|       showParameters = false; | ||||
|     } | ||||
|  | ||||
|     // Request the number of attachments | ||||
| @@ -394,18 +407,13 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> { | ||||
|       ListTile( | ||||
|         title: Text(L10().availableStock), | ||||
|         subtitle: Text(L10().stockDetails), | ||||
|         leading: FaIcon(FontAwesomeIcons.boxesStacked, color: COLOR_CLICK), | ||||
|         leading: FaIcon(FontAwesomeIcons.boxesStacked), | ||||
|         trailing: Text( | ||||
|           part.stockString(), | ||||
|           style: TextStyle( | ||||
|             fontWeight: FontWeight.bold, | ||||
|           ), | ||||
|         ), | ||||
|         onTap: () { | ||||
|           setState(() { | ||||
|             tabIndex = 1; | ||||
|           }); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
| @@ -436,14 +444,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> { | ||||
|                 title: Text(L10().billOfMaterials), | ||||
|                 leading: FaIcon(FontAwesomeIcons.tableList, color: COLOR_CLICK), | ||||
|                 trailing: Text(bomCount.toString()), | ||||
|                 onTap: () { | ||||
|                   Navigator.push( | ||||
|                     context, | ||||
|                     MaterialPageRoute( | ||||
|                       builder: (context) => BillOfMaterialsWidget(part) | ||||
|                     ) | ||||
|                   ); | ||||
|                 } | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
| @@ -543,24 +543,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (showParameters) { | ||||
|       tiles.add( | ||||
|           ListTile( | ||||
|               title: Text(L10().parameters), | ||||
|               leading: FaIcon(FontAwesomeIcons.tableList, color: COLOR_CLICK), | ||||
|               trailing: parameterCount > 0 ? Text(parameterCount.toString()) : null, | ||||
|               onTap: () { | ||||
|                 Navigator.push( | ||||
|                   context, | ||||
|                   MaterialPageRoute( | ||||
|                     builder: (context) => PartParameterWidget(part) | ||||
|                   ) | ||||
|                 ); | ||||
|               } | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     // Notes field | ||||
|     tiles.add( | ||||
|         ListTile( | ||||
| @@ -682,89 +664,49 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> { | ||||
|           } | ||||
|         } | ||||
|     ); | ||||
|  | ||||
|   } | ||||
|  | ||||
|   List<Widget> actionTiles(BuildContext context) { | ||||
|     List<Widget> tiles = []; | ||||
|  | ||||
|     tiles.add(headerTile()); | ||||
|  | ||||
|     tiles.add( | ||||
|       ListTile( | ||||
|         title: Text(L10().stockItemCreate), | ||||
|         leading: FaIcon(FontAwesomeIcons.box), | ||||
|         onTap: () { | ||||
|           _newStockItem(context); | ||||
|         }, | ||||
|       ) | ||||
|     ); | ||||
|  | ||||
|     if (api.supportModernBarcodes) { | ||||
|       tiles.add( | ||||
|         customBarcodeActionTile(context, this, part.customBarcode, "part", part.pk) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return tiles; | ||||
|   } | ||||
|  | ||||
|   int stockItemCount = 0; | ||||
|  | ||||
|   Widget getSelectedWidget(int index) { | ||||
|     switch (index) { | ||||
|       case 0: | ||||
|         return Center( | ||||
|           child: ListView( | ||||
|             children: ListTile.divideTiles( | ||||
|               context: context, | ||||
|               tiles: partTiles() | ||||
|             ).toList() | ||||
|         ), | ||||
|       ); | ||||
|       case 1: | ||||
|         return PaginatedStockItemList( | ||||
|           {"part": "${part.pk}"}, | ||||
|           true, | ||||
|         ); | ||||
|       case 2: | ||||
|         return Center( | ||||
|           child: ListView( | ||||
|             children: ListTile.divideTiles( | ||||
|               context: context, | ||||
|               tiles: actionTiles(context) | ||||
|             ).toList() | ||||
|           ) | ||||
|         ); | ||||
|       default: | ||||
|         return Center(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget getBottomNavBar(BuildContext context) { | ||||
|     return BottomNavigationBar( | ||||
|       currentIndex: tabIndex, | ||||
|       onTap: onTabSelectionChanged, | ||||
|       items: <BottomNavigationBarItem> [ | ||||
|         BottomNavigationBarItem( | ||||
|           icon: FaIcon(FontAwesomeIcons.circleInfo), | ||||
|           label: L10().details, | ||||
|         ), | ||||
|         BottomNavigationBarItem( | ||||
|           icon: FaIcon(FontAwesomeIcons.boxesStacked), | ||||
|           label: L10().stock | ||||
|         ), | ||||
|         BottomNavigationBarItem( | ||||
|           icon: FaIcon(FontAwesomeIcons.wrench), | ||||
|           label: L10().actions, | ||||
|         ), | ||||
|       ] | ||||
|     ); | ||||
|   List<Widget> getTabIcons(BuildContext context) { | ||||
|     List<Widget> icons = [ | ||||
|       Tab(text: L10().details), | ||||
|       Tab(text: L10().stock) | ||||
|     ]; | ||||
|  | ||||
|     if (showBom && part.isAssembly) { | ||||
|       icons.add(Tab(text: L10().bom)); | ||||
|     } | ||||
|  | ||||
|     if (showParameters) { | ||||
|       icons.add(Tab(text: L10().parameters)); | ||||
|     } | ||||
|  | ||||
|     return icons; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|     return getSelectedWidget(tabIndex); | ||||
|   List<Widget> getTabs(BuildContext context) { | ||||
|     List<Widget> tabs = [ | ||||
|       Center( | ||||
|         child: ListView( | ||||
|           children: ListTile.divideTiles( | ||||
|           context: context, | ||||
|           tiles: partTiles() | ||||
|           ).toList() | ||||
|         ) | ||||
|       ), | ||||
|       PaginatedStockItemList({"part": part.pk.toString()}, true) | ||||
|     ]; | ||||
|  | ||||
|     if (showBom && part.isAssembly) { | ||||
|       tabs.add(PaginatedBomList({"part": part.pk.toString()}, showSearch: true, isParentPart: true)); | ||||
|     } | ||||
|  | ||||
|     if (showParameters) { | ||||
|       tabs.add(PaginatedParameterList({"part": part.pk.toString()}, true)); | ||||
|     } | ||||
|  | ||||
|     return tabs; | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -35,10 +35,10 @@ class _PartImageState extends RefreshableState<PartImageWidget> { | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => part.fullname; | ||||
|   String getAppBarTitle() => part.fullname; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) { | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|  | ||||
|     List<Widget> actions = []; | ||||
|  | ||||
|   | ||||
| @@ -36,10 +36,10 @@ class _PartListState extends RefreshableState<PartList> { | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => title.isNotEmpty ? title : L10().parts; | ||||
|   String getAppBarTitle() => title.isNotEmpty ? title : L10().parts; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) => [ | ||||
|   List<Widget> appBarActions(BuildContext context) => [ | ||||
|     IconButton( | ||||
|       icon: FaIcon(FontAwesomeIcons.filter), | ||||
|       onPressed: () async { | ||||
|   | ||||
| @@ -30,10 +30,10 @@ class _PartNotesState extends RefreshableState<PartNotesWidget> { | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().partNotes; | ||||
|   String getAppBarTitle() => L10().partNotes; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) { | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|  | ||||
|     List<Widget> actions = []; | ||||
|  | ||||
|   | ||||
| @@ -24,12 +24,12 @@ class _ParameterWidgetState extends RefreshableState<PartParameterWidget> { | ||||
|   _ParameterWidgetState(); | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) { | ||||
|   String getAppBarTitle() { | ||||
|     return L10().parameters; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) { | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -38,10 +38,10 @@ class _PartSupplierState extends RefreshableState<PartSupplierWidget> { | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().partSuppliers; | ||||
|   String getAppBarTitle() => L10().partSuppliers; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext contexts) { | ||||
|   List<Widget> appBarActions(BuildContext contexts) { | ||||
|     // TODO | ||||
|     return []; | ||||
|   } | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import "package:flutter/material.dart"; | ||||
|  | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
| import "package:one_context/one_context.dart"; | ||||
|  | ||||
| @@ -7,12 +6,13 @@ import "package:inventree/api.dart"; | ||||
| import "package:inventree/api_form.dart"; | ||||
| import "package:inventree/app_colors.dart"; | ||||
| import "package:inventree/helpers.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
| import "package:inventree/inventree/company.dart"; | ||||
| import "package:inventree/inventree/purchase_order.dart"; | ||||
| import "package:inventree/widget/attachment_widget.dart"; | ||||
| import "package:inventree/widget/company_detail.dart"; | ||||
| import "package:inventree/widget/refreshable_state.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
| import "package:inventree/widget/snacks.dart"; | ||||
| import "package:inventree/widget/stock_list.dart"; | ||||
|  | ||||
| @@ -41,17 +41,17 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg | ||||
|   int attachmentCount = 0; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().purchaseOrder; | ||||
|   String getAppBarTitle() => L10().purchaseOrder; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) { | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|     List<Widget> actions = []; | ||||
|  | ||||
|     if (InvenTreeAPI().checkPermission("purchase_order", "change")) { | ||||
|       actions.add( | ||||
|         IconButton( | ||||
|           icon: FaIcon(FontAwesomeIcons.penToSquare), | ||||
|           tooltip: L10().edit, | ||||
|           icon: Icon(Icons.edit_square), | ||||
|           tooltip: L10().purchaseOrderEdit, | ||||
|           onPressed: () { | ||||
|             editOrder(context); | ||||
|           } | ||||
| @@ -145,12 +145,6 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg | ||||
|       title: Text(L10().lineItems), | ||||
|       leading: FaIcon(FontAwesomeIcons.clipboardList, color: COLOR_CLICK), | ||||
|       trailing: Text("${order.lineItemCount}"), | ||||
|       onTap: () { | ||||
|         setState(() { | ||||
|           // Switch to the "line items" tab | ||||
|           tabIndex = 1; | ||||
|         }); | ||||
|       }, | ||||
|     )); | ||||
|  | ||||
|     tiles.add(ListTile( | ||||
| @@ -165,12 +159,6 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg | ||||
|       title: Text(L10().received), | ||||
|       leading: FaIcon(FontAwesomeIcons.clipboardCheck, color: COLOR_CLICK), | ||||
|       trailing: Text("${completedLines}"), | ||||
|       onTap: () { | ||||
|         setState(() { | ||||
|           // Switch to the "received items" tab | ||||
|           tabIndex = 2; | ||||
|         }); | ||||
|       }, | ||||
|     )); | ||||
|  | ||||
|     if (order.issueDate.isNotEmpty) { | ||||
| @@ -371,58 +359,23 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg | ||||
|  | ||||
|     return tiles; | ||||
|   } | ||||
|  | ||||
|    | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|  | ||||
|     return Center( | ||||
|       child: getSelectedWidget(context, tabIndex), | ||||
|     ); | ||||
|   List<Widget> getTabIcons(BuildContext context) { | ||||
|     return [ | ||||
|       Tab(text: L10().details), | ||||
|       Tab(text: L10().lineItems), | ||||
|       Tab(text: L10().received) | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   Widget getSelectedWidget(BuildContext context, int index) { | ||||
|     switch (index) { | ||||
|       case 0: | ||||
|         return ListView( | ||||
|           children: orderTiles(context) | ||||
|         ); | ||||
|       case 1: | ||||
|         return ListView( | ||||
|           children: lineTiles(context) | ||||
|         ); | ||||
|       case 2: | ||||
|         // Stock items received against this order | ||||
|         Map<String, String> filters = { | ||||
|           "purchase_order": "${order.pk}" | ||||
|         }; | ||||
|  | ||||
|         return PaginatedStockItemList(filters, true); | ||||
|  | ||||
|       default: | ||||
|         return ListView(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|    | ||||
|   @override | ||||
|   Widget getBottomNavBar(BuildContext context) { | ||||
|     return BottomNavigationBar( | ||||
|       currentIndex: tabIndex, | ||||
|       onTap: onTabSelectionChanged, | ||||
|       items: [ | ||||
|         BottomNavigationBarItem( | ||||
|           icon: FaIcon(FontAwesomeIcons.circleInfo), | ||||
|           label: L10().details | ||||
|         ), | ||||
|         BottomNavigationBarItem( | ||||
|           icon: FaIcon(FontAwesomeIcons.tableList), | ||||
|           label: L10().lineItems, | ||||
|         ), | ||||
|         BottomNavigationBarItem( | ||||
|           icon: FaIcon(FontAwesomeIcons.boxesStacked), | ||||
|           label: L10().stockItems | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   List<Widget> getTabs(BuildContext context) { | ||||
|     return [ | ||||
|       ListView(children: orderTiles(context)), | ||||
|       ListView(children: lineTiles(context)), | ||||
|       PaginatedStockItemList({"purchase_order": order.pk.toString()}, true), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -33,10 +33,10 @@ class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWi | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().purchaseOrders; | ||||
|   String getAppBarTitle() => L10().purchaseOrders; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) => [ | ||||
|   List<Widget> appBarActions(BuildContext context) => [ | ||||
|     IconButton( | ||||
|       icon: FaIcon(FontAwesomeIcons.filter), | ||||
|       onPressed: () async { | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:flutter_speed_dial/flutter_speed_dial.dart"; | ||||
|  | ||||
| import "package:inventree/api.dart"; | ||||
| import "package:inventree/barcode.dart"; | ||||
|  | ||||
| import "package:inventree/widget/back.dart"; | ||||
| import "package:inventree/widget/drawer.dart"; | ||||
| import "package:inventree/widget/search.dart"; | ||||
|  | ||||
|  | ||||
| /* | ||||
| @@ -11,17 +14,22 @@ import "package:inventree/widget/drawer.dart"; | ||||
|  */ | ||||
| mixin BaseWidgetProperties { | ||||
|  | ||||
|   // Return a list of appBar actions (default = None) | ||||
|   List<Widget> getAppBarActions(BuildContext context) => []; | ||||
|   /* | ||||
|    * Return a list of appBar actions | ||||
|    * By default, no appBar actions are available | ||||
|    */ | ||||
|   List<Widget> appBarActions(BuildContext context) => []; | ||||
|  | ||||
|   // Return a title for the appBar | ||||
|   String getAppBarTitle(BuildContext context) { return "--- app bar ---"; } | ||||
|   String getAppBarTitle() { return "--- app bar ---"; } | ||||
|  | ||||
|   // Function to construct a drawer (override if needed) | ||||
|   Widget getDrawer(BuildContext context) { | ||||
|     return InvenTreeDrawer(context); | ||||
|   } | ||||
|  | ||||
|   List<Widget> getTabs(BuildContext context) => []; | ||||
|  | ||||
|   // Function to construct a body (MUST BE PROVIDED) | ||||
|   Widget getBody(BuildContext context) { | ||||
|  | ||||
| @@ -29,18 +37,145 @@ mixin BaseWidgetProperties { | ||||
|     return ListView(); | ||||
|   } | ||||
|  | ||||
|   Widget? getBottomNavBar(BuildContext context) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Construct the top AppBar for this view | ||||
|    */ | ||||
|   AppBar? buildAppBar(BuildContext context, GlobalKey<ScaffoldState> key) { | ||||
|  | ||||
|     List<Widget> tabs = getTabIcons(context); | ||||
|  | ||||
|     return AppBar( | ||||
|       title: Text(getAppBarTitle(context)), | ||||
|       actions: getAppBarActions(context), | ||||
|       centerTitle: false, | ||||
|       bottom: tabs.isEmpty ? null : TabBar(tabs: tabs), | ||||
|       title: Text(getAppBarTitle()), | ||||
|       actions: appBarActions(context), | ||||
|       leading: backButton(context, key), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Construct a global navigation bar at the bottom of the screen | ||||
|    * - Button to access navigation menu | ||||
|    * - Button to access global search | ||||
|    * - Button to access barcode scan | ||||
|    */ | ||||
|   BottomAppBar? buildBottomAppBar(BuildContext context, GlobalKey<ScaffoldState> key) { | ||||
|  | ||||
|     const double iconSize = 32; | ||||
|     const Color iconColor = Colors.blueGrey; | ||||
|  | ||||
|     List<Widget> icons = [ | ||||
|       IconButton( | ||||
|         icon: Icon(Icons.menu, color: iconColor), | ||||
|         iconSize: iconSize, | ||||
|         onPressed: () { | ||||
|           if (key.currentState != null) { | ||||
|             key.currentState!.openDrawer(); | ||||
|           } | ||||
|         }, | ||||
|       ), | ||||
|       IconButton( | ||||
|         icon: Icon(Icons.search, color: iconColor), | ||||
|         iconSize: iconSize, | ||||
|         onPressed: () { | ||||
|           if (InvenTreeAPI().checkConnection()) { | ||||
|             Navigator.push( | ||||
|                 context, | ||||
|                 MaterialPageRoute( | ||||
|                     builder: (context) => SearchWidget(true) | ||||
|                 ) | ||||
|             ); | ||||
|           } | ||||
|         }, | ||||
|       ), | ||||
|       IconButton( | ||||
|         icon: Icon(Icons.qr_code_scanner, color: iconColor), | ||||
|         iconSize: iconSize, | ||||
|         onPressed: () { | ||||
|           if (InvenTreeAPI().checkConnection()) { | ||||
|             scanQrCode(context); | ||||
|           } | ||||
|         }, | ||||
|       ) | ||||
|     ]; | ||||
|  | ||||
|     return BottomAppBar( | ||||
|         shape: CircularNotchedRectangle(), | ||||
|         notchMargin: 20, | ||||
|         child: IconTheme( | ||||
|             data: IconThemeData(color: Theme.of(context).colorScheme.onPrimary), | ||||
|             child: Row( | ||||
|               mainAxisAlignment: MainAxisAlignment.start, | ||||
|               mainAxisSize: MainAxisSize.max, | ||||
|               children: icons, | ||||
|             ) | ||||
|         ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Build out a set of SpeedDialChild widgets, to serve as "actions" for this view | ||||
|    * Should be re-implemented by particular view with the required actions | ||||
|    * By default, returns an empty list, and thus nothing will be rendered | ||||
|    */ | ||||
|   List<SpeedDialChild> actionButtons(BuildContext context) => []; | ||||
|  | ||||
|   /* | ||||
|    * Build out a set of barcode actions available for this view | ||||
|    */ | ||||
|   List<SpeedDialChild> barcodeButtons(BuildContext context) => []; | ||||
|  | ||||
|   /* | ||||
|    * Build out action buttons for a given widget | ||||
|    */ | ||||
|   Widget? buildSpeedDial(BuildContext context) { | ||||
|  | ||||
|     final actions = actionButtons(context); | ||||
|     final barcodeActions = barcodeButtons(context); | ||||
|  | ||||
|     if (actions.isEmpty && barcodeActions.isEmpty) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     List<Widget> children = []; | ||||
|  | ||||
|     if (barcodeActions.isNotEmpty) { | ||||
|       children.add( | ||||
|         SpeedDial( | ||||
|           icon: Icons.qr_code_scanner, | ||||
|           activeIcon: Icons.close, | ||||
|           children: barcodeActions, | ||||
|           spacing: 14, | ||||
|           childPadding: const EdgeInsets.all(5), | ||||
|           spaceBetweenChildren: 15, | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (actions.isNotEmpty) { | ||||
|       children.add( | ||||
|           SpeedDial( | ||||
|             icon: Icons.more_horiz, | ||||
|             activeIcon: Icons.close, | ||||
|             children: actions, | ||||
|             spacing: 14, | ||||
|             childPadding: const EdgeInsets.all(5), | ||||
|             spaceBetweenChildren: 15, | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Wrap( | ||||
|       direction: Axis.horizontal, | ||||
|       children: children, | ||||
|       spacing: 15, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   // Return list of "tabs" for this widget | ||||
|   List<Widget> getTabIcons(BuildContext context) => []; | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -57,9 +192,6 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> with | ||||
|   // Storage for context once "Build" is called | ||||
|   late BuildContext? _context; | ||||
|  | ||||
|   // Current tab index (used for widgets which display bottom tabs) | ||||
|   int tabIndex = 0; | ||||
|  | ||||
|   // Bool indicator | ||||
|   bool loading = false; | ||||
|  | ||||
| @@ -68,16 +200,6 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> with | ||||
|   // Helper function to return API instance | ||||
|   InvenTreeAPI get api => InvenTreeAPI(); | ||||
|  | ||||
|   // Update current tab selection | ||||
|   void onTabSelectionChanged(int index) { | ||||
|  | ||||
|     if (mounted) { | ||||
|       setState(() { | ||||
|         tabIndex = index; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
| @@ -124,21 +246,38 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> with | ||||
|     // Save the context for future use | ||||
|     _context = context; | ||||
|  | ||||
|     return Scaffold( | ||||
|     List<Widget> tabs = getTabIcons(context); | ||||
|  | ||||
|     Widget body = tabs.isEmpty ? getBody(context) : TabBarView(children: getTabs(context)); | ||||
|  | ||||
|     Scaffold view = Scaffold( | ||||
|       key: refreshableKey, | ||||
|       appBar: buildAppBar(context, refreshableKey), | ||||
|       drawer: getDrawer(context), | ||||
|       floatingActionButton: buildSpeedDial(context), | ||||
|       floatingActionButtonLocation: FloatingActionButtonLocation | ||||
|           .miniEndDocked, | ||||
|       body: Builder( | ||||
|         builder: (BuildContext context) { | ||||
|           return RefreshIndicator( | ||||
|               onRefresh: () async { | ||||
|                 refresh(context); | ||||
|               }, | ||||
|               child: getBody(context) | ||||
|           ); | ||||
|         } | ||||
|           builder: (BuildContext context) { | ||||
|             return RefreshIndicator( | ||||
|                 onRefresh: () async { | ||||
|                   refresh(context); | ||||
|                 }, | ||||
|                 child: body | ||||
|             ); | ||||
|           } | ||||
|       ), | ||||
|       bottomNavigationBar: getBottomNavBar(context), | ||||
|       bottomNavigationBar: buildBottomAppBar(context, refreshableKey), | ||||
|     ); | ||||
|  | ||||
|     // Default implementation is *not* tabbed | ||||
|     if (tabs.isNotEmpty) { | ||||
|       return DefaultTabController( | ||||
|           length: tabs.length, | ||||
|           child: view, | ||||
|       ); | ||||
|     } else { | ||||
|       return view; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -54,7 +54,7 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> { | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().search; | ||||
|   String getAppBarTitle() => L10().search; | ||||
|  | ||||
|   @override | ||||
|   AppBar? buildAppBar(BuildContext context, GlobalKey<ScaffoldState> key) { | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:flutter_speed_dial/flutter_speed_dial.dart"; | ||||
|  | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| @@ -43,42 +44,34 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | ||||
|   _StockItemDisplayState(); | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().stockItem; | ||||
|   String getAppBarTitle() => L10().stockItem; | ||||
|  | ||||
|   bool stockShowHistory = false; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) { | ||||
|  | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|     List<Widget> actions = []; | ||||
|  | ||||
|     if (InvenTreeAPI().checkPermission("stock", "view")) { | ||||
|       actions.add( | ||||
|         IconButton( | ||||
|           icon: FaIcon(FontAwesomeIcons.globe), | ||||
|           onPressed: _openInvenTreePage, | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (InvenTreeAPI().supportsMixin("locate")) { | ||||
|       actions.add( | ||||
|         IconButton( | ||||
|           icon: FaIcon(FontAwesomeIcons.magnifyingGlassLocation), | ||||
|           tooltip: L10().locateItem, | ||||
|           onPressed: () async { | ||||
|             InvenTreeAPI().locateItemOrLocation(context, item: widget.item.pk); | ||||
|           }, | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (InvenTreeAPI().checkPermission("stock", "change")) { | ||||
|     if (api.supportsMixin("locate")) { | ||||
|       actions.add( | ||||
|           IconButton( | ||||
|             icon: FaIcon(FontAwesomeIcons.penToSquare), | ||||
|             tooltip: L10().edit, | ||||
|             onPressed: () { _editStockItem(context); }, | ||||
|             icon: Icon(Icons.travel_explore), | ||||
|             tooltip: L10().locateItem, | ||||
|             onPressed: () async { | ||||
|               api.locateItemOrLocation(context, item: widget.item.pk); | ||||
|             } | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (api.checkPermission("stock", "change")) { | ||||
|       actions.add( | ||||
|           IconButton( | ||||
|               icon: Icon(Icons.edit_square), | ||||
|               tooltip: L10().editItem, | ||||
|               onPressed: () { | ||||
|                 _editStockItem(context); | ||||
|               } | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
| @@ -86,8 +79,115 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   Future<void> _openInvenTreePage() async { | ||||
|     widget.item.goToInvenTreePage(); | ||||
|   @override | ||||
|   List<SpeedDialChild> actionButtons(BuildContext context) { | ||||
|  | ||||
|     List<SpeedDialChild> actions = []; | ||||
|  | ||||
|     if (api.checkPermission("stock", "change")) { | ||||
|  | ||||
|       // Stock adjustment actions available if item is *not* serialized | ||||
|       if (!widget.item.isSerialized()) { | ||||
|  | ||||
|         actions.add( | ||||
|           SpeedDialChild( | ||||
|             child: FaIcon(FontAwesomeIcons.circleCheck, color: Colors.blue), | ||||
|             label: L10().countStock, | ||||
|             onTap: _countStockDialog, | ||||
|           ) | ||||
|         ); | ||||
|  | ||||
|         actions.add( | ||||
|           SpeedDialChild( | ||||
|             child: FaIcon(FontAwesomeIcons.circleMinus, color: Colors.red), | ||||
|             label: L10().removeStock, | ||||
|             onTap: _removeStockDialog, | ||||
|           ) | ||||
|         ); | ||||
|  | ||||
|         actions.add( | ||||
|           SpeedDialChild( | ||||
|             child: FaIcon(FontAwesomeIcons.circlePlus, color: Colors.green), | ||||
|             label: L10().addStock, | ||||
|             onTap: _addStockDialog, | ||||
|           ) | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       // Transfer item | ||||
|       actions.add( | ||||
|         SpeedDialChild( | ||||
|           child: Icon(Icons.trolley), | ||||
|           label: L10().transferStock, | ||||
|           onTap: () { | ||||
|             _transferStockDialog(context); | ||||
|           } | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (labels.isNotEmpty) { | ||||
|       actions.add( | ||||
|         SpeedDialChild( | ||||
|           child: FaIcon(FontAwesomeIcons.print), | ||||
|           label: L10().printLabel, | ||||
|           onTap: () { | ||||
|             _printLabel(context); | ||||
|           } | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (api.checkPermission("stock", "delete")) { | ||||
|       actions.add( | ||||
|           SpeedDialChild( | ||||
|               child: FaIcon(FontAwesomeIcons.trashCan, color: Colors.red), | ||||
|               label: L10().stockItemDelete, | ||||
|               onTap: () { | ||||
|                 _deleteItem(context); | ||||
|               } | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<SpeedDialChild> barcodeButtons(BuildContext context) { | ||||
|     List<SpeedDialChild> actions = []; | ||||
|  | ||||
|     if (api.checkPermission("stock", "change")) { | ||||
|       // Scan item into location | ||||
|       actions.add( | ||||
|           SpeedDialChild( | ||||
|               child: Icon(Icons.qr_code_scanner), | ||||
|               label: L10().scanIntoLocation, | ||||
|               onTap: () { | ||||
|                 Navigator.push( | ||||
|                     context, | ||||
|                     MaterialPageRoute(builder: (context) => | ||||
|                         InvenTreeQRView( | ||||
|                             StockItemScanIntoLocationHandler(widget.item))) | ||||
|                 ).then((ctx) { | ||||
|                   refresh(context); | ||||
|                 }); | ||||
|               } | ||||
|           ) | ||||
|       ); | ||||
|  | ||||
|       if (api.supportModernBarcodes) { | ||||
|         actions.add( | ||||
|             customBarcodeAction( | ||||
|                 context, this, | ||||
|                 widget.item.customBarcode, | ||||
|                 "stockitem", widget.item.pk | ||||
|             ) | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   // Is label printing enabled for this StockItem? | ||||
| @@ -740,170 +840,13 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | ||||
|     return tiles; | ||||
|   } | ||||
|  | ||||
|   List<Widget> actionTiles(BuildContext context) { | ||||
|     List<Widget> tiles = []; | ||||
|  | ||||
|     tiles.add(headerTile()); | ||||
|  | ||||
|     // First check that the user has the required permissions to adjust stock | ||||
|     if (!InvenTreeAPI().checkPermission("stock", "change")) { | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           title: Text(L10().permissionRequired), | ||||
|           leading: FaIcon(FontAwesomeIcons.userXmark) | ||||
|         ) | ||||
|       ); | ||||
|  | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           subtitle: Text(L10().permissionAccountDenied), | ||||
|         ) | ||||
|       ); | ||||
|  | ||||
|       return tiles; | ||||
|     } | ||||
|  | ||||
|     // "Count" is not available for serialized stock | ||||
|     if (!widget.item.isSerialized()) { | ||||
|       tiles.add( | ||||
|           ListTile( | ||||
|               title: Text(L10().countStock), | ||||
|               leading: FaIcon(FontAwesomeIcons.circleCheck, color: COLOR_CLICK), | ||||
|               onTap: _countStockDialog, | ||||
|               trailing: Text(widget.item.quantityString(includeUnits: true)), | ||||
|           ) | ||||
|       ); | ||||
|  | ||||
|       tiles.add( | ||||
|           ListTile( | ||||
|               title: Text(L10().removeStock), | ||||
|               leading: FaIcon(FontAwesomeIcons.circleMinus, color: COLOR_CLICK), | ||||
|               onTap: _removeStockDialog, | ||||
|           ) | ||||
|       ); | ||||
|  | ||||
|       tiles.add( | ||||
|           ListTile( | ||||
|               title: Text(L10().addStock), | ||||
|               leading: FaIcon(FontAwesomeIcons.circlePlus, color: COLOR_CLICK), | ||||
|               onTap: _addStockDialog, | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     tiles.add( | ||||
|       ListTile( | ||||
|         title: Text(L10().transferStock), | ||||
|         subtitle: Text(L10().transferStockDetail), | ||||
|         leading: FaIcon(FontAwesomeIcons.rightLeft, color: COLOR_CLICK), | ||||
|         onTap: () { _transferStockDialog(context); }, | ||||
|       ) | ||||
|     ); | ||||
|  | ||||
|     // Scan item into a location | ||||
|     tiles.add( | ||||
|       ListTile( | ||||
|         title: Text(L10().scanIntoLocation), | ||||
|         subtitle: Text(L10().scanIntoLocationDetail), | ||||
|         leading: FaIcon(FontAwesomeIcons.rightLeft, color: COLOR_CLICK), | ||||
|         trailing: Icon(Icons.qr_code_scanner), | ||||
|         onTap: () { | ||||
|           Navigator.push( | ||||
|             context, | ||||
|             MaterialPageRoute(builder: (context) => InvenTreeQRView(StockItemScanIntoLocationHandler(widget.item))) | ||||
|           ).then((ctx) { | ||||
|             refresh(context); | ||||
|           }); | ||||
|         }, | ||||
|       ) | ||||
|     ); | ||||
|  | ||||
|     if (InvenTreeAPI().supportModernBarcodes || widget.item.customBarcode.isEmpty) { | ||||
|       tiles.add(customBarcodeActionTile(context, this, widget.item.customBarcode, "stockitem", widget.item.pk)); | ||||
|     } else { | ||||
|       // Note: Custom legacy barcodes (only for StockItem model) are handled differently | ||||
|       tiles.add( | ||||
|           ListTile( | ||||
|               title: Text(L10().barcodeUnassign), | ||||
|               leading: Icon(Icons.qr_code, color: COLOR_CLICK), | ||||
|               onTap: () async { | ||||
|                 await widget.item.update(values: {"uid": ""}); | ||||
|                 refresh(context); | ||||
|               } | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     // Print label (if label printing plugins exist) | ||||
|     if (labels.isNotEmpty) { | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           title: Text(L10().printLabel), | ||||
|           leading: FaIcon(FontAwesomeIcons.print, color: COLOR_CLICK), | ||||
|           onTap: () { | ||||
|             _printLabel(context); | ||||
|           }, | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     // If the user has permission to delete this stock item | ||||
|     if (InvenTreeAPI().checkPermission("stock", "delete")) { | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           title: Text("Delete Stock Item"), | ||||
|           leading: FaIcon(FontAwesomeIcons.trashCan, color: COLOR_DANGER), | ||||
|           onTap: () { | ||||
|             _deleteItem(context); | ||||
|           }, | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return tiles; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget getBottomNavBar(BuildContext context) { | ||||
|     return BottomNavigationBar( | ||||
|       currentIndex: tabIndex, | ||||
|       onTap: onTabSelectionChanged, | ||||
|       items: <BottomNavigationBarItem> [ | ||||
|         BottomNavigationBarItem( | ||||
|           icon: FaIcon(FontAwesomeIcons.circleInfo), | ||||
|           label: L10().details, | ||||
|         ), | ||||
|         BottomNavigationBarItem( | ||||
|           icon: FaIcon(FontAwesomeIcons.wrench), | ||||
|           label: L10().actions,        ), | ||||
|       ] | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget getSelectedWidget(int index) { | ||||
|     switch (index) { | ||||
|       case 0: | ||||
|         return ListView( | ||||
|           children: ListTile.divideTiles( | ||||
|             context: context, | ||||
|             tiles: detailTiles() | ||||
|           ).toList(), | ||||
|         ); | ||||
|       case 1: | ||||
|         return ListView( | ||||
|           children: ListTile.divideTiles( | ||||
|             context: context, | ||||
|             tiles: actionTiles(context) | ||||
|           ).toList() | ||||
|         ); | ||||
|       default: | ||||
|         return ListView(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|     return getSelectedWidget(tabIndex); | ||||
|     return ListView( | ||||
|       children: ListTile.divideTiles( | ||||
|           context: context, | ||||
|           tiles: detailTiles() | ||||
|       ).toList() | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -24,7 +24,7 @@ class _StockItemHistoryDisplayState extends RefreshableState<StockItemHistoryWid | ||||
|   final InvenTreeStockItem item; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().stockItemHistory; | ||||
|   String getAppBarTitle() => L10().stockItemHistory; | ||||
|  | ||||
|   List<InvenTreeStockItemHistory> history = []; | ||||
|  | ||||
|   | ||||
| @@ -28,10 +28,10 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes | ||||
|   _StockItemTestResultDisplayState(this.item); | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().testResults; | ||||
|   String getAppBarTitle() => L10().testResults; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) { | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|     return [ | ||||
|       IconButton( | ||||
|           icon: FaIcon(FontAwesomeIcons.circlePlus), | ||||
|   | ||||
| @@ -30,10 +30,10 @@ class _StockListState extends RefreshableState<StockItemList> { | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().stockItems; | ||||
|   String getAppBarTitle() => L10().stockItems; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) => [ | ||||
|   List<Widget> appBarActions(BuildContext context) => [ | ||||
|     IconButton( | ||||
|       icon: FaIcon(FontAwesomeIcons.filter), | ||||
|       onPressed: () async { | ||||
|   | ||||
| @@ -27,7 +27,7 @@ class _StockNotesState extends RefreshableState<StockNotesWidget> { | ||||
|   final InvenTreeStockItem item; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().stockItemNotes; | ||||
|   String getAppBarTitle() => L10().stockItemNotes; | ||||
|  | ||||
|   @override | ||||
|   Future<void> request(BuildContext context) async { | ||||
| @@ -37,7 +37,7 @@ class _StockNotesState extends RefreshableState<StockNotesWidget> { | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) { | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|     List<Widget> actions = []; | ||||
|  | ||||
|     if (InvenTreeAPI().checkPermission("stock", "change")) { | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:flutter_speed_dial/flutter_speed_dial.dart"; | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| import "package:inventree/api.dart"; | ||||
| @@ -35,39 +36,64 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge | ||||
|   _SupplierPartDisplayState(); | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().supplierPart; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) { | ||||
|     List<Widget> actions = []; | ||||
|  | ||||
|     actions.add( | ||||
|       IconButton( | ||||
|         icon: FaIcon(FontAwesomeIcons.penToSquare), | ||||
|         tooltip: L10().edit, | ||||
|         onPressed: () { | ||||
|           editSupplierPart(context); | ||||
|         }, | ||||
|       ) | ||||
|     ); | ||||
|  | ||||
|     return actions; | ||||
|   } | ||||
|   String getAppBarTitle() => L10().supplierPart; | ||||
|  | ||||
|   /* | ||||
|    * Launch a form to edit the current SupplierPart instance | ||||
|    */ | ||||
|   Future<void> editSupplierPart(BuildContext context) async { | ||||
|     widget.supplierPart.editForm( | ||||
|       context, | ||||
|       L10().supplierPartEdit, | ||||
|       onSuccess: (data) async { | ||||
|         refresh(context); | ||||
|         showSnackIcon(L10().supplierPartUpdated, success: true); | ||||
|       } | ||||
|         context, | ||||
|         L10().supplierPartEdit, | ||||
|         onSuccess: (data) async { | ||||
|           refresh(context); | ||||
|           showSnackIcon(L10().supplierPartUpdated, success: true); | ||||
|         } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<SpeedDialChild> barcodeButtons(BuildContext context) { | ||||
|     List<SpeedDialChild> actions = []; | ||||
|  | ||||
|     if (api.checkPermission("purchase_order", "change") || | ||||
|         api.checkPermission("sales_order", "change") || | ||||
|         api.checkPermission("return_order", "change")) { | ||||
|  | ||||
|       actions.add( | ||||
|         customBarcodeAction( | ||||
|           context, this, | ||||
|           widget.supplierPart.customBarcode, | ||||
|           "supplierpart", | ||||
|           widget.supplierPart.pk | ||||
|         ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<Widget> appBarActions(BuildContext context) { | ||||
|     List<Widget> actions = []; | ||||
|  | ||||
|     if (api.checkPermission("purchase_order", "change") || | ||||
|         api.checkPermission("sales_order", "change") || | ||||
|         api.checkPermission("return_order", "change")) { | ||||
|       actions.add( | ||||
|           IconButton( | ||||
|               icon: Icon(Icons.edit_square), | ||||
|               tooltip: L10().edit, | ||||
|               onPressed: () { | ||||
|                 editSupplierPart(context); | ||||
|               } | ||||
|           ) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<void> request(BuildContext context) async { | ||||
|     final bool result = widget.supplierPart.pk > 0 && await widget.supplierPart.reload(); | ||||
| @@ -179,60 +205,14 @@ class _SupplierPartDisplayState extends RefreshableState<SupplierPartDetailWidge | ||||
|     return tiles; | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * Return a list of actions which can be performed for this SupplierPart | ||||
|    */ | ||||
|   List<Widget> actionTiles(BuildContext context) { | ||||
|     List<Widget> tiles = []; | ||||
|  | ||||
|     tiles.add( | ||||
|       customBarcodeActionTile(context, this, widget.supplierPart.customBarcode, "supplierpart", widget.supplierPart.pk) | ||||
|     ); | ||||
|  | ||||
|     return tiles; | ||||
|   } | ||||
|  | ||||
|   Widget getSelectedWidget(int index) { | ||||
|     switch (index) { | ||||
|       case 0: | ||||
|         return ListView( | ||||
|             children: ListTile.divideTiles( | ||||
|               context: context, | ||||
|               tiles: detailTiles(context), | ||||
|             ).toList() | ||||
|         ); | ||||
|       case 1: | ||||
|         return ListView( | ||||
|           children: ListTile.divideTiles( | ||||
|             context: context, | ||||
|             tiles: actionTiles(context) | ||||
|           ).toList() | ||||
|         ); | ||||
|       default: | ||||
|         return ListView(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|     return getSelectedWidget(tabIndex); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget getBottomNavBar(BuildContext context) { | ||||
|     return BottomNavigationBar( | ||||
|       currentIndex: tabIndex, | ||||
|       onTap: onTabSelectionChanged, | ||||
|       items: [ | ||||
|         BottomNavigationBarItem( | ||||
|           icon: FaIcon(FontAwesomeIcons.circleInfo), | ||||
|           label: L10().details, | ||||
|         ), | ||||
|         BottomNavigationBarItem( | ||||
|           icon: FaIcon(FontAwesomeIcons.wrench), | ||||
|           label: L10().actions | ||||
|         ) | ||||
|       ] | ||||
|     return ListView( | ||||
|         children: ListTile.divideTiles( | ||||
|           context: context, | ||||
|           tiles: detailTiles(context), | ||||
|         ).toList() | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -29,12 +29,12 @@ class SupplierPartList extends StatefulWidget { | ||||
| class _SupplierPartListState extends RefreshableState<SupplierPartList> { | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().supplierParts; | ||||
|   String getAppBarTitle() => L10().supplierParts; | ||||
|  | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) => [ | ||||
|   List<Widget> appBarActions(BuildContext context) => [ | ||||
|     IconButton( | ||||
|       icon: FaIcon(FontAwesomeIcons.filter), | ||||
|       onPressed: () async { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user