UI updates (#849)

* Updated dashboard

- Display "outstanding" badges
- Display "overdue" badges
- Pull-to-refresh

* Remove dead code

* Refactor app colors

* Add "pending shipments" icon

* Remove custom spinner

* Refactor error dialog

* Updated redirect after login

* Refactor login/account pages

- Better UX and confirmation messages

* Refactor API error messages

* Improve token management

- Secure storage
- Handle session expiry
- Per-profile HTTPS certificate checks

* Improved error messages
This commit is contained in:
Oliver
2026-07-06 21:10:10 +10:00
committed by GitHub
parent f737172180
commit 349a8e0ef5
45 changed files with 1240 additions and 477 deletions
+21 -19
View File
@@ -83,7 +83,7 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
if (showCameraShortcut && widget.order.canEdit) {
actions.add(
SpeedDialChild(
child: const Icon(TablerIcons.camera, color: Colors.blue),
child: Icon(TablerIcons.camera, color: COLOR_ACTION),
label: L10().takePicture,
onTap: () async {
_uploadImage(context);
@@ -98,7 +98,7 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
if (widget.order.canIssue) {
actions.add(
SpeedDialChild(
child: const Icon(TablerIcons.send, color: Colors.blue),
child: Icon(TablerIcons.send, color: COLOR_ACTION),
label: L10().issueOrder,
onTap: () async {
_issueOrder(context);
@@ -111,7 +111,7 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
if (widget.order.canCompleteOrder) {
actions.add(
SpeedDialChild(
child: const Icon(TablerIcons.check, color: Colors.green),
child: Icon(TablerIcons.check, color: COLOR_SUCCESS),
label: L10().completeOrder,
onTap: () async {
_completeOrder(context);
@@ -124,7 +124,7 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
if (widget.order.canHold) {
actions.add(
SpeedDialChild(
child: const Icon(TablerIcons.player_pause, color: Colors.orange),
child: Icon(TablerIcons.player_pause, color: COLOR_WARNING),
label: L10().holdOrder,
onTap: () async {
_holdOrder(context);
@@ -137,9 +137,9 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
if (widget.order.isInProgress) {
actions.add(
SpeedDialChild(
child: const Icon(
child: Icon(
TablerIcons.arrow_autofit_down,
color: Colors.purple,
color: Theme.of(context).colorScheme.secondary,
),
label: L10().allocateAuto,
onTap: () async {
@@ -154,7 +154,7 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
widget.order.allocatedLineItemCount > 0) {
actions.add(
SpeedDialChild(
child: const Icon(TablerIcons.arrow_autofit_up, color: Colors.red),
child: Icon(TablerIcons.arrow_autofit_up, color: COLOR_DANGER),
label: L10().unallocateStock,
onTap: () async {
_unallocateAll(context);
@@ -167,7 +167,7 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
if (widget.order.canCancel) {
actions.add(
SpeedDialChild(
child: const Icon(TablerIcons.circle_x, color: Colors.red),
child: Icon(TablerIcons.circle_x, color: COLOR_DANGER),
label: L10().cancelOrder,
onTap: () async {
_cancelOrder(context);
@@ -192,7 +192,7 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
L10().issueOrder,
L10().issueOrderConfirm,
icon: TablerIcons.send,
color: Colors.blue,
color: COLOR_ACTION,
acceptText: L10().issue,
onAccept: () async {
widget.order.issue().then((dynamic) {
@@ -208,7 +208,7 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
L10().completeOrder,
L10().completeOrderConfirm,
icon: TablerIcons.check,
color: Colors.green,
color: COLOR_SUCCESS,
acceptText: L10().complete,
onAccept: () async {
widget.order.completeOrder().then((dynamic) {
@@ -224,7 +224,7 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
L10().holdOrder,
L10().holdOrderConfirm,
icon: TablerIcons.player_pause,
color: Colors.orange,
color: COLOR_WARNING,
acceptText: L10().hold,
onAccept: () async {
widget.order.hold().then((dynamic) {
@@ -240,7 +240,7 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
L10().cancelOrder,
L10().cancelOrderConfirm,
icon: TablerIcons.circle_x,
color: Colors.red,
color: COLOR_DANGER,
acceptText: L10().cancel,
onAccept: () async {
widget.order.cancel().then((dynamic) {
@@ -256,7 +256,7 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
L10().allocateAuto,
L10().allocateAutoDetail,
icon: TablerIcons.arrow_autofit_down,
color: Colors.purple,
color: Theme.of(context).colorScheme.secondary,
acceptText: L10().allocate,
onAccept: () async {
widget.order.autoAllocate().then((dynamic) {
@@ -272,7 +272,7 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
L10().unallocateStock,
L10().buildOrderUnallocateDetail,
icon: TablerIcons.trash,
color: Colors.orange,
color: COLOR_WARNING,
acceptText: L10().unallocate,
onAccept: () async {
widget.order.unallocateAll().then((dynamic) {
@@ -399,7 +399,7 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
height: 32,
child: InvenTreeAPI().getThumbnail(part.thumbnail),
)
: const Icon(TablerIcons.box, color: Colors.blue),
: Icon(TablerIcons.box, color: COLOR_ACTION),
onTap: () {
part.goToDetailPage(context);
},
@@ -419,14 +419,14 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
);
// Progress bar
Color progressColor = Colors.blue;
Color progressColor = COLOR_PROGRESS;
if (widget.order.isComplete) {
progressColor = Colors.green;
progressColor = COLOR_SUCCESS;
} else if (widget.order.targetDate.isNotEmpty &&
DateTime.tryParse(widget.order.targetDate) != null &&
DateTime.tryParse(widget.order.targetDate)!.isBefore(DateTime.now())) {
progressColor = Colors.red;
progressColor = COLOR_DANGER;
}
tiles.add(
@@ -434,7 +434,9 @@ class _BuildOrderDetailState extends RefreshableState<BuildOrderDetailWidget> {
title: LinearProgressIndicator(
value: widget.order.progressPercent / 100.0,
color: progressColor,
backgroundColor: const Color(0xFFEEEEEE),
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
leading: const Icon(TablerIcons.chart_bar),
trailing: Text(
+2 -2
View File
@@ -60,7 +60,7 @@ class _BuildItemDetailWidgetState
// Unallocate button
buttons.add(
SpeedDialChild(
child: const Icon(TablerIcons.minus, color: Colors.red),
child: Icon(TablerIcons.minus, color: COLOR_DANGER),
label: L10().unallocate,
onTap: () async {
_unallocateStock(context);
@@ -99,7 +99,7 @@ class _BuildItemDetailWidgetState
L10().unallocateStock,
L10().unallocateStockConfirm,
icon: TablerIcons.minus,
color: Colors.red,
color: COLOR_DANGER,
acceptText: L10().unallocate,
onAccept: () async {
widget.item.delete().then((result) {
+2 -1
View File
@@ -3,6 +3,7 @@ import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/widget/progress.dart";
import "package:inventree/api.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/l10.dart";
import "package:inventree/inventree/build.dart";
@@ -149,7 +150,7 @@ class BuildOrderListItem extends StatelessWidget {
DateTime.tryParse(
order.targetDate,
)!.isBefore(DateTime.now()))
? Colors.red
? COLOR_DANGER
: null,
),
),
+2 -1
View File
@@ -3,6 +3,7 @@ import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/api.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/l10.dart";
import "package:inventree/inventree/company.dart";
@@ -55,7 +56,7 @@ class _CompanyListWidgetState extends RefreshableState<CompanyListWidget> {
if (InvenTreeAPI().checkPermission("company", "add")) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_plus, color: Colors.green),
child: Icon(TablerIcons.circle_plus, color: COLOR_SUCCESS),
label: L10().companyAdd,
onTap: () {
_addCompany(context);
+130 -73
View File
@@ -3,11 +3,11 @@ import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:one_context/one_context.dart";
import "package:inventree/api.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/helpers.dart";
import "package:inventree/l10.dart";
import "package:inventree/preferences.dart";
import "package:inventree/widget/snacks.dart";
/*
* Launch a dialog allowing the user to select from a list of options
@@ -113,85 +113,153 @@ Future<void> confirmationDialog(
);
}
/*
* Convert a raw API field name (snake_case) into a human-readable label,
* e.g. "target_date" -> "Target Date"
*/
String _humanizeFieldName(String field) {
return field
.split("_")
.where((word) => word.isNotEmpty)
.map((word) => word[0].toUpperCase() + word.substring(1))
.join(" ");
}
/*
* Build the body content for an error dialog, given either a plain
* description string or a structured APIResponse.
*/
Widget _buildErrorContent(String description, APIResponse? response) {
List<Widget> children = [];
if (description.isNotEmpty) {
children.add(Text(description));
} else if (response != null) {
final Map<String, dynamic> data = response.isMap() ? response.asMap() : {};
if (data["detail"] is String) {
// Standard DRF shape for auth / permission / not-found / throttle errors
children.add(Text(data["detail"] as String));
} else {
// Non-field errors (form-level validation failures)
List<String> nonFieldErrors = [];
for (String key in ["detail", "non_field_errors", "__all__", "errors"]) {
dynamic value = data[key];
if (value is String) {
nonFieldErrors.add(value);
} else if (value is List) {
nonFieldErrors.addAll(value.map((e) => e.toString()));
}
}
for (String error in nonFieldErrors) {
children.add(
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(error),
),
);
}
// Per-field validation errors (typically a 400 response)
if (response.statusCode == 400 && response.data is Map<String, dynamic>) {
for (String field in data.keys) {
if ([
"detail",
"non_field_errors",
"__all__",
"errors",
].contains(field)) {
continue;
}
dynamic error = data[field];
List<String> messages = error is List
? error.map((e) => e.toString()).toList()
: [error.toString()];
children.add(
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_humanizeFieldName(field),
style: const TextStyle(fontWeight: FontWeight.bold),
),
for (String message in messages) Text(message),
],
),
),
);
}
}
// Nothing recognized above - fall back to a labeled raw diagnostic dump
if (children.isEmpty) {
children.add(Text(statusCodeToString(response.statusCode)));
children.add(const SizedBox(height: 8));
children.add(
Text(
L10().responseData,
style: const TextStyle(fontWeight: FontWeight.bold),
),
);
children.add(
Text(
response.data.toString(),
style: const TextStyle(fontFamily: "monospace", fontSize: 12),
),
);
}
}
}
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: SingleChildScrollView(
child: Column(mainAxisSize: MainAxisSize.min, children: children),
),
);
}
/*
* Construct an error dialog showing information to the user
*
* @title = Title to be displayed at the top of the dialog
* @description = Simple string description of error
* @data = Error response (e.g from server)
* @response = Error response (e.g from server)
*/
Future<void> showErrorDialog(
String title, {
String description = "",
APIResponse? response,
IconData icon = TablerIcons.exclamation_circle,
Color? color,
Function? onDismissed,
}) async {
List<Widget> children = [];
if (description.isNotEmpty) {
children.add(ListTile(title: Text(description)));
} else if (response != null) {
// Look for extra error information in the provided APIResponse object
switch (response.statusCode) {
case 400: // Bad request (typically bad input)
if (response.data is Map<String, dynamic>) {
for (String field in response.asMap().keys) {
dynamic error = response.data[field];
if (error is List) {
for (int ii = 0; ii < error.length; ii++) {
children.add(
ListTile(
title: Text(field),
subtitle: Text(error[ii].toString()),
),
);
}
} else {
children.add(
ListTile(
title: Text(field),
subtitle: Text(response.data[field].toString()),
),
);
}
}
} else {
children.add(
ListTile(
title: Text(L10().responseInvalid),
subtitle: Text(response.data.toString()),
),
);
}
default:
// Unhandled server response
children.add(
ListTile(
title: Text(L10().statusCode),
subtitle: Text(response.statusCode.toString()),
),
);
children.add(
ListTile(
title: Text(L10().responseData),
subtitle: Text(response.data.toString()),
),
);
}
}
if (!hasContext()) {
return;
}
final Color dialogColor = color ?? COLOR_DANGER;
OneContext()
.showDialog(
builder: (context) => SimpleDialog(
title: ListTile(title: Text(title), leading: Icon(icon)),
children: children,
builder: (context) => AlertDialog(
icon: Icon(icon, color: dialogColor),
iconColor: dialogColor,
title: Text(title),
content: _buildErrorContent(description, response),
actions: [
TextButton(
child: Text(L10().ok),
onPressed: () {
Navigator.pop(context);
},
),
],
),
)
.then((value) {
@@ -233,18 +301,7 @@ Future<void> showServerError(
description += "\nURL: $url";
showSnackIcon(
title,
success: false,
actionText: L10().details,
onAction: () {
showErrorDialog(
title,
description: description,
icon: TablerIcons.server,
);
},
);
showErrorDialog(title, description: description, icon: TablerIcons.server);
}
/*
+275 -42
View File
@@ -6,6 +6,7 @@ import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/api.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/inventree/build.dart";
import "package:inventree/inventree/part.dart";
import "package:inventree/inventree/update_check.dart";
import "package:inventree/inventree/purchase_order.dart";
@@ -26,9 +27,96 @@ import "package:inventree/widget/order/sales_order_list.dart";
import "package:inventree/widget/build/build_list.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/company_list.dart";
/*
* Build a small count badge with the given background/foreground colors,
* or null if there is nothing to show.
* Extracted as a top-level function (rather than a private State method) so it
* can be unit tested in isolation, without needing a live API connection.
*/
Widget? _buildCountBadge(
int? count, {
required IconData icon,
required Color background,
required Color foreground,
}) {
if (count == null || count <= 0) {
return null;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: foreground),
const SizedBox(width: 4),
Text(count.toString(), style: TextStyle(color: foreground)),
],
),
);
}
// "Overdue" is the urgent case - use the theme's error colors
Widget? buildOverdueBadge(BuildContext context, int? count) {
final ColorScheme colors = Theme.of(context).colorScheme;
return _buildCountBadge(
count,
icon: TablerIcons.calendar_exclamation,
background: colors.errorContainer,
foreground: colors.onErrorContainer,
);
}
// "Outstanding" is informational rather than urgent - use a less serious color
Widget? buildOutstandingBadge(BuildContext context, int? count) {
final ColorScheme colors = Theme.of(context).colorScheme;
return _buildCountBadge(
count,
icon: TablerIcons.progress,
background: colors.secondaryContainer,
foreground: colors.onSecondaryContainer,
);
}
/*
* Combine the "outstanding" and "overdue" badges for a single tile into one
* widget (side by side), or null if neither has anything to show.
*/
Widget? buildOrderBadges(
BuildContext context, {
int? outstandingCount,
int? overdueCount,
}) {
final List<Widget> badges = [];
final Widget? outstanding = buildOutstandingBadge(context, outstandingCount);
if (outstanding != null) {
badges.add(outstanding);
}
final Widget? overdue = buildOverdueBadge(context, overdueCount);
if (overdue != null) {
if (badges.isNotEmpty) {
badges.add(const SizedBox(width: 6));
}
badges.add(overdue);
}
if (badges.isEmpty) {
return null;
}
return Row(mainAxisSize: MainAxisSize.min, children: badges);
}
class InvenTreeHomePage extends StatefulWidget {
const InvenTreeHomePage({Key? key}) : super(key: key);
@@ -54,6 +142,8 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
// Reload the widget
});
}
_loadOrderCounts();
});
}
@@ -71,6 +161,15 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
// Selected user profile
UserProfile? _profile;
// Order counts (null = not loaded / not visible)
int? _buildOverdueCount;
int? _buildOutstandingCount;
int? _poOverdueCount;
int? _poOutstandingCount;
int? _soOverdueCount;
int? _soOutstandingCount;
int? _shipmentsPendingCount;
void _showParts(BuildContext context) {
if (!InvenTreeAPI().checkConnection()) return;
@@ -233,6 +332,107 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
as bool;
setState(() {});
_loadOrderCounts();
}
/*
* Load the "overdue" and "outstanding" counts for build / purchase / sales orders,
* to be displayed as badges against the relevant home screen tiles.
* Only requests counts for tiles which are actually visible to the user.
*
* Each count is a separate cheap query (count() uses limit=1 server-side),
* rather than fetching the full outstanding order list and counting locally -
* that would require fetching *all* outstanding orders to be accurate.
*/
Future<void> _loadOrderCounts() async {
if (!InvenTreeAPI().isConnected()) {
return;
}
final bool loadBuild =
homeShowBuild && InvenTreeAPI().checkRole("build", "view");
final bool loadPo = homeShowPo && InvenTreePurchaseOrder().canView;
final bool loadSo = homeShowSo && InvenTreeSalesOrder().canView;
final bool loadShipments =
homeShowShipments && InvenTreeSalesOrderShipment().canView;
int? buildOverdue;
int? buildOutstanding;
int? poOverdue;
int? poOutstanding;
int? soOverdue;
int? soOutstanding;
int? shipmentsPending;
final List<Future<void>> requests = [];
if (loadBuild) {
requests.add(
InvenTreeBuildOrder().count(filters: {"overdue": "true"}).then((c) {
buildOverdue = c;
}),
);
requests.add(
InvenTreeBuildOrder().count(filters: {"outstanding": "true"}).then((c) {
buildOutstanding = c;
}),
);
}
if (loadPo) {
requests.add(
InvenTreePurchaseOrder().count(filters: {"overdue": "true"}).then((c) {
poOverdue = c;
}),
);
requests.add(
InvenTreePurchaseOrder().count(filters: {"outstanding": "true"}).then((
c,
) {
poOutstanding = c;
}),
);
}
if (loadSo) {
requests.add(
InvenTreeSalesOrder().count(filters: {"overdue": "true"}).then((c) {
soOverdue = c;
}),
);
requests.add(
InvenTreeSalesOrder().count(filters: {"outstanding": "true"}).then((c) {
soOutstanding = c;
}),
);
}
if (loadShipments) {
requests.add(
InvenTreeSalesOrderShipment()
.count(filters: {"order_outstanding": "true", "shipped": "false"})
.then((c) {
shipmentsPending = c;
}),
);
}
await Future.wait(requests);
if (!mounted) {
return;
}
setState(() {
_buildOverdueCount = buildOverdue;
_buildOutstandingCount = buildOutstanding;
_poOverdueCount = poOverdue;
_poOutstandingCount = poOutstanding;
_soOverdueCount = soOverdue;
_soOutstandingCount = soOutstanding;
_shipmentsPendingCount = shipmentsPending;
});
}
Future<void> _loadProfile() async {
@@ -277,7 +477,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
child: ListTile(
leading: Icon(
icon,
color: connected && allowed ? COLOR_ACTION : Colors.grey,
color: connected && allowed ? COLOR_ACTION : COLOR_GRAY_LIGHT,
),
title: Text(label),
trailing: trailing,
@@ -362,6 +562,11 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
},
role: "build",
permission: "view",
trailing: buildOrderBadges(
context,
outstandingCount: _buildOutstandingCount,
overdueCount: _buildOverdueCount,
),
),
);
}
@@ -376,6 +581,11 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
callback: () {
_showPurchaseOrders(context);
},
trailing: buildOrderBadges(
context,
outstandingCount: _poOutstandingCount,
overdueCount: _poOverdueCount,
),
),
);
}
@@ -389,6 +599,11 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
callback: () {
_showSalesOrders(context);
},
trailing: buildOrderBadges(
context,
outstandingCount: _soOutstandingCount,
overdueCount: _soOverdueCount,
),
),
);
}
@@ -402,6 +617,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
callback: () {
_showPendingShipments(context);
},
trailing: buildOutstandingBadge(context, _shipmentsPendingCount),
),
);
}
@@ -420,22 +636,6 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
);
}
// TODO: Add these tiles back in once the features are fleshed out
/*
// Manufacturers
if (homeShowManufacturers) {
tiles.add(_listTile(
context,
L10().manufacturers,
TablerIcons.building_factory_2,
callback: () {
_showManufacturers(context);
}
));
}
*/
// Customers
if (homeShowCustomers) {
tiles.add(
@@ -473,7 +673,11 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
} else if (connecting) {
title = L10().serverConnecting;
subtitle = serverAddress;
leading = Spinner(icon: TablerIcons.loader_2, color: COLOR_PROGRESS);
leading = SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(color: COLOR_PROGRESS, strokeWidth: 2),
);
}
return Center(
@@ -523,12 +727,21 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
children: getListTiles(context),
childAspectRatio: aspect,
primary: false,
// Ensure the grid is always draggable, even if the tiles don't fill the viewport,
// so that "pull down to refresh" works regardless of screen size / tile count
physics: const AlwaysScrollableScrollPhysics(),
crossAxisSpacing: padding,
mainAxisSpacing: padding,
padding: EdgeInsets.all(padding),
);
}
// Refresh handler for "pull down to refresh" on the home screen
Future<void> _onRefresh() async {
await _loadProfile();
await _loadOrderCounts();
}
@override
Widget build(BuildContext context) {
var connected = InvenTreeAPI().isConnected();
@@ -545,38 +758,58 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage>
Text(L10().appTitle),
],
),
backgroundColor: COLOR_APP_BAR,
actions: [
IconButton(
icon: Stack(
children: [
Icon(TablerIcons.server),
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: connected
? COLOR_SUCCESS
: (connecting ? COLOR_PROGRESS : COLOR_DANGER),
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).scaffoldBackgroundColor,
width: 1.5,
InkWell(
onTap: _selectProfile,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (connected &&
(InvenTreeAPI().profile?.name ?? "").isNotEmpty)
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 120),
child: Padding(
padding: const EdgeInsets.only(right: 6),
child: Text(
InvenTreeAPI().profile!.name,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
),
Stack(
children: [
Icon(TablerIcons.server),
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: connected
? COLOR_SUCCESS
: (connecting ? COLOR_PROGRESS : COLOR_DANGER),
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).scaffoldBackgroundColor,
width: 1.5,
),
),
),
),
],
),
),
],
],
),
),
onPressed: _selectProfile,
),
],
),
drawer: InvenTreeDrawer(context),
body: getBody(context),
body: RefreshIndicator(onRefresh: _onRefresh, child: getBody(context)),
bottomNavigationBar: InvenTreeAPI().isConnected()
? buildBottomAppBar(context, homeKey)
: null,
+2 -1
View File
@@ -2,6 +2,7 @@ import "package:flutter/material.dart";
import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/l10.dart";
import "package:inventree/inventree/model.dart";
import "package:inventree/inventree/purchase_order.dart";
@@ -53,7 +54,7 @@ class _PurchaseOrderExtraLineListWidgetState
if (widget.order.canEdit) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_plus, color: Colors.green),
child: Icon(TablerIcons.circle_plus, color: COLOR_SUCCESS),
label: L10().lineItemAdd,
onTap: () {
_addLineItem(context);
+1 -1
View File
@@ -67,7 +67,7 @@ class _POLineDetailWidgetState extends RefreshableState<POLineDetailWidget> {
if (!widget.item.isComplete) {
buttons.add(
SpeedDialChild(
child: Icon(TablerIcons.transition_right, color: Colors.blue),
child: Icon(TablerIcons.transition_right, color: COLOR_ACTION),
label: L10().receiveItem,
onTap: () async {
receiveLineItem(context);
+7 -7
View File
@@ -95,7 +95,7 @@ class _PurchaseOrderDetailState
if (showCameraShortcut && widget.order.canEdit) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.camera, color: Colors.blue),
child: Icon(TablerIcons.camera, color: COLOR_ACTION),
label: L10().takePicture,
onTap: () async {
_uploadImage(context);
@@ -108,7 +108,7 @@ class _PurchaseOrderDetailState
if (widget.order.isPending) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_plus, color: Colors.green),
child: Icon(TablerIcons.circle_plus, color: COLOR_SUCCESS),
label: L10().lineItemAdd,
onTap: () async {
_addLineItem(context);
@@ -118,7 +118,7 @@ class _PurchaseOrderDetailState
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.send, color: Colors.blue),
child: Icon(TablerIcons.send, color: COLOR_ACTION),
label: L10().issueOrder,
onTap: () async {
_issueOrder(context);
@@ -130,7 +130,7 @@ class _PurchaseOrderDetailState
if (widget.order.isOpen && !widget.order.isPending) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_check, color: Colors.green),
child: Icon(TablerIcons.circle_check, color: COLOR_SUCCESS),
label: L10().completeOrder,
onTap: () async {
_completeOrder(context);
@@ -142,7 +142,7 @@ class _PurchaseOrderDetailState
if (widget.order.isOpen) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_x, color: Colors.red),
child: Icon(TablerIcons.circle_x, color: COLOR_DANGER),
label: L10().cancelOrder,
onTap: () async {
_cancelOrder(context);
@@ -193,7 +193,7 @@ class _PurchaseOrderDetailState
L10().issueOrder,
L10().issueOrderConfirm,
icon: TablerIcons.send,
color: Colors.blue,
color: COLOR_ACTION,
acceptText: L10().issue,
onAccept: () async {
widget.order.issueOrder().then((dynamic) {
@@ -227,7 +227,7 @@ class _PurchaseOrderDetailState
L10().cancelOrder,
L10().cancelOrderConfirm,
icon: TablerIcons.circle_x,
color: Colors.red,
color: COLOR_DANGER,
acceptText: L10().cancel,
onAccept: () async {
widget.order.cancelOrder().then((dynamic) {
+7 -7
View File
@@ -127,7 +127,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
L10().issueOrder,
L10().issueOrderConfirm,
icon: TablerIcons.send,
color: Colors.blue,
color: COLOR_ACTION,
acceptText: L10().issue,
onAccept: () async {
widget.order.issueOrder().then((dynamic) {
@@ -143,7 +143,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
L10().cancelOrder,
L10().cancelOrderConfirm,
icon: TablerIcons.circle_x,
color: Colors.red,
color: COLOR_DANGER,
acceptText: L10().cancel,
onAccept: () async {
await widget.order.cancelOrder().then((dynamic) {
@@ -160,7 +160,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
if (showCameraShortcut && widget.order.canEdit) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.camera, color: Colors.blue),
child: Icon(TablerIcons.camera, color: COLOR_ACTION),
label: L10().takePicture,
onTap: () async {
_uploadImage(context);
@@ -172,7 +172,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
if (widget.order.isPending) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.send, color: Colors.blue),
child: Icon(TablerIcons.send, color: COLOR_ACTION),
label: L10().issueOrder,
onTap: () async {
_issueOrder(context);
@@ -184,7 +184,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
if (widget.order.isOpen) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_x, color: Colors.red),
child: Icon(TablerIcons.circle_x, color: COLOR_DANGER),
label: L10().cancelOrder,
onTap: () async {
_cancelOrder(context);
@@ -198,7 +198,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
InvenTreeSOLineItem().canCreate) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_plus, color: Colors.green),
child: Icon(TablerIcons.circle_plus, color: COLOR_SUCCESS),
label: L10().lineItemAdd,
onTap: () async {
_addLineItem(context);
@@ -208,7 +208,7 @@ class _SalesOrderDetailState extends RefreshableState<SalesOrderDetailWidget> {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_plus, color: Colors.green),
child: Icon(TablerIcons.circle_plus, color: COLOR_SUCCESS),
label: L10().shipmentAdd,
onTap: () async {
_addShipment(context);
+2 -1
View File
@@ -2,6 +2,7 @@ import "package:flutter/material.dart";
import "package:flutter_speed_dial/flutter_speed_dial.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/l10.dart";
import "package:inventree/inventree/model.dart";
@@ -55,7 +56,7 @@ class _SalesOrderExtraLineListWidgetState
if (widget.order.canEdit) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_plus, color: Colors.green),
child: Icon(TablerIcons.circle_plus, color: COLOR_SUCCESS),
label: L10().lineItemAdd,
onTap: () {
_addLineItem(context);
+1 -1
View File
@@ -111,7 +111,7 @@ class _SOLineDetailWidgetState extends RefreshableState<SoLineDetailWidget> {
if (order != null && order!.isOpen) {
buttons.add(
SpeedDialChild(
child: Icon(TablerIcons.transition_right, color: Colors.blue),
child: Icon(TablerIcons.transition_right, color: COLOR_ACTION),
label: L10().allocateStock,
onTap: () async {
_allocateStock(context);
+4 -4
View File
@@ -154,7 +154,7 @@ class _SOShipmentDetailWidgetState
if (showCameraShortcut) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.camera, color: Colors.blue),
child: Icon(TablerIcons.camera, color: COLOR_ACTION),
label: L10().takePicture,
onTap: () async {
_uploadImage(context);
@@ -167,7 +167,7 @@ class _SOShipmentDetailWidgetState
if (!widget.shipment.isChecked && !widget.shipment.isShipped) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.check, color: Colors.green),
child: Icon(TablerIcons.check, color: COLOR_SUCCESS),
label: L10().shipmentCheck,
onTap: () async {
widget.shipment
@@ -185,7 +185,7 @@ class _SOShipmentDetailWidgetState
if (widget.shipment.isChecked && !widget.shipment.isShipped) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.x, color: Colors.red),
child: Icon(TablerIcons.x, color: COLOR_DANGER),
label: L10().shipmentUncheck,
onTap: () async {
widget.shipment.update(values: {"checked_by": null}).then((_) {
@@ -201,7 +201,7 @@ class _SOShipmentDetailWidgetState
if (!widget.shipment.isShipped) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.truck_delivery, color: Colors.green),
child: Icon(TablerIcons.truck_delivery, color: COLOR_SUCCESS),
label: L10().shipmentSend,
onTap: () async {
_sendShipment(context);
+1 -1
View File
@@ -22,7 +22,7 @@ Widget ProgressBar(double value, {double maximum = 1.0}) {
return LinearProgressIndicator(
value: v,
backgroundColor: Colors.grey,
backgroundColor: COLOR_GRAY_LIGHT,
color: v >= 1 ? COLOR_SUCCESS : COLOR_WARNING,
);
}
-1
View File
@@ -56,7 +56,6 @@ mixin BaseWidgetProperties {
centerTitle: false,
bottom: tabs.isEmpty ? null : TabBar(tabs: tabs),
title: Text(getAppBarTitle()),
backgroundColor: COLOR_APP_BAR,
actions: appBarActions(context),
leading: backButton(context, key),
);
+4 -3
View File
@@ -4,6 +4,7 @@ import "package:flutter/material.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:one_context/one_context.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/helpers.dart";
import "package:inventree/l10.dart";
@@ -33,16 +34,16 @@ void showSnackIcon(
_currentSnackOverlay?.remove();
_currentSnackOverlay = null;
Color backgroundColor = Colors.deepOrange;
Color backgroundColor = COLOR_DANGER;
if (success != null && success == true) {
backgroundColor = Colors.lightGreen;
backgroundColor = COLOR_SUCCESS;
if (icon == null && onAction == null) {
icon = TablerIcons.circle_check;
}
} else if (success != null && success == false) {
backgroundColor = Colors.deepOrange;
backgroundColor = COLOR_DANGER;
if (icon == null && onAction == null) {
icon = TablerIcons.exclamation_circle;
-45
View File
@@ -1,45 +0,0 @@
import "package:flutter/material.dart";
import "package:inventree/app_colors.dart";
class Spinner extends StatefulWidget {
const Spinner({
this.color = COLOR_GRAY_LIGHT,
Key? key,
required this.icon,
this.duration = const Duration(milliseconds: 1800),
}) : super(key: key);
final IconData? icon;
final Duration duration;
final Color color;
@override
_SpinnerState createState() => _SpinnerState();
}
class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin {
late AnimationController? _controller;
Widget? _child;
@override
void initState() {
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 2000),
)..repeat();
_child = Icon(widget.icon, color: widget.color);
super.initState();
}
@override
void dispose() {
_controller!.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RotationTransition(turns: _controller!, child: _child);
}
}
+5 -5
View File
@@ -94,7 +94,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
if (!widget.item.isSerialized()) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_check, color: Colors.blue),
child: Icon(TablerIcons.circle_check, color: COLOR_ACTION),
label: L10().countStock,
onTap: _countStockDialog,
),
@@ -102,7 +102,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_minus, color: Colors.red),
child: Icon(TablerIcons.circle_minus, color: COLOR_DANGER),
label: L10().removeStock,
onTap: _removeStockDialog,
),
@@ -110,7 +110,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.circle_plus, color: Colors.green),
child: Icon(TablerIcons.circle_plus, color: COLOR_SUCCESS),
label: L10().addStock,
onTap: _addStockDialog,
),
@@ -144,7 +144,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
if (widget.item.canDelete) {
actions.add(
SpeedDialChild(
child: Icon(TablerIcons.trash, color: Colors.red),
child: Icon(TablerIcons.trash, color: COLOR_DANGER),
label: L10().stockItemDelete,
onTap: () {
_deleteItem(context);
@@ -322,7 +322,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
L10().stockItemDelete,
L10().stockItemDeleteConfirm,
icon: TablerIcons.trash,
color: Colors.red,
color: COLOR_DANGER,
acceptText: L10().delete,
onAccept: () async {
final bool result = await widget.item.delete();
@@ -189,7 +189,7 @@ class _StockItemTestResultDisplayState
String _value = "";
String _date = "";
Widget _icon = Icon(TablerIcons.help_circle, color: Colors.lightBlue);
Widget _icon = Icon(TablerIcons.help_circle, color: COLOR_GRAY_LIGHT);
bool _valueRequired = false;
bool _attachmentRequired = false;
@@ -214,7 +214,7 @@ class _StockItemTestResultDisplayState
}
if (!_hasResult) {
_icon = Icon(TablerIcons.help_circle, color: Colors.blue);
_icon = Icon(TablerIcons.help_circle, color: COLOR_GRAY_LIGHT);
} else if (_result == true) {
_icon = Icon(TablerIcons.circle_check, color: COLOR_SUCCESS);
} else if (_result == false) {