2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-27 21:16:48 +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:
Oliver 2023-04-08 23:59:11 +10:00 committed by GitHub
parent 74176cdda8
commit a8f87e2f5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 979 additions and 1159 deletions

View File

@ -1,9 +1,12 @@
## InvenTree App Release Notes
---
### 0.10.3 - April 2023
### 0.11.0 - April 2023
---
- Adds globally accessible action button for "search"
- Adds globally accessible action button for "barcode scan"
- Implement context actions using floating actions buttons
- Support barcode scanning for purchase orders
### 0.10.2 - March 2023

View File

@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
<string>11.0</string>
</dict>
</plist>

View File

@ -42,4 +42,4 @@ post_install do |installer|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
end
end
end
end

View File

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@ -236,6 +236,7 @@
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@ -250,6 +251,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@ -276,17 +278,17 @@
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
"${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework",
"${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework",
"${BUILT_PRODUCTS_DIR}/audioplayers/audioplayers.framework",
"${BUILT_PRODUCTS_DIR}/camera/camera.framework",
"${BUILT_PRODUCTS_DIR}/audioplayers_darwin/audioplayers_darwin.framework",
"${BUILT_PRODUCTS_DIR}/camera_avfoundation/camera_avfoundation.framework",
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework",
"${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework",
"${BUILT_PRODUCTS_DIR}/open_file/open_file.framework",
"${BUILT_PRODUCTS_DIR}/image_picker_ios/image_picker_ios.framework",
"${BUILT_PRODUCTS_DIR}/open_filex/open_filex.framework",
"${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/path_provider_ios/path_provider_ios.framework",
"${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework",
"${BUILT_PRODUCTS_DIR}/qr_code_scanner/qr_code_scanner.framework",
"${BUILT_PRODUCTS_DIR}/sentry_flutter/sentry_flutter.framework",
"${BUILT_PRODUCTS_DIR}/shared_preferences_ios/shared_preferences_ios.framework",
"${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework",
"${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework",
"${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework",
);
@ -299,17 +301,17 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audioplayers.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/camera.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audioplayers_darwin.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/camera_avfoundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/open_file.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/open_filex.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/qr_code_scanner.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sentry_flutter.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework",
);
@ -392,7 +394,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
@ -415,7 +417,7 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -476,7 +478,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@ -523,7 +525,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
@ -546,7 +548,7 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -577,7 +579,7 @@
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View File

@ -71,5 +71,9 @@
<array>
<string>https</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@ -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);
});
},
}
);
}
}

View File

@ -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()
)
)
);

View File

@ -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);

View File

@ -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 {

View File

@ -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();
}
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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) {

View File

@ -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),
)
);
}
}
}

View File

@ -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,
);
}
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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();
},
),

View File

@ -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;
}
}

View File

@ -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 = [];

View File

@ -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 {

View File

@ -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 = [];

View File

@ -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 [];
}

View File

@ -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 [];
}

View File

@ -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),
];
}
}

View File

@ -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 {

View File

@ -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;
}
}
}

View File

@ -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) {

View File

@ -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()
);
}
}

View File

@ -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 = [];

View File

@ -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),

View File

@ -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 {

View File

@ -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")) {

View File

@ -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()
);
}
}

View File

@ -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 {

View File

@ -395,6 +395,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.7"
flutter_speed_dial:
dependency: "direct main"
description:
name: flutter_speed_dial
sha256: "41d7ad0bc224248637b3a5e0b9083e912a75445bdb450cf82b8ed06d7af7c61d"
url: "https://pub.dev"
source: hosted
version: "6.2.0"
flutter_test:
dependency: "direct dev"
description: flutter

View File

@ -24,6 +24,7 @@ dependencies:
flutter_localized_locales: ^2.0.4
flutter_markdown: ^0.6.13+1 # Rendering markdown
flutter_overlay_loader: ^2.0.0 # Overlay screen support
flutter_speed_dial: ^6.2.0 # Speed dial / FAB implementation
font_awesome_flutter: ^10.3.0 # FontAwesome icon set
http: ^0.13.4
image_picker: ^0.8.6+1 # Select or take photos