mirror of
https://github.com/inventree/inventree-app.git
synced 2025-04-28 13:36:50 +00:00
commit
1750f93720
@ -230,6 +230,9 @@ class InvenTreeAPI {
|
||||
|
||||
int get apiVersion => _apiVersion;
|
||||
|
||||
// Notification support requires API v25 or newer
|
||||
bool get supportsNotifications => isConnected() && apiVersion >= 25;
|
||||
|
||||
// Are plugins enabled on the server?
|
||||
bool _pluginsEnabled = false;
|
||||
|
||||
@ -428,6 +431,7 @@ class InvenTreeAPI {
|
||||
|
||||
// Return the received token
|
||||
_token = (data["token"] ?? "") as String;
|
||||
|
||||
print("Received token - $_token");
|
||||
|
||||
// Request user role information (async)
|
||||
|
51
lib/inventree/notification.dart
Normal file
51
lib/inventree/notification.dart
Normal file
@ -0,0 +1,51 @@
|
||||
import "package:inventree/inventree/model.dart";
|
||||
|
||||
/*
|
||||
* Class representing a "notification"
|
||||
*/
|
||||
|
||||
class InvenTreeNotification extends InvenTreeModel {
|
||||
|
||||
InvenTreeNotification() : super();
|
||||
|
||||
InvenTreeNotification.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeNotification createFromJson(Map<String, dynamic> json) {
|
||||
return InvenTreeNotification.fromJson(json);
|
||||
}
|
||||
|
||||
@override
|
||||
String get URL => "notifications/";
|
||||
|
||||
@override
|
||||
Map<String, String> defaultListFilters() {
|
||||
|
||||
// By default, only return 'unread' notifications
|
||||
return {
|
||||
"read": "false",
|
||||
};
|
||||
}
|
||||
|
||||
String get message => (jsondata["message"] ?? "") as String;
|
||||
|
||||
DateTime? get creationDate {
|
||||
if (jsondata.containsKey("creation")) {
|
||||
return DateTime.tryParse((jsondata["creation"] ?? "") as String);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Dismiss this notification (mark as read)
|
||||
*/
|
||||
Future<void> dismiss() async {
|
||||
|
||||
await api.post(
|
||||
"${url}read/",
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -533,7 +533,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
Map<String, dynamic> data = {};
|
||||
|
||||
// Note: Format of adjustment API was updated in API v14
|
||||
if (InvenTreeAPI().supportModernStockTransactions()) {
|
||||
if (api.supportModernStockTransactions()) {
|
||||
// Modern (> 14) API
|
||||
data = {
|
||||
"items": [
|
||||
@ -560,7 +560,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
}
|
||||
|
||||
// Expected API return code depends on server API version
|
||||
final int expected_response = InvenTreeAPI().supportModernStockTransactions() ? 201 : 200;
|
||||
final int expected_response = api.supportModernStockTransactions() ? 201 : 200;
|
||||
|
||||
var response = await api.post(
|
||||
endpoint,
|
||||
|
@ -308,6 +308,9 @@
|
||||
"description": "history"
|
||||
},
|
||||
|
||||
"home": "Home",
|
||||
"@homeScreen": {},
|
||||
|
||||
"homeScreen": "Home Screen",
|
||||
"@homeScreen": {},
|
||||
|
||||
@ -461,6 +464,12 @@
|
||||
"description": "Notes"
|
||||
},
|
||||
|
||||
"notifications": "Notifications",
|
||||
"@notifications": {},
|
||||
|
||||
"notificationsEmpty": "No unread notifications",
|
||||
"@notificationsEmpty": {},
|
||||
|
||||
"noResponse": "No Response from Server",
|
||||
"@noResponse": {},
|
||||
|
||||
@ -631,6 +640,9 @@
|
||||
"quantityPositive": "Quantity must be positive",
|
||||
"@quantityPositive": {},
|
||||
|
||||
"queryEmpty": "Enter search query",
|
||||
"@queryEmpty": {},
|
||||
|
||||
"queryNoResults": "No results for query",
|
||||
"@queryNoResults": {},
|
||||
|
||||
|
@ -12,7 +12,6 @@ Widget backButton(BuildContext context, GlobalKey<ScaffoldState> key) {
|
||||
onLongPress: () {
|
||||
// Display the menu
|
||||
key.currentState!.openDrawer();
|
||||
print("hello?");
|
||||
},
|
||||
child: IconButton(
|
||||
icon: BackButtonIcon(),
|
||||
|
@ -174,7 +174,6 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
icon: FaIcon(FontAwesomeIcons.shapes),
|
||||
label: L10().parts,
|
||||
),
|
||||
// TODO - Add the "actions" item back in
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.wrench),
|
||||
label: L10().actions
|
||||
|
@ -41,7 +41,7 @@ class InvenTreeDrawer extends StatelessWidget {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SearchWidget()
|
||||
builder: (context) => SearchWidget(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,24 +1,29 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/app_settings.dart";
|
||||
import "package:inventree/barcode.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/settings/login.dart";
|
||||
import "package:inventree/settings/settings.dart";
|
||||
import "package:inventree/user_profile.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/barcode.dart";
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/settings/login.dart";
|
||||
|
||||
import "package:inventree/inventree/notification.dart";
|
||||
|
||||
import "package:inventree/widget/category_display.dart";
|
||||
import "package:inventree/widget/company_list.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/snacks.dart";
|
||||
import "package:inventree/widget/drawer.dart";
|
||||
|
||||
import "package:inventree/app_settings.dart";
|
||||
|
||||
|
||||
class InvenTreeHomePage extends StatefulWidget {
|
||||
@ -32,15 +37,29 @@ class InvenTreeHomePage extends StatefulWidget {
|
||||
class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||
|
||||
_InvenTreeHomePageState() : super() {
|
||||
|
||||
// Load display settings
|
||||
_loadSettings();
|
||||
|
||||
// Initially load the profile and attempt server connection
|
||||
_loadProfile();
|
||||
|
||||
_refreshNotifications();
|
||||
|
||||
// Refresh notifications every ~30 seconds
|
||||
Timer.periodic(
|
||||
Duration(
|
||||
milliseconds: 30000,
|
||||
), (timer) {
|
||||
_refreshNotifications();
|
||||
});
|
||||
}
|
||||
|
||||
// Index of bottom navigation bar
|
||||
int _tabIndex = 0;
|
||||
|
||||
// Number of outstanding notifications
|
||||
int _notificationCounter = 0;
|
||||
|
||||
bool homeShowPo = false;
|
||||
bool homeShowSubscribed = false;
|
||||
bool homeShowManufacturers = false;
|
||||
@ -52,18 +71,6 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||
// Selected user profile
|
||||
UserProfile? _profile;
|
||||
|
||||
void _search(BuildContext context) {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SearchWidget()
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
void _scan(BuildContext context) {
|
||||
if (!InvenTreeAPI().checkConnection(context)) return;
|
||||
|
||||
@ -168,6 +175,18 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
/*
|
||||
* Refresh the number of active notifications for this user
|
||||
*/
|
||||
Future<void> _refreshNotifications() async {
|
||||
|
||||
final notifications = await InvenTreeNotification().list();
|
||||
|
||||
setState(() {
|
||||
_notificationCounter = notifications.length;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Widget _listTile(BuildContext context, String label, IconData icon, {Function()? callback, String role = "", String permission = ""}) {
|
||||
|
||||
@ -224,16 +243,6 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||
}
|
||||
));
|
||||
|
||||
// Search widget
|
||||
tiles.add(_listTile(
|
||||
context,
|
||||
L10().search,
|
||||
FontAwesomeIcons.search,
|
||||
callback: () {
|
||||
_search(context);
|
||||
}
|
||||
));
|
||||
|
||||
// Parts
|
||||
tiles.add(_listTile(
|
||||
context,
|
||||
@ -327,6 +336,79 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||
return tiles;
|
||||
}
|
||||
|
||||
/*
|
||||
* Return the main body widget for display.
|
||||
* This depends on the current value of _tabIndex
|
||||
*/
|
||||
Widget getBody(BuildContext 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.home),
|
||||
label: L10().home,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: FaIcon(FontAwesomeIcons.search),
|
||||
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;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@ -345,10 +427,18 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||
],
|
||||
),
|
||||
drawer: InvenTreeDrawer(context),
|
||||
body: ListView(
|
||||
scrollDirection: Axis.vertical,
|
||||
children: getListTiles(context),
|
||||
)
|
||||
body: getBody(context),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
currentIndex: _tabIndex,
|
||||
onTap: (int index) {
|
||||
setState(() {
|
||||
_tabIndex = index;
|
||||
});
|
||||
|
||||
_refreshNotifications();
|
||||
},
|
||||
items: getNavBarItems(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
100
lib/widget/notifications.dart
Normal file
100
lib/widget/notifications.dart
Normal file
@ -0,0 +1,100 @@
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/inventree/notification.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
|
||||
|
||||
class NotificationWidget extends StatefulWidget {
|
||||
|
||||
@override
|
||||
_NotificationState createState() => _NotificationState();
|
||||
|
||||
}
|
||||
|
||||
|
||||
class _NotificationState extends RefreshableState<NotificationWidget> {
|
||||
|
||||
_NotificationState() : super();
|
||||
|
||||
List<InvenTreeNotification> notifications = [];
|
||||
|
||||
@override
|
||||
AppBar? buildAppBar(BuildContext context) {
|
||||
// No app bar for the notification widget
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> request (BuildContext context) async {
|
||||
|
||||
final results = await InvenTreeNotification().list();
|
||||
|
||||
notifications.clear();
|
||||
|
||||
for (InvenTreeModel n in results) {
|
||||
if (n is InvenTreeNotification) {
|
||||
notifications.add(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> dismissNotification(BuildContext context, InvenTreeNotification notification) async {
|
||||
|
||||
await notification.dismiss();
|
||||
|
||||
refresh(context);
|
||||
|
||||
}
|
||||
|
||||
List<Widget> renderNotifications(BuildContext context) {
|
||||
|
||||
List<Widget> tiles = [];
|
||||
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10().notifications,
|
||||
),
|
||||
subtitle: notifications.isEmpty ? Text(L10().notificationsEmpty) : null,
|
||||
leading: notifications.isEmpty ? FaIcon(FontAwesomeIcons.bellSlash) : FaIcon(FontAwesomeIcons.bell),
|
||||
trailing: Text("${notifications.length}"),
|
||||
)
|
||||
);
|
||||
|
||||
for (var notification in notifications) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(notification.name),
|
||||
subtitle: Text(notification.message),
|
||||
trailing: IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.bookmark),
|
||||
onPressed: () async {
|
||||
dismissNotification(context, notification);
|
||||
},
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return tiles;
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
Widget getBody(BuildContext context) {
|
||||
return Center(
|
||||
child: ListView(
|
||||
children: ListTile.divideTiles(
|
||||
context: context,
|
||||
tiles: renderNotifications(context),
|
||||
).toList()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -80,6 +80,14 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
||||
return null;
|
||||
}
|
||||
|
||||
AppBar? buildAppBar(BuildContext context) {
|
||||
return AppBar(
|
||||
title: Text(getAppBarTitle(context)),
|
||||
actions: getAppBarActions(context),
|
||||
leading: backButton(context, refreshableKey),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@ -88,11 +96,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
|
||||
|
||||
return Scaffold(
|
||||
key: refreshableKey,
|
||||
appBar: AppBar(
|
||||
title: Text(getAppBarTitle(context)),
|
||||
actions: getAppBarActions(context),
|
||||
leading: backButton(context, refreshableKey),
|
||||
),
|
||||
appBar: buildAppBar(context),
|
||||
drawer: getDrawer(context),
|
||||
floatingActionButton: getFab(context),
|
||||
body: Builder(
|
||||
|
@ -21,16 +21,33 @@ import "package:inventree/widget/location_list.dart";
|
||||
// Widget for performing database-wide search
|
||||
class SearchWidget extends StatefulWidget {
|
||||
|
||||
const SearchWidget(this.hasAppbar);
|
||||
|
||||
final bool hasAppbar;
|
||||
|
||||
@override
|
||||
_SearchDisplayState createState() => _SearchDisplayState();
|
||||
_SearchDisplayState createState() => _SearchDisplayState(hasAppbar);
|
||||
|
||||
}
|
||||
|
||||
class _SearchDisplayState extends RefreshableState<SearchWidget> {
|
||||
|
||||
_SearchDisplayState(this.hasAppBar) : super();
|
||||
|
||||
final bool hasAppBar;
|
||||
|
||||
@override
|
||||
String getAppBarTitle(BuildContext context) => L10().search;
|
||||
|
||||
@override
|
||||
AppBar? buildAppBar(BuildContext context) {
|
||||
if (hasAppBar) {
|
||||
return super.buildAppBar(context);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
Timer? debounceTimer;
|
||||
@ -155,12 +172,15 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
|
||||
child: ListTile(
|
||||
title: TextField(
|
||||
readOnly: false,
|
||||
decoration: InputDecoration(
|
||||
helperText: L10().queryEmpty,
|
||||
),
|
||||
controller: searchController,
|
||||
onChanged: (String text) {
|
||||
onSearchTextChanged(text);
|
||||
},
|
||||
),
|
||||
leading: IconButton(
|
||||
trailing: IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.backspace, color: Colors.red),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
@ -315,7 +335,7 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
if (results.isEmpty) {
|
||||
if (results.isEmpty && searchController.text.isNotEmpty) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().queryNoResults),
|
||||
|
@ -28,7 +28,7 @@ class _StockListState extends RefreshableState<StockItemList> {
|
||||
final Map<String, String> filters;
|
||||
|
||||
@override
|
||||
String getAppBarTitle(BuildContext context) => L10().purchaseOrders;
|
||||
String getAppBarTitle(BuildContext context) => L10().stockItems;
|
||||
|
||||
@override
|
||||
Widget getBody(BuildContext context) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user