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:
parent
74176cdda8
commit
a8f87e2f5a
@ -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
|
||||
|
@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>9.0</string>
|
||||
<string>11.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -42,4 +42,4 @@ post_install do |installer|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -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",
|
||||
|
@ -71,5 +71,9 @@
|
||||
<array>
|
||||
<string>https</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user