mirror of
https://github.com/inventree/inventree-app.git
synced 2026-09-09 03:10:14 +00:00
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:
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user