mirror of
https://github.com/inventree/inventree-app.git
synced 2025-06-12 18:25:26 +00:00
Stock display (#379)
* Display stock quantity more prominently * Cleanup search widget * Update for stock_detail widget * More tweaks * Change bottom bar icon * Display boolean parameters appropriately * Adds ability to edit part parameters * Bump icon size a bit * Improvements to filter options - Allow filtering by "option" type - To start with, filter stock by status code * Remove debug message * Remove getTriState method - No longer needed - Remove associated unit tests * Adjust filters based on server API version * Muted colors
This commit is contained in:
@ -154,8 +154,8 @@ class CheckBoxField extends FormField<bool> {
|
||||
onSaved: onSaved,
|
||||
initialValue: initial,
|
||||
builder: (FormFieldState<bool> state) {
|
||||
|
||||
return CheckboxListTile(
|
||||
//dense: state.hasError,
|
||||
title: label != null ? Text(label, style: labelStyle) : null,
|
||||
value: state.value,
|
||||
tristate: tristate,
|
||||
|
@ -38,44 +38,39 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
||||
// Override in implementing class
|
||||
String get prefix => "prefix_";
|
||||
|
||||
// Return a map of boolean filtering options available for this list
|
||||
// Should be overridden by an implementing subclass
|
||||
Map<String, Map<String, dynamic>> get filterOptions => {};
|
||||
|
||||
// Return the boolean value of a particular boolean filter
|
||||
Future<bool?> getBooleanFilterValue(String key) async {
|
||||
key = "${prefix}bool_${key}";
|
||||
Future<dynamic> getFilterValue(String key) async {
|
||||
key = "${prefix}filter_${key}";
|
||||
|
||||
Map<String, dynamic> opts = filterOptions[key] ?? {};
|
||||
dynamic backup = opts["default"];
|
||||
final result = await InvenTreeSettingsManager().getValue(key, backup);
|
||||
|
||||
bool? backup;
|
||||
dynamic v = opts["default"];
|
||||
|
||||
if (v is bool) {
|
||||
backup = v;
|
||||
}
|
||||
|
||||
final result = await InvenTreeSettingsManager().getTriState(key, backup);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Set the boolean value of a particular boolean filter
|
||||
Future<void> setBooleanFilterValue(String key, bool? value) async {
|
||||
key = "${prefix}bool_${key}";
|
||||
Future<void> setFilterValue(String key, dynamic value) async {
|
||||
key = "${prefix}filter_${key}";
|
||||
await InvenTreeSettingsManager().setValue(key, value);
|
||||
}
|
||||
|
||||
// Construct the boolean filter options for this list
|
||||
Future<Map<String, String>> constructBooleanFilters() async {
|
||||
Future<Map<String, String>> constructFilters() async {
|
||||
|
||||
Map<String, String> f = {};
|
||||
|
||||
for (String k in filterOptions.keys) {
|
||||
bool? value = await getBooleanFilterValue(k);
|
||||
dynamic value = await getFilterValue(k);
|
||||
|
||||
if (value is bool) {
|
||||
f[k] = value ? "true" : "false";
|
||||
// Skip null values
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
f[k] = value.toString();
|
||||
}
|
||||
|
||||
return f;
|
||||
@ -164,7 +159,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
||||
}
|
||||
};
|
||||
|
||||
// Add in boolean filter options
|
||||
// Add in selected filter options
|
||||
for (String key in filterOptions.keys) {
|
||||
Map<String, dynamic> opts = filterOptions[key] ?? {};
|
||||
|
||||
@ -172,17 +167,18 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
||||
String label = (opts["label"] ?? key) as String;
|
||||
String? help_text = opts["help_text"] as String?;
|
||||
|
||||
List<dynamic> choices = (opts["choices"] ?? []) as List<dynamic>;
|
||||
|
||||
bool tristate = (opts["tristate"] ?? true) as bool;
|
||||
|
||||
bool? v = await getBooleanFilterValue(key);
|
||||
dynamic v = await getFilterValue(key);
|
||||
|
||||
// Prevent null value if not tristate
|
||||
if (!tristate && v == null) {
|
||||
v = false;
|
||||
}
|
||||
|
||||
// Add in the particular field
|
||||
fields[key] = {
|
||||
Map<String, dynamic> filter = {
|
||||
"type": "boolean",
|
||||
"display_name": label,
|
||||
"label": label,
|
||||
@ -190,6 +186,16 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
||||
"value": v,
|
||||
"tristate": (opts["tristate"] ?? true) as bool,
|
||||
};
|
||||
|
||||
if (choices.isNotEmpty) {
|
||||
// Configure as a choice input
|
||||
filter["type"] = "choice";
|
||||
filter["choices"] = choices;
|
||||
|
||||
filter.remove("tristate");
|
||||
}
|
||||
|
||||
fields[key] = filter;
|
||||
}
|
||||
|
||||
// Launch an interactive form for the user to select options
|
||||
@ -211,16 +217,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
||||
|
||||
// Save boolean fields
|
||||
for (String key in filterOptions.keys) {
|
||||
|
||||
bool? v;
|
||||
|
||||
dynamic value = data[key];
|
||||
|
||||
if (value is bool) {
|
||||
v = value;
|
||||
}
|
||||
|
||||
await setBooleanFilterValue(key, v);
|
||||
await setFilterValue(key, data[key]);
|
||||
}
|
||||
|
||||
// Refresh data from the server
|
||||
@ -293,7 +290,7 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
||||
params["ordering"] = o;
|
||||
}
|
||||
|
||||
Map<String, String> f = await constructBooleanFilters();
|
||||
Map<String, String> f = await constructFilters();
|
||||
|
||||
if (f.isNotEmpty) {
|
||||
params.addAll(f);
|
||||
@ -348,6 +345,10 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
||||
void updateSearchTerm() {
|
||||
searchTerm = searchController.text;
|
||||
_pagingController.refresh();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
// Function to construct a single paginated item
|
||||
@ -409,19 +410,19 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta
|
||||
*/
|
||||
Widget buildSearchInput(BuildContext context) {
|
||||
return ListTile(
|
||||
trailing: orderingOptions.isEmpty ? null : GestureDetector(
|
||||
child: FaIcon(FontAwesomeIcons.sort, color: COLOR_ACTION),
|
||||
leading: orderingOptions.isEmpty ? null : GestureDetector(
|
||||
child: Icon(Icons.filter_list, color: COLOR_ACTION, size: 32),
|
||||
onTap: () async {
|
||||
_saveOrderingOptions(context);
|
||||
},
|
||||
),
|
||||
leading: GestureDetector(
|
||||
trailing: GestureDetector(
|
||||
child: FaIcon(
|
||||
searchController.text.isEmpty ? FontAwesomeIcons.magnifyingGlass : FontAwesomeIcons.deleteLeft,
|
||||
color: searchController.text.isNotEmpty ? COLOR_DANGER : null,
|
||||
color: searchController.text.isNotEmpty ? COLOR_DANGER : COLOR_ACTION,
|
||||
),
|
||||
onTap: () {
|
||||
if (searchController.text.isEmpty) {
|
||||
if (searchController.text.isNotEmpty) {
|
||||
searchController.clear();
|
||||
}
|
||||
updateSearchTerm();
|
||||
|
@ -228,20 +228,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Toggle the "star" status of this paricular part
|
||||
*/
|
||||
Future <void> _toggleStar(BuildContext context) async {
|
||||
|
||||
if (InvenTreePart().canView) {
|
||||
showLoadingOverlay(context);
|
||||
await part.update(values: {"starred": "${!part.starred}"});
|
||||
hideLoadingOverlay();
|
||||
refresh(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _editPartDialog(BuildContext context) {
|
||||
|
||||
part.editForm(
|
||||
@ -259,13 +245,11 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
child: ListTile(
|
||||
title: Text("${part.fullname}"),
|
||||
subtitle: Text("${part.description}"),
|
||||
trailing: IconButton(
|
||||
icon: FaIcon(part.starred ? FontAwesomeIcons.solidStar : FontAwesomeIcons.star,
|
||||
color: part.starred ? Colors.yellowAccent : null,
|
||||
),
|
||||
onPressed: () {
|
||||
_toggleStar(context);
|
||||
},
|
||||
trailing: Text(
|
||||
part.stockString(),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
)
|
||||
),
|
||||
leading: GestureDetector(
|
||||
child: api.getImage(part.thumbnail),
|
||||
|
@ -133,7 +133,13 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> {
|
||||
return ListTile(
|
||||
title: Text(part.fullname),
|
||||
subtitle: Text(part.description),
|
||||
trailing: Text(part.stockString()),
|
||||
trailing: Text(
|
||||
part.stockString(),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold
|
||||
)
|
||||
),
|
||||
leading: InvenTreeAPI().getThumbnail(part.thumbnail),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
|
||||
|
@ -4,6 +4,7 @@ import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/widget/paginator.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
|
||||
/*
|
||||
@ -75,7 +76,7 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi
|
||||
|
||||
@override
|
||||
Map<String, String> get orderingOptions => {
|
||||
// TODO
|
||||
|
||||
};
|
||||
|
||||
@override
|
||||
@ -91,6 +92,22 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi
|
||||
return page;
|
||||
}
|
||||
|
||||
Future<void> editParameter(InvenTreePartParameter parameter) async {
|
||||
|
||||
// Checkbox values are handled separately
|
||||
if (parameter.is_checkbox) {
|
||||
return;
|
||||
} else {
|
||||
parameter.editForm(
|
||||
context,
|
||||
L10().editParameter,
|
||||
onSuccess: (data) async {
|
||||
updateSearchTerm();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildItem(BuildContext context, InvenTreeModel model) {
|
||||
|
||||
@ -99,7 +116,28 @@ class _PaginatedParameterState extends PaginatedSearchState<PaginatedParameterLi
|
||||
return ListTile(
|
||||
title: Text(parameter.name),
|
||||
subtitle: Text(parameter.description),
|
||||
trailing: Text(parameter.valueString),
|
||||
trailing: parameter.is_checkbox
|
||||
? Switch(
|
||||
value: parameter.as_bool,
|
||||
onChanged: (bool value) {
|
||||
if (parameter.canEdit) {
|
||||
showLoadingOverlay(context);
|
||||
parameter.update(
|
||||
values: {
|
||||
"data": value.toString()
|
||||
}
|
||||
).then((value) async{
|
||||
hideLoadingOverlay();
|
||||
updateSearchTerm();
|
||||
});
|
||||
}
|
||||
},
|
||||
) : Text(parameter.valueString),
|
||||
onTap: parameter.is_checkbox ? null : () async {
|
||||
if (parameter.canEdit) {
|
||||
editParameter(parameter);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -71,7 +71,7 @@ mixin BaseWidgetProperties {
|
||||
*/
|
||||
BottomAppBar? buildBottomAppBar(BuildContext context, GlobalKey<ScaffoldState> key) {
|
||||
|
||||
const double iconSize = 32;
|
||||
const double iconSize = 40;
|
||||
|
||||
List<Widget> icons = [
|
||||
IconButton(
|
||||
@ -98,7 +98,7 @@ mixin BaseWidgetProperties {
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.qr_code_scanner, color: COLOR_ACTION),
|
||||
icon: Icon(Icons.barcode_reader, color: COLOR_ACTION),
|
||||
iconSize: iconSize,
|
||||
onPressed: () {
|
||||
if (InvenTreeAPI().checkConnection()) {
|
||||
|
@ -577,8 +577,9 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
subtitle: Text("${widget.item.partDescription}"),
|
||||
leading: InvenTreeAPI().getThumbnail(widget.item.partImage),
|
||||
trailing: Text(
|
||||
api.StockStatus.label(widget.item.status),
|
||||
widget.item.quantityString(),
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: api.StockStatus.color(widget.item.status),
|
||||
)
|
||||
),
|
||||
@ -615,6 +616,41 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
return tiles;
|
||||
}
|
||||
|
||||
// Location information
|
||||
if ((widget.item.locationId > 0) && (widget.item.locationName.isNotEmpty)) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().stockLocation),
|
||||
subtitle: Text("${widget.item.locationPathString}"),
|
||||
leading: FaIcon(
|
||||
FontAwesomeIcons.locationDot,
|
||||
color: COLOR_ACTION,
|
||||
),
|
||||
onTap: () async {
|
||||
if (widget.item.locationId > 0) {
|
||||
|
||||
showLoadingOverlay(context);
|
||||
var loc = await InvenTreeStockLocation().get(widget.item.locationId);
|
||||
hideLoadingOverlay();
|
||||
|
||||
if (loc is InvenTreeStockLocation) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) => LocationDisplayWidget(loc)));
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().stockLocation),
|
||||
leading: FaIcon(FontAwesomeIcons.locationDot),
|
||||
subtitle: Text(L10().locationNotSet),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Quantity information
|
||||
if (widget.item.isSerialized()) {
|
||||
tiles.add(
|
||||
@ -634,40 +670,19 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
// Location information
|
||||
if ((widget.item.locationId > 0) && (widget.item.locationName.isNotEmpty)) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().stockLocation),
|
||||
subtitle: Text("${widget.item.locationPathString}"),
|
||||
leading: FaIcon(
|
||||
FontAwesomeIcons.locationDot,
|
||||
color: COLOR_ACTION,
|
||||
),
|
||||
onTap: () async {
|
||||
if (widget.item.locationId > 0) {
|
||||
|
||||
showLoadingOverlay(context);
|
||||
var loc = await InvenTreeStockLocation().get(widget.item.locationId);
|
||||
hideLoadingOverlay();
|
||||
|
||||
if (loc is InvenTreeStockLocation) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) => LocationDisplayWidget(loc)));
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().stockLocation),
|
||||
leading: FaIcon(FontAwesomeIcons.locationDot),
|
||||
subtitle: Text(L10().locationNotSet),
|
||||
// Stock item status information
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().status),
|
||||
leading: FaIcon(FontAwesomeIcons.circleInfo),
|
||||
trailing: Text(
|
||||
api.StockStatus.label(widget.item.status),
|
||||
style: TextStyle(
|
||||
color: api.StockStatus.color(widget.item.status),
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Supplier part information (if available)
|
||||
if (widget.item.supplierPartId > 0) {
|
||||
@ -676,7 +691,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
title: Text(L10().supplierPart),
|
||||
subtitle: Text(widget.item.supplierSKU),
|
||||
leading: FaIcon(FontAwesomeIcons.building, color: COLOR_ACTION),
|
||||
trailing: InvenTreeAPI().getThumbnail(widget.item.supplierImage),
|
||||
trailing: InvenTreeAPI().getThumbnail(widget.item.supplierImage, hideIfNull: true),
|
||||
onTap: () async {
|
||||
showLoadingOverlay(context);
|
||||
var sp = await InvenTreeSupplierPart().get(
|
||||
|
@ -79,24 +79,37 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
|
||||
};
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> get filterOptions => {
|
||||
"in_stock": {
|
||||
"default": true,
|
||||
"label": L10().filterInStock,
|
||||
"help_text": L10().filterInStockDetail,
|
||||
"tristate": true,
|
||||
},
|
||||
"cascade": {
|
||||
"default": false,
|
||||
"label": L10().includeSublocations,
|
||||
"help_text": L10().includeSublocationsDetail,
|
||||
"tristate": false,
|
||||
},
|
||||
"serialized": {
|
||||
"label": L10().filterSerialized,
|
||||
"help_text": L10().filterSerializedDetail,
|
||||
Map<String, Map<String, dynamic>> get filterOptions {
|
||||
Map<String, Map<String, dynamic>> filters = {
|
||||
"in_stock": {
|
||||
"default": true,
|
||||
"label": L10().filterInStock,
|
||||
"help_text": L10().filterInStockDetail,
|
||||
"tristate": true,
|
||||
},
|
||||
"cascade": {
|
||||
"default": false,
|
||||
"label": L10().includeSublocations,
|
||||
"help_text": L10().includeSublocationsDetail,
|
||||
"tristate": false,
|
||||
},
|
||||
"serialized": {
|
||||
"label": L10().filterSerialized,
|
||||
"help_text": L10().filterSerializedDetail,
|
||||
},
|
||||
"status": {
|
||||
"label": L10().status,
|
||||
"help_text": L10().statusCode,
|
||||
"choices": InvenTreeAPI().StockStatus.choices,
|
||||
}
|
||||
};
|
||||
|
||||
if (!InvenTreeAPI().supportsStatusLabelEndpoints) {
|
||||
filters.remove("status");
|
||||
}
|
||||
};
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async {
|
||||
@ -125,6 +138,7 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt
|
||||
trailing: Text("${item.displayQuantity}",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: InvenTreeAPI().StockStatus.color(item.status),
|
||||
),
|
||||
),
|
||||
|
Reference in New Issue
Block a user