mirror of
https://github.com/inventree/inventree-app.git
synced 2025-04-28 13:36:50 +00:00
commit
51f3be899b
@ -1,6 +1,17 @@
|
||||
## InvenTree App Release Notes
|
||||
---
|
||||
|
||||
### 0.3.1 - July 2021
|
||||
---
|
||||
|
||||
- Adds new "API driven" forms
|
||||
- Improvements for Part editing form
|
||||
- Improvements for PartCategory editing form
|
||||
- Improvements for StockLocation editing form
|
||||
- Adds ability to edit StockItem
|
||||
- Display purchase price (where available) for StockItem
|
||||
- Updated translations
|
||||
|
||||
### 0.2.10 - July 2021
|
||||
---
|
||||
|
||||
|
@ -6,8 +6,8 @@ export "COCOAPODS_PARALLEL_CODE_SIGN=true"
|
||||
export "FLUTTER_TARGET=lib\main.dart"
|
||||
export "FLUTTER_BUILD_DIR=build"
|
||||
export "SYMROOT=${SOURCE_ROOT}/../build\ios"
|
||||
export "FLUTTER_BUILD_NAME=0.2.10"
|
||||
export "FLUTTER_BUILD_NUMBER=18"
|
||||
export "FLUTTER_BUILD_NAME=0.3.1"
|
||||
export "FLUTTER_BUILD_NUMBER=19"
|
||||
export "DART_OBFUSCATION=false"
|
||||
export "TRACK_WIDGET_CREATION=false"
|
||||
export "TREE_SHAKE_ICONS=false"
|
||||
|
29
lib/api.dart
29
lib/api.dart
@ -2,20 +2,20 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'package:inventree/inventree/sentry.dart';
|
||||
import 'package:inventree/user_profile.dart';
|
||||
import 'package:inventree/widget/snacks.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
|
||||
import 'package:inventree/widget/dialogs.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:inventree/inventree/sentry.dart';
|
||||
import 'package:inventree/user_profile.dart';
|
||||
import 'package:inventree/widget/snacks.dart';
|
||||
|
||||
|
||||
/**
|
||||
@ -93,7 +93,7 @@ class InvenTreeFileService extends FileService {
|
||||
class InvenTreeAPI {
|
||||
|
||||
// Minimum required API version for server
|
||||
static const _minApiVersion = 6;
|
||||
static const _minApiVersion = 7;
|
||||
|
||||
// Endpoint for requesting an API token
|
||||
static const _URL_GET_TOKEN = "user/token/";
|
||||
@ -128,7 +128,13 @@ class InvenTreeAPI {
|
||||
|
||||
String get imageUrl => _makeUrl("/image/");
|
||||
|
||||
String makeApiUrl(String endpoint) => _makeUrl("/api/" + endpoint);
|
||||
String makeApiUrl(String endpoint) {
|
||||
if (endpoint.startsWith("/api/") || endpoint.startsWith("api/")) {
|
||||
return _makeUrl(endpoint);
|
||||
} else {
|
||||
return _makeUrl("/api/" + endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
String makeUrl(String endpoint) => _makeUrl(endpoint);
|
||||
|
||||
@ -431,7 +437,7 @@ class InvenTreeAPI {
|
||||
|
||||
|
||||
// Perform a PATCH request
|
||||
Future<APIResponse> patch(String url, {Map<String, String> body = const {}, int expectedStatusCode=200}) async {
|
||||
Future<APIResponse> patch(String url, {Map<String, String> body = const {}, int? expectedStatusCode}) async {
|
||||
var _body = Map<String, String>();
|
||||
|
||||
// Copy across provided data
|
||||
@ -593,8 +599,6 @@ class InvenTreeAPI {
|
||||
|
||||
Uri? _uri = Uri.tryParse(_url);
|
||||
|
||||
print("apiRequest ${method} -> ${url}");
|
||||
|
||||
if (_uri == null) {
|
||||
showServerError(L10().invalidHost, L10().invalidHostDetails);
|
||||
return null;
|
||||
@ -621,12 +625,15 @@ class InvenTreeAPI {
|
||||
|
||||
return _request;
|
||||
} on SocketException catch (error) {
|
||||
print("SocketException at ${url}: ${error.toString()}");
|
||||
showServerError(L10().connectionRefused, error.toString());
|
||||
return null;
|
||||
} on TimeoutException {
|
||||
print("TimeoutException at ${url}");
|
||||
showTimeoutError();
|
||||
return null;
|
||||
} catch (error, stackTrace) {
|
||||
print("Server error at ${url}: ${error.toString()}");
|
||||
showServerError(L10().serverError, error.toString());
|
||||
sentryReportError(error, stackTrace);
|
||||
return null;
|
||||
@ -809,4 +816,4 @@ class InvenTreeAPI {
|
||||
cacheManager: manager,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
681
lib/api_form.dart
Normal file
681
lib/api_form.dart
Normal file
@ -0,0 +1,681 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:dropdown_search/dropdown_search.dart';
|
||||
|
||||
import 'package:inventree/api.dart';
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/inventree/stock.dart';
|
||||
import 'package:inventree/widget/fields.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:inventree/widget/snacks.dart';
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Class that represents a single "form field",
|
||||
* defined by the InvenTree API
|
||||
*/
|
||||
class APIFormField {
|
||||
|
||||
final _controller = TextEditingController();
|
||||
|
||||
// Constructor
|
||||
APIFormField(this.name, this.data);
|
||||
|
||||
// Name of this field
|
||||
final String name;
|
||||
|
||||
// JSON data which defines the field
|
||||
final dynamic data;
|
||||
|
||||
dynamic initial_data;
|
||||
|
||||
// Get the "api_url" associated with a related field
|
||||
String get api_url => data["api_url"] ?? "";
|
||||
|
||||
// Get the "model" associated with a related field
|
||||
String get model => data["model"] ?? "";
|
||||
|
||||
// Is this field hidden?
|
||||
bool get hidden => (data['hidden'] ?? false) as bool;
|
||||
|
||||
// Is this field read only?
|
||||
bool get readOnly => (data['read_only'] ?? false) as bool;
|
||||
|
||||
// Get the "value" as a string (look for "default" if not available)
|
||||
dynamic get value => (data['value'] ?? data['default']);
|
||||
|
||||
// Get the "default" as a string
|
||||
dynamic get defaultValue => data['default'];
|
||||
|
||||
Map<String, String> get filters {
|
||||
|
||||
Map<String, String> _filters = {};
|
||||
|
||||
// Start with the provided "model" filters
|
||||
if (data.containsKey("filters")) {
|
||||
|
||||
dynamic f = data["filters"];
|
||||
|
||||
if (f is Map) {
|
||||
f.forEach((key, value) {
|
||||
_filters[key] = value.toString();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Now, look at the provided "instance_filters"
|
||||
if (data.containsKey("instance_filters")) {
|
||||
|
||||
dynamic f = data["instance_filters"];
|
||||
|
||||
if (f is Map) {
|
||||
f.forEach((key, value) {
|
||||
_filters[key] = value.toString();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return _filters;
|
||||
|
||||
}
|
||||
|
||||
bool hasErrors() => errorMessages().length > 0;
|
||||
|
||||
// Return the error message associated with this field
|
||||
List<String> errorMessages() {
|
||||
List<dynamic> errors = data['errors'] ?? [];
|
||||
|
||||
List<String> messages = [];
|
||||
|
||||
for (dynamic error in errors) {
|
||||
messages.add(error.toString());
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
// Is this field required?
|
||||
bool get required => (data['required'] ?? false) as bool;
|
||||
|
||||
String get type => (data['type'] ?? '').toString();
|
||||
|
||||
String get label => (data['label'] ?? '').toString();
|
||||
|
||||
String get helpText => (data['help_text'] ?? '').toString();
|
||||
|
||||
String get placeholderText => (data['placeholder'] ?? '').toString();
|
||||
|
||||
List<dynamic> get choices => data["choices"] ?? [];
|
||||
|
||||
Future<void> loadInitialData() async {
|
||||
|
||||
// Only for "related fields"
|
||||
if (type != "related field") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Null value? No point!
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int? pk = int.tryParse(value.toString());
|
||||
|
||||
if (pk == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String url = api_url + "/" + pk.toString() + "/";
|
||||
|
||||
final APIResponse response = await InvenTreeAPI().get(
|
||||
url,
|
||||
params: filters,
|
||||
);
|
||||
|
||||
if (response.isValid()) {
|
||||
initial_data = response.data;
|
||||
}
|
||||
}
|
||||
|
||||
// Construct a widget for this input
|
||||
Widget constructField() {
|
||||
switch (type) {
|
||||
case "string":
|
||||
case "url":
|
||||
return _constructString();
|
||||
case "boolean":
|
||||
return _constructBoolean();
|
||||
case "related field":
|
||||
return _constructRelatedField();
|
||||
case "choice":
|
||||
return _constructChoiceField();
|
||||
default:
|
||||
return ListTile(
|
||||
title: Text(
|
||||
"Unsupported field type: '${type}'",
|
||||
style: TextStyle(
|
||||
color: COLOR_DANGER,
|
||||
fontStyle: FontStyle.italic),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _constructChoiceField() {
|
||||
|
||||
dynamic _initial;
|
||||
|
||||
// Check if the current value is within the allowed values
|
||||
for (var opt in choices) {
|
||||
if (opt['value'] == value) {
|
||||
_initial = opt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return DropdownSearch<dynamic>(
|
||||
mode: Mode.BOTTOM_SHEET,
|
||||
showSelectedItem: false,
|
||||
selectedItem: _initial,
|
||||
items: choices,
|
||||
label: label,
|
||||
hint: helpText,
|
||||
onChanged: null,
|
||||
autoFocusSearchBox: true,
|
||||
showClearButton: !required,
|
||||
itemAsString: (dynamic item) {
|
||||
return item['display_name'];
|
||||
},
|
||||
onSaved: (item) {
|
||||
if (item == null) {
|
||||
data['value'] = null;
|
||||
} else {
|
||||
data['value'] = item['value'];
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Construct an input for a related field
|
||||
Widget _constructRelatedField() {
|
||||
|
||||
return DropdownSearch<dynamic>(
|
||||
mode: Mode.BOTTOM_SHEET,
|
||||
showSelectedItem: true,
|
||||
selectedItem: initial_data,
|
||||
onFind: (String filter) async {
|
||||
|
||||
Map<String, String> _filters = {};
|
||||
|
||||
filters.forEach((key, value) {
|
||||
_filters[key] = value;
|
||||
});
|
||||
|
||||
_filters["search"] = filter;
|
||||
_filters["offset"] = "0";
|
||||
_filters["limit"] = "25";
|
||||
|
||||
final APIResponse response = await InvenTreeAPI().get(
|
||||
api_url,
|
||||
params: _filters
|
||||
);
|
||||
|
||||
if (response.isValid()) {
|
||||
|
||||
List<dynamic> results = [];
|
||||
|
||||
for (var result in response.data['results'] ?? []) {
|
||||
results.add(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
label: label,
|
||||
hint: helpText,
|
||||
onChanged: null,
|
||||
showClearButton: !required,
|
||||
itemAsString: (dynamic item) {
|
||||
return item['pathstring'];
|
||||
},
|
||||
dropdownBuilder: (context, item, itemAsString) {
|
||||
return _renderRelatedField(item, true, false);
|
||||
},
|
||||
popupItemBuilder: (context, item, isSelected) {
|
||||
return _renderRelatedField(item, isSelected, true);
|
||||
},
|
||||
onSaved: (item) {
|
||||
if (item != null) {
|
||||
data['value'] = item['pk'] ?? null;
|
||||
} else {
|
||||
data['value'] = null;
|
||||
}
|
||||
},
|
||||
isFilteredOnline: true,
|
||||
showSearchBox: true,
|
||||
autoFocusSearchBox: true,
|
||||
compareFn: (dynamic item, dynamic selectedItem) {
|
||||
// Comparison is based on the PK value
|
||||
|
||||
if (item == null || selectedItem == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return item['pk'] == selectedItem['pk'];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget _renderRelatedField(dynamic item, bool selected, bool extended) {
|
||||
// Render a "related field" based on the "model" type
|
||||
|
||||
if (item == null) {
|
||||
return Text(
|
||||
helpText,
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
switch (model) {
|
||||
case "partcategory":
|
||||
|
||||
var cat = InvenTreePartCategory.fromJson(item);
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
cat.pathstring,
|
||||
style: TextStyle(fontWeight: selected && extended ? FontWeight.bold : FontWeight.normal)
|
||||
),
|
||||
subtitle: extended ? Text(
|
||||
cat.description,
|
||||
style: TextStyle(fontWeight: selected ? FontWeight.bold : FontWeight.normal),
|
||||
) : null,
|
||||
);
|
||||
case "stocklocation":
|
||||
|
||||
var loc = InvenTreeStockLocation.fromJson(item);
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
loc.pathstring,
|
||||
style: TextStyle(fontWeight: selected && extended ? FontWeight.bold : FontWeight.normal)
|
||||
),
|
||||
subtitle: extended ? Text(
|
||||
loc.description,
|
||||
style: TextStyle(fontWeight: selected ? FontWeight.bold : FontWeight.normal),
|
||||
) : null,
|
||||
);
|
||||
default:
|
||||
return ListTile(
|
||||
title: Text(
|
||||
"Unsupported model",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: COLOR_DANGER
|
||||
)
|
||||
),
|
||||
subtitle: Text("Model '${model}' rendering not supported"),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Construct a string input element
|
||||
Widget _constructString() {
|
||||
|
||||
return TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: required ? label + "*" : label,
|
||||
labelStyle: _labelStyle(),
|
||||
helperText: helpText,
|
||||
helperStyle: _helperStyle(),
|
||||
hintText: placeholderText,
|
||||
),
|
||||
initialValue: value ?? '',
|
||||
onSaved: (val) {
|
||||
data["value"] = val;
|
||||
},
|
||||
validator: (value) {
|
||||
if (required && (value == null || value.isEmpty)) {
|
||||
// return L10().valueCannotBeEmpty;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Construct a boolean input element
|
||||
Widget _constructBoolean() {
|
||||
|
||||
return CheckBoxField(
|
||||
label: label,
|
||||
labelStyle: _labelStyle(),
|
||||
helperText: helpText,
|
||||
helperStyle: _helperStyle(),
|
||||
initial: value,
|
||||
onSaved: (val) {
|
||||
data['value'] = val;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle _labelStyle() {
|
||||
return new TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
fontFamily: "arial",
|
||||
color: hasErrors() ? COLOR_DANGER : COLOR_GRAY,
|
||||
fontStyle: FontStyle.normal,
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle _helperStyle() {
|
||||
return new TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: hasErrors() ? COLOR_DANGER : COLOR_GRAY,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Extract field options from a returned OPTIONS request
|
||||
*/
|
||||
Map<String, dynamic> extractFields(APIResponse response) {
|
||||
|
||||
if (!response.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!response.data.containsKey("actions")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
var actions = response.data["actions"];
|
||||
|
||||
return actions["POST"] ?? actions["PUT"] ?? actions["PATCH"] ?? {};
|
||||
}
|
||||
|
||||
/*
|
||||
* Launch an API-driven form,
|
||||
* which uses the OPTIONS metadata (at the provided URL)
|
||||
* to determine how the form elements should be rendered!
|
||||
*
|
||||
* @param title is the title text to display on the form
|
||||
* @param url is the API URl to make the OPTIONS request to
|
||||
* @param fields is a map of fields to display (with optional overrides)
|
||||
* @param modelData is the (optional) existing modelData
|
||||
* @param method is the HTTP method to use to send the form data to the server (e.g. POST / PATCH)
|
||||
*/
|
||||
|
||||
Future<void> launchApiForm(BuildContext context, String title, String url, Map<String, dynamic> fields, {Map<String, dynamic> modelData = const {}, String method = "PATCH", Function? onSuccess, Function? onCancel}) async {
|
||||
|
||||
var options = await InvenTreeAPI().options(url);
|
||||
|
||||
// Invalid response from server
|
||||
if (!options.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var availableFields = extractFields(options);
|
||||
|
||||
if (availableFields.isEmpty) {
|
||||
// User does not have permission to perform this action
|
||||
showSnackIcon(
|
||||
L10().response403,
|
||||
icon: FontAwesomeIcons.userTimes,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct a list of APIFormField objects
|
||||
List<APIFormField> formFields = [];
|
||||
|
||||
// Iterate through the provided fields we wish to display
|
||||
for (String fieldName in fields.keys) {
|
||||
|
||||
// Check that the field is actually available at the API endpoint
|
||||
if (!availableFields.containsKey(fieldName)) {
|
||||
print("Field '${fieldName}' not available at '${url}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
var remoteField = availableFields[fieldName] ?? {};
|
||||
var localField = fields[fieldName] ?? {};
|
||||
|
||||
// Override defined field parameters, if provided
|
||||
for (String key in localField.keys) {
|
||||
// Special consideration must be taken here!
|
||||
if (key == "filters") {
|
||||
|
||||
if (!remoteField.containsKey("filters")) {
|
||||
remoteField["filters"] = {};
|
||||
}
|
||||
|
||||
var filters = localField["filters"];
|
||||
|
||||
if (filters is Map) {
|
||||
filters.forEach((key, value) {
|
||||
remoteField["filters"][key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
remoteField[key] = localField[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Update fields with existing model data
|
||||
for (String key in modelData.keys) {
|
||||
|
||||
dynamic value = modelData[key];
|
||||
|
||||
if (availableFields.containsKey(key)) {
|
||||
availableFields[key]['value'] = value;
|
||||
}
|
||||
}
|
||||
|
||||
formFields.add(APIFormField(fieldName, remoteField));
|
||||
}
|
||||
|
||||
// Grab existing data for each form field
|
||||
for (var field in formFields) {
|
||||
await field.loadInitialData();
|
||||
}
|
||||
|
||||
// Now, launch a new widget!
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => APIFormWidget(
|
||||
title,
|
||||
url,
|
||||
formFields,
|
||||
onSuccess: onSuccess,
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
class APIFormWidget extends StatefulWidget {
|
||||
|
||||
//! Form title to display
|
||||
final String title;
|
||||
|
||||
//! API URL
|
||||
final String url;
|
||||
|
||||
final List<APIFormField> fields;
|
||||
|
||||
Function? onSuccess;
|
||||
|
||||
APIFormWidget(
|
||||
this.title,
|
||||
this.url,
|
||||
this.fields,
|
||||
{
|
||||
Key? key,
|
||||
this.onSuccess,
|
||||
}
|
||||
) : super(key: key);
|
||||
|
||||
@override
|
||||
_APIFormWidgetState createState() => _APIFormWidgetState(title, url, fields, onSuccess);
|
||||
|
||||
}
|
||||
|
||||
|
||||
class _APIFormWidgetState extends State<APIFormWidget> {
|
||||
|
||||
final _formKey = new GlobalKey<FormState>();
|
||||
|
||||
String title;
|
||||
|
||||
String url;
|
||||
|
||||
List<APIFormField> fields;
|
||||
|
||||
Function? onSuccess;
|
||||
|
||||
_APIFormWidgetState(this.title, this.url, this.fields, this.onSuccess) : super();
|
||||
|
||||
List<Widget> _buildForm() {
|
||||
|
||||
List<Widget> widgets = [];
|
||||
|
||||
for (var field in fields) {
|
||||
|
||||
if (field.hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
widgets.add(field.constructField());
|
||||
|
||||
if (field.hasErrors()) {
|
||||
for (String error in field.errorMessages()) {
|
||||
widgets.add(
|
||||
ListTile(
|
||||
title: Text(
|
||||
error,
|
||||
style: TextStyle(
|
||||
color: COLOR_DANGER,
|
||||
fontStyle: FontStyle.italic,
|
||||
fontSize: 16,
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
Future<void> _save(BuildContext context) async {
|
||||
|
||||
// Package up the form data
|
||||
Map<String, String> _data = {};
|
||||
|
||||
for (var field in fields) {
|
||||
|
||||
dynamic value = field.value;
|
||||
|
||||
if (value == null) {
|
||||
_data[field.name] = "";
|
||||
} else {
|
||||
_data[field.name] = value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle "POST" forms too!!
|
||||
final response = await InvenTreeAPI().patch(
|
||||
url,
|
||||
body: _data,
|
||||
);
|
||||
|
||||
if (!response.isValid()) {
|
||||
// TODO: Display an error message!
|
||||
return;
|
||||
}
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
case 201:
|
||||
// Form was successfully validated by the server
|
||||
|
||||
// Hide this form
|
||||
Navigator.pop(context);
|
||||
|
||||
// TODO: Display a snackBar
|
||||
|
||||
// Run custom onSuccess function
|
||||
var successFunc = onSuccess;
|
||||
|
||||
if (successFunc != null) {
|
||||
successFunc();
|
||||
}
|
||||
return;
|
||||
case 400:
|
||||
// Form submission / validation error
|
||||
|
||||
// Update field errors
|
||||
for (var field in fields) {
|
||||
field.data['errors'] = response.data[field.name];
|
||||
}
|
||||
break;
|
||||
// TODO: Other status codes?
|
||||
}
|
||||
|
||||
setState(() {
|
||||
// Refresh the form
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.save),
|
||||
onPressed: () {
|
||||
|
||||
if (_formKey.currentState!.validate()) {
|
||||
_formKey.currentState!.save();
|
||||
|
||||
_save(context);
|
||||
}
|
||||
},
|
||||
)
|
||||
]
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildForm(),
|
||||
),
|
||||
padding: EdgeInsets.all(16),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
}
|
19
lib/app_colors.dart
Normal file
19
lib/app_colors.dart
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
const Color COLOR_GRAY = Color.fromRGBO(50, 50, 50, 1);
|
||||
const Color COLOR_GRAY_LIGHT = Color.fromRGBO(150, 150, 150, 1);
|
||||
|
||||
const Color COLOR_CLICK = Color.fromRGBO(175, 150, 100, 0.9);
|
||||
|
||||
const Color COLOR_BLUE = Color.fromRGBO(0, 0, 250, 1);
|
||||
|
||||
const Color COLOR_STAR = Color.fromRGBO(250, 250, 100, 1);
|
||||
|
||||
const Color COLOR_WARNING = Color.fromRGBO(250, 150, 50, 1);
|
||||
const Color COLOR_DANGER = Color.fromRGBO(250, 50, 50, 1);
|
||||
const Color COLOR_SUCCESS = Color.fromRGBO(50, 250, 50, 1);
|
||||
const Color COLOR_PROGRESS = Color.fromRGBO(50, 50, 250, 1);
|
||||
|
||||
const Color COLOR_SELECTED = Color.fromRGBO(0, 0, 0, 0.05);
|
@ -271,8 +271,7 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler {
|
||||
|
||||
final InvenTreeStockItem item;
|
||||
|
||||
StockItemBarcodeAssignmentHandler(this.item) {
|
||||
}
|
||||
StockItemBarcodeAssignmentHandler(this.item);
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().barcodeScanAssign;
|
||||
|
@ -126,8 +126,7 @@ class InvenTreeModel {
|
||||
}
|
||||
|
||||
// Return the API detail endpoint for this Model object
|
||||
String get url => "${URL}/${pk}/";
|
||||
|
||||
String get url => "${URL}/${pk}/".replaceAll("//", "/");
|
||||
|
||||
// Search this Model type in the database
|
||||
Future<List<InvenTreeModel>> search(BuildContext context, String searchTerm, {Map<String, String> filters = const {}}) async {
|
||||
@ -277,8 +276,6 @@ class InvenTreeModel {
|
||||
params[key] = filters[key] ?? '';
|
||||
}
|
||||
|
||||
print("LIST: $URL ${params.toString()}");
|
||||
|
||||
var response = await api.get(URL, params: params);
|
||||
|
||||
// A list of "InvenTreeModel" items
|
||||
@ -288,18 +285,22 @@ class InvenTreeModel {
|
||||
return results;
|
||||
}
|
||||
|
||||
// TODO - handle possible error cases:
|
||||
// - No data receieved
|
||||
// - Data is not a list of maps
|
||||
dynamic data;
|
||||
|
||||
for (var d in response.data) {
|
||||
if (response.data is List) {
|
||||
data = response.data;
|
||||
} else if (response.data.containsKey('results')) {
|
||||
data = response.data['results'];
|
||||
} else {
|
||||
data = [];
|
||||
}
|
||||
|
||||
for (var d in data) {
|
||||
|
||||
// Create a new object (of the current class type
|
||||
InvenTreeModel obj = createFromJson(d);
|
||||
|
||||
if (obj != null) {
|
||||
results.add(obj);
|
||||
}
|
||||
results.add(obj);
|
||||
}
|
||||
|
||||
return results;
|
||||
|
@ -60,6 +60,8 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
|
||||
String statusLabel(BuildContext context) {
|
||||
|
||||
// TODO: Delete me - The translated status values are provided by the API!
|
||||
|
||||
switch (status) {
|
||||
case OK:
|
||||
return L10().ok;
|
||||
@ -220,6 +222,15 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
|
||||
int get partId => jsondata['part'] ?? -1;
|
||||
|
||||
String get purchasePrice => jsondata['purchase_price'];
|
||||
|
||||
bool get hasPurchasePrice {
|
||||
|
||||
String pp = purchasePrice;
|
||||
|
||||
return pp.isNotEmpty && pp.trim() != "-";
|
||||
}
|
||||
|
||||
int get trackingItemCount => (jsondata['tracking_items'] ?? 0) as int;
|
||||
|
||||
// Date of last update
|
||||
@ -476,14 +487,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
||||
expectedStatusCode: 200
|
||||
);
|
||||
|
||||
print("Adjustment completed!");
|
||||
|
||||
if (response == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stock adjustment succeeded!
|
||||
return true;
|
||||
return response.isValid();
|
||||
}
|
||||
|
||||
Future<bool> countStock(BuildContext context, double q, {String? notes}) async {
|
||||
|
2
lib/l10n
2
lib/l10n
@ -1 +1 @@
|
||||
Subproject commit af4cd9026a96d44d60f9187119f5ce19c74738d3
|
||||
Subproject commit 46d08c9cc0043113fee5c0d134861c5d12554b71
|
@ -1,3 +1,4 @@
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/widget/dialogs.dart';
|
||||
import 'package:inventree/widget/fields.dart';
|
||||
import 'package:inventree/widget/spinner.dart';
|
||||
@ -55,7 +56,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
key: _addProfileKey,
|
||||
callback: () {
|
||||
if (createNew) {
|
||||
// TODO - create the new profile...
|
||||
|
||||
UserProfile profile = UserProfile(
|
||||
name: _name,
|
||||
server: _server,
|
||||
@ -219,7 +220,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
if ((InvenTreeAPI().profile?.key ?? '') != profile.key) {
|
||||
return FaIcon(
|
||||
FontAwesomeIcons.questionCircle,
|
||||
color: Color.fromRGBO(250, 150, 50, 1)
|
||||
color: COLOR_WARNING
|
||||
);
|
||||
}
|
||||
|
||||
@ -227,17 +228,17 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
if (InvenTreeAPI().isConnected()) {
|
||||
return FaIcon(
|
||||
FontAwesomeIcons.checkCircle,
|
||||
color: Color.fromRGBO(50, 250, 50, 1)
|
||||
color: COLOR_SUCCESS
|
||||
);
|
||||
} else if (InvenTreeAPI().isConnecting()) {
|
||||
return Spinner(
|
||||
icon: FontAwesomeIcons.spinner,
|
||||
color: Color.fromRGBO(50, 50, 250, 1),
|
||||
color: COLOR_PROGRESS,
|
||||
);
|
||||
} else {
|
||||
return FaIcon(
|
||||
FontAwesomeIcons.timesCircle,
|
||||
color: Color.fromRGBO(250, 50, 50, 1),
|
||||
color: COLOR_DANGER,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -255,7 +256,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
|
||||
title: Text(
|
||||
profile.name,
|
||||
),
|
||||
tileColor: profile.selected ? Color.fromRGBO(0, 0, 0, 0.05) : null,
|
||||
tileColor: profile.selected ? COLOR_SELECTED : null,
|
||||
subtitle: Text("${profile.server}"),
|
||||
trailing: _getProfileIcon(profile),
|
||||
onTap: () {
|
||||
|
@ -1,5 +1,6 @@
|
||||
|
||||
import 'package:inventree/api.dart';
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/app_settings.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/inventree/sentry.dart';
|
||||
@ -22,6 +23,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
import '../api_form.dart';
|
||||
|
||||
class CategoryDisplayWidget extends StatefulWidget {
|
||||
|
||||
CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
|
||||
@ -35,7 +38,6 @@ class CategoryDisplayWidget extends StatefulWidget {
|
||||
|
||||
class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
|
||||
final _editCategoryKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
String getAppBarTitle(BuildContext context) => L10().partCategory;
|
||||
@ -71,7 +73,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.edit),
|
||||
tooltip: L10().edit,
|
||||
onPressed: _editCategoryDialog,
|
||||
onPressed: () {
|
||||
_editCategoryDialog(context);
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -80,49 +84,26 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
|
||||
}
|
||||
|
||||
void _editCategory(Map<String, String> values) async {
|
||||
void _editCategoryDialog(BuildContext context) {
|
||||
|
||||
final bool result = await category!.update(values: values);
|
||||
|
||||
showSnackIcon(
|
||||
result ? "Category edited" : "Category editing failed",
|
||||
success: result
|
||||
);
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void _editCategoryDialog() {
|
||||
final _cat = category;
|
||||
|
||||
// Cannot edit top-level category
|
||||
if (category == null) {
|
||||
if (_cat == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var _name;
|
||||
var _description;
|
||||
|
||||
showFormDialog(
|
||||
launchApiForm(
|
||||
context,
|
||||
L10().editCategory,
|
||||
key: _editCategoryKey,
|
||||
callback: () {
|
||||
_editCategory({
|
||||
"name": _name,
|
||||
"description": _description
|
||||
});
|
||||
_cat.url,
|
||||
{
|
||||
"name": {},
|
||||
"description": {},
|
||||
"parent": {},
|
||||
},
|
||||
fields: <Widget>[
|
||||
StringField(
|
||||
label: L10().name,
|
||||
initial: category?.name,
|
||||
onSaved: (value) => _name = value
|
||||
),
|
||||
StringField(
|
||||
label: L10().description,
|
||||
initial: category?.description,
|
||||
onSaved: (value) => _description = value
|
||||
)
|
||||
]
|
||||
modelData: _cat.jsondata,
|
||||
onSuccess: refresh,
|
||||
);
|
||||
}
|
||||
|
||||
@ -186,7 +167,10 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||
ListTile(
|
||||
title: Text(L10().parentCategory),
|
||||
subtitle: Text("${category?.parentpathstring}"),
|
||||
leading: FaIcon(FontAwesomeIcons.levelUpAlt),
|
||||
leading: FaIcon(
|
||||
FontAwesomeIcons.levelUpAlt,
|
||||
color: COLOR_CLICK,
|
||||
),
|
||||
onTap: () {
|
||||
if (category == null || ((category?.parentId ?? 0) < 0)) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
|
||||
|
@ -1,7 +1,6 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import 'package:inventree/l10.dart';
|
||||
|
||||
import 'dart:async';
|
||||
@ -92,23 +91,28 @@ class ImagePickerField extends FormField<File> {
|
||||
|
||||
|
||||
class CheckBoxField extends FormField<bool> {
|
||||
CheckBoxField({String? label, String? hint, bool initial = false, Function(bool?)? onSaved}) :
|
||||
CheckBoxField({
|
||||
String? label, bool initial = false, Function(bool?)? onSaved,
|
||||
TextStyle? labelStyle,
|
||||
String? helperText,
|
||||
TextStyle? helperStyle,
|
||||
}) :
|
||||
super(
|
||||
onSaved: onSaved,
|
||||
initialValue: initial,
|
||||
builder: (FormFieldState<bool> state) {
|
||||
return CheckboxListTile(
|
||||
//dense: state.hasError,
|
||||
title: label == null ? null : Text(label),
|
||||
title: label != null ? Text(label, style: labelStyle) : null,
|
||||
value: state.value,
|
||||
onChanged: state.didChange,
|
||||
subtitle: hint == null ? null : Text(hint),
|
||||
subtitle: helperText != null ? Text(helperText, style: helperStyle) : null,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
class StringField extends TextFormField {
|
||||
|
||||
StringField({String label = "", String? hint, String? initial, Function(String?)? onSaved, Function? validator, bool allowEmpty = false, bool isEnabled = true}) :
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/user_profile.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -19,6 +20,7 @@ import 'package:inventree/widget/spinner.dart';
|
||||
import 'package:inventree/widget/drawer.dart';
|
||||
|
||||
class InvenTreeHomePage extends StatefulWidget {
|
||||
|
||||
InvenTreeHomePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -130,7 +132,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||
leading: FaIcon(FontAwesomeIcons.server),
|
||||
trailing: FaIcon(
|
||||
FontAwesomeIcons.user,
|
||||
color: Color.fromRGBO(250, 50, 50, 1),
|
||||
color: COLOR_DANGER,
|
||||
),
|
||||
onTap: () {
|
||||
_selectProfile();
|
||||
@ -146,7 +148,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||
leading: FaIcon(FontAwesomeIcons.server),
|
||||
trailing: Spinner(
|
||||
icon: FontAwesomeIcons.spinner,
|
||||
color: Color.fromRGBO(50, 50, 250, 1),
|
||||
color: COLOR_PROGRESS,
|
||||
),
|
||||
onTap: () {
|
||||
_selectProfile();
|
||||
@ -159,7 +161,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||
leading: FaIcon(FontAwesomeIcons.server),
|
||||
trailing: FaIcon(
|
||||
FontAwesomeIcons.checkCircle,
|
||||
color: Color.fromRGBO(50, 250, 50, 1)
|
||||
color: COLOR_SUCCESS
|
||||
),
|
||||
onTap: () {
|
||||
_selectProfile();
|
||||
@ -172,7 +174,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
|
||||
leading: FaIcon(FontAwesomeIcons.server),
|
||||
trailing: FaIcon(
|
||||
FontAwesomeIcons.timesCircle,
|
||||
color: Color.fromRGBO(250, 50, 50, 1),
|
||||
color: COLOR_DANGER,
|
||||
),
|
||||
onTap: () {
|
||||
_selectProfile();
|
||||
|
@ -1,4 +1,6 @@
|
||||
import 'package:inventree/api.dart';
|
||||
import 'package:inventree/api_form.dart';
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/app_settings.dart';
|
||||
import 'package:inventree/barcode.dart';
|
||||
import 'package:inventree/inventree/sentry.dart';
|
||||
@ -71,7 +73,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.edit),
|
||||
tooltip: L10().edit,
|
||||
onPressed: _editLocationDialog,
|
||||
onPressed: () { _editLocationDialog(context); },
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -79,23 +81,27 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
return actions;
|
||||
}
|
||||
|
||||
void _editLocation(Map<String, String> values) async {
|
||||
void _editLocationDialog(BuildContext context) {
|
||||
|
||||
bool result = false;
|
||||
final _loc = location;
|
||||
|
||||
if (location != null) {
|
||||
result = await location!.update(values: values);
|
||||
|
||||
showSnackIcon(
|
||||
result ? "Location edited" : "Location editing failed",
|
||||
success: result
|
||||
);
|
||||
if (_loc == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
refresh();
|
||||
}
|
||||
launchApiForm(
|
||||
context,
|
||||
L10().editLocation,
|
||||
_loc.url,
|
||||
{
|
||||
"name": {},
|
||||
"description": {},
|
||||
"parent": {},
|
||||
},
|
||||
modelData: _loc.jsondata,
|
||||
onSuccess: refresh
|
||||
);
|
||||
|
||||
void _editLocationDialog() {
|
||||
// Values which an be edited
|
||||
var _name;
|
||||
var _description;
|
||||
@ -103,28 +109,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
if (location == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFormDialog(L10().editLocation,
|
||||
key: _editLocationKey,
|
||||
callback: () {
|
||||
_editLocation({
|
||||
"name": _name,
|
||||
"description": _description
|
||||
});
|
||||
},
|
||||
fields: <Widget> [
|
||||
StringField(
|
||||
label: L10().name,
|
||||
initial: location?.name ?? '',
|
||||
onSaved: (value) => _name = value,
|
||||
),
|
||||
StringField(
|
||||
label: L10().description,
|
||||
initial: location?.description ?? '',
|
||||
onSaved: (value) => _description = value,
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
_LocationDisplayState(this.location);
|
||||
@ -193,7 +177,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
|
||||
ListTile(
|
||||
title: Text(L10().parentCategory),
|
||||
subtitle: Text("${location!.parentpathstring}"),
|
||||
leading: FaIcon(FontAwesomeIcons.levelUpAlt),
|
||||
leading: FaIcon(FontAwesomeIcons.levelUpAlt, color: COLOR_CLICK),
|
||||
onTap: () {
|
||||
|
||||
int parent = location?.parentId ?? -1;
|
||||
@ -319,7 +303,7 @@ List<Widget> detailTiles() {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().barcodeScanInItems),
|
||||
leading: FaIcon(FontAwesomeIcons.exchangeAlt),
|
||||
leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
|
||||
trailing: FaIcon(FontAwesomeIcons.qrcode),
|
||||
onTap: () {
|
||||
|
||||
|
@ -21,19 +21,14 @@ class PaginatedSearchWidget extends StatelessWidget {
|
||||
leading: GestureDetector(
|
||||
child: FaIcon(controller.text.isEmpty ? FontAwesomeIcons.search : FontAwesomeIcons.backspace),
|
||||
onTap: () {
|
||||
if (onChanged != null) {
|
||||
controller.clear();
|
||||
onChanged();
|
||||
}
|
||||
controller.clear();
|
||||
onChanged();
|
||||
},
|
||||
),
|
||||
title: TextFormField(
|
||||
controller: controller,
|
||||
onChanged: (value) {
|
||||
|
||||
if (onChanged != null) {
|
||||
onChanged();
|
||||
}
|
||||
onChanged();
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: L10().search,
|
||||
|
@ -1,14 +1,16 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:inventree/widget/part_notes.dart';
|
||||
import 'package:inventree/widget/progress.dart';
|
||||
import 'package:inventree/widget/snacks.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:inventree/l10.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:inventree/app_colors.dart';
|
||||
|
||||
import 'package:inventree/l10.dart';
|
||||
import 'package:inventree/api_form.dart';
|
||||
import 'package:inventree/widget/part_notes.dart';
|
||||
import 'package:inventree/widget/progress.dart';
|
||||
import 'package:inventree/widget/snacks.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/widget/full_screen_image.dart';
|
||||
import 'package:inventree/widget/category_display.dart';
|
||||
@ -59,7 +61,9 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.edit),
|
||||
tooltip: L10().edit,
|
||||
onPressed: _editPartDialog,
|
||||
onPressed: () {
|
||||
_editPartDialog(context);
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -169,58 +173,36 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
void _editPartDialog() {
|
||||
void _editPartDialog(BuildContext context) {
|
||||
|
||||
// Values which can be edited
|
||||
var _name;
|
||||
var _description;
|
||||
var _ipn;
|
||||
var _keywords;
|
||||
var _link;
|
||||
launchApiForm(
|
||||
context,
|
||||
L10().editPart,
|
||||
part.url,
|
||||
{
|
||||
"name": {},
|
||||
"description": {},
|
||||
"IPN": {},
|
||||
"revision": {},
|
||||
"keywords": {},
|
||||
"link": {},
|
||||
|
||||
showFormDialog(L10().editPart,
|
||||
key: _editPartKey,
|
||||
callback: () {
|
||||
_savePart({
|
||||
"name": _name,
|
||||
"description": _description,
|
||||
"IPN": _ipn,
|
||||
"keywords": _keywords,
|
||||
"link": _link
|
||||
});
|
||||
},
|
||||
fields: <Widget>[
|
||||
StringField(
|
||||
label: L10().name,
|
||||
initial: part.name,
|
||||
onSaved: (value) => _name = value,
|
||||
),
|
||||
StringField(
|
||||
label: L10().description,
|
||||
initial: part.description,
|
||||
onSaved: (value) => _description = value,
|
||||
),
|
||||
StringField(
|
||||
label: L10().internalPartNumber,
|
||||
initial: part.IPN,
|
||||
allowEmpty: true,
|
||||
onSaved: (value) => _ipn = value,
|
||||
),
|
||||
StringField(
|
||||
label: L10().keywords,
|
||||
initial: part.keywords,
|
||||
allowEmpty: true,
|
||||
onSaved: (value) => _keywords = value,
|
||||
),
|
||||
StringField(
|
||||
label: L10().link,
|
||||
initial: part.link,
|
||||
allowEmpty: true,
|
||||
onSaved: (value) => _link = value
|
||||
)
|
||||
]
|
||||
"category": {
|
||||
},
|
||||
|
||||
// Checkbox fields
|
||||
"active": {},
|
||||
"assembly": {},
|
||||
"component": {},
|
||||
"purchaseable": {},
|
||||
"salable": {},
|
||||
"trackable": {},
|
||||
"is_template": {},
|
||||
"virtual": {},
|
||||
},
|
||||
modelData: part.jsondata,
|
||||
onSuccess: refresh,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
Widget headerTile() {
|
||||
@ -230,7 +212,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
subtitle: Text("${part.description}"),
|
||||
trailing: IconButton(
|
||||
icon: FaIcon(part.starred ? FontAwesomeIcons.solidStar : FontAwesomeIcons.star,
|
||||
color: part.starred ? Color.fromRGBO(250, 250, 100, 1) : null,
|
||||
color: part.starred ? COLOR_STAR : null,
|
||||
),
|
||||
onPressed: _toggleStar,
|
||||
),
|
||||
@ -264,13 +246,36 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
return tiles;
|
||||
}
|
||||
|
||||
if (!part.isActive) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10().inactive,
|
||||
style: TextStyle(
|
||||
color: COLOR_DANGER
|
||||
)
|
||||
),
|
||||
subtitle: Text(
|
||||
L10().inactiveDetail,
|
||||
style: TextStyle(
|
||||
color: COLOR_DANGER
|
||||
)
|
||||
),
|
||||
leading: FaIcon(
|
||||
FontAwesomeIcons.exclamationCircle,
|
||||
color: COLOR_DANGER
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Category information
|
||||
if (part.categoryName.isNotEmpty) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().partCategory),
|
||||
subtitle: Text("${part.categoryName}"),
|
||||
leading: FaIcon(FontAwesomeIcons.sitemap),
|
||||
leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK),
|
||||
onTap: () {
|
||||
if (part.categoryId > 0) {
|
||||
InvenTreePartCategory().get(part.categoryId).then((var cat) {
|
||||
@ -289,7 +294,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
ListTile(
|
||||
title: Text(L10().partCategory),
|
||||
subtitle: Text(L10().partCategoryTopLevel),
|
||||
leading: FaIcon(FontAwesomeIcons.sitemap),
|
||||
leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
|
||||
},
|
||||
@ -301,7 +306,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().stock),
|
||||
leading: FaIcon(FontAwesomeIcons.boxes),
|
||||
leading: FaIcon(FontAwesomeIcons.boxes, color: COLOR_CLICK),
|
||||
trailing: Text("${part.inStockString}"),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
@ -387,8 +392,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text("${part.link}"),
|
||||
leading: FaIcon(FontAwesomeIcons.link),
|
||||
trailing: FaIcon(FontAwesomeIcons.externalLinkAlt),
|
||||
leading: FaIcon(FontAwesomeIcons.link, color: COLOR_CLICK),
|
||||
onTap: () {
|
||||
part.openLink();
|
||||
},
|
||||
@ -412,7 +416,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().notes),
|
||||
leading: FaIcon(FontAwesomeIcons.stickyNote),
|
||||
leading: FaIcon(FontAwesomeIcons.stickyNote, color: COLOR_CLICK),
|
||||
trailing: Text(""),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
@ -539,4 +543,4 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
||||
Widget getBody(BuildContext context) {
|
||||
return getSelectedWidget(tabIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:inventree/app_colors.dart';
|
||||
|
||||
class Spinner extends StatefulWidget {
|
||||
final IconData? icon;
|
||||
@ -9,7 +10,7 @@ class Spinner extends StatefulWidget {
|
||||
final Color color;
|
||||
|
||||
const Spinner({
|
||||
this.color = const Color.fromRGBO(150, 150, 150, 1),
|
||||
this.color = COLOR_GRAY_LIGHT,
|
||||
Key? key,
|
||||
@required this.icon,
|
||||
this.duration = const Duration(milliseconds: 1800),
|
||||
|
@ -1,4 +1,6 @@
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/barcode.dart';
|
||||
import 'package:inventree/inventree/model.dart';
|
||||
import 'package:inventree/inventree/stock.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/widget/dialogs.dart';
|
||||
@ -17,9 +19,11 @@ import 'package:inventree/l10.dart';
|
||||
|
||||
import 'package:inventree/api.dart';
|
||||
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
import 'package:dropdown_search/dropdown_search.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
import '../api_form.dart';
|
||||
|
||||
class StockDetailWidget extends StatefulWidget {
|
||||
|
||||
StockDetailWidget(this.item, {Key? key}) : super(key: key);
|
||||
@ -49,20 +53,29 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
|
||||
@override
|
||||
List<Widget> getAppBarActions(BuildContext context) {
|
||||
return <Widget>[
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.globe),
|
||||
onPressed: _openInvenTreePage,
|
||||
),
|
||||
// TODO: Hide the 'edit' button if the user does not have permission!!
|
||||
/*
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.edit),
|
||||
tooltip: L10().edit,
|
||||
onPressed: _editPartDialog,
|
||||
)
|
||||
*/
|
||||
];
|
||||
|
||||
List<Widget> actions = [];
|
||||
|
||||
if (InvenTreeAPI().checkPermission('stock', 'view')) {
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.globe),
|
||||
onPressed: _openInvenTreePage,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (InvenTreeAPI().checkPermission('stock', 'change')) {
|
||||
actions.add(
|
||||
IconButton(
|
||||
icon: FaIcon(FontAwesomeIcons.edit),
|
||||
tooltip: L10().edit,
|
||||
onPressed: () { _editStockItem(context); },
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
Future<void> _openInvenTreePage() async {
|
||||
@ -95,6 +108,24 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
await item.getTestResults();
|
||||
}
|
||||
|
||||
void _editStockItem(BuildContext context) async {
|
||||
|
||||
launchApiForm(
|
||||
context,
|
||||
L10().editItem,
|
||||
item.url,
|
||||
{
|
||||
"status": {},
|
||||
"batch": {},
|
||||
"packaging": {},
|
||||
"link": {},
|
||||
},
|
||||
modelData: item.jsondata,
|
||||
onSuccess: refresh
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
void _addStock() async {
|
||||
|
||||
double quantity = double.parse(_quantityController.text);
|
||||
@ -241,7 +272,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
}
|
||||
|
||||
|
||||
void _transferStock(InvenTreeStockLocation location) async {
|
||||
void _transferStock(int locationId) async {
|
||||
|
||||
double quantity = double.tryParse(_quantityController.text) ?? item.quantity;
|
||||
String notes = _notesController.text;
|
||||
@ -249,7 +280,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
_quantityController.clear();
|
||||
_notesController.clear();
|
||||
|
||||
var result = await item.transferStock(location.pk, quantity: quantity, notes: notes);
|
||||
var result = await item.transferStock(locationId, quantity: quantity, notes: notes);
|
||||
|
||||
refresh();
|
||||
|
||||
@ -258,22 +289,22 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
void _transferStockDialog() async {
|
||||
void _transferStockDialog(BuildContext context) async {
|
||||
|
||||
var locations = await InvenTreeStockLocation().list();
|
||||
final _selectedController = TextEditingController();
|
||||
|
||||
InvenTreeStockLocation? selectedLocation;
|
||||
int? location_pk;
|
||||
|
||||
_quantityController.text = "${item.quantityString}";
|
||||
|
||||
showFormDialog(L10().transferStock,
|
||||
key: _moveStockKey,
|
||||
callback: () {
|
||||
var _loc = selectedLocation;
|
||||
var _pk = location_pk;
|
||||
|
||||
if (_loc != null) {
|
||||
_transferStock(_loc);
|
||||
if (_pk != null) {
|
||||
_transferStock(_pk);
|
||||
}
|
||||
},
|
||||
fields: <Widget>[
|
||||
@ -282,47 +313,57 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
controller: _quantityController,
|
||||
max: item.quantity,
|
||||
),
|
||||
TypeAheadFormField(
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
controller: _selectedController,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10().searchLocation,
|
||||
border: OutlineInputBorder()
|
||||
)
|
||||
),
|
||||
suggestionsCallback: (pattern) async {
|
||||
List<InvenTreeStockLocation> suggestions = [];
|
||||
DropdownSearch<dynamic>(
|
||||
mode: Mode.BOTTOM_SHEET,
|
||||
showSelectedItem: false,
|
||||
autoFocusSearchBox: true,
|
||||
selectedItem: null,
|
||||
errorBuilder: (context, entry, exception) {
|
||||
print("entry: $entry");
|
||||
print(exception.toString());
|
||||
|
||||
for (var loc in locations) {
|
||||
if (loc.matchAgainstString(pattern)) {
|
||||
suggestions.add(loc as InvenTreeStockLocation);
|
||||
}
|
||||
return Text(
|
||||
exception.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
)
|
||||
);
|
||||
},
|
||||
onFind: (String filter) async {
|
||||
|
||||
Map<String, String> _filters = {
|
||||
"search": filter,
|
||||
"offset": "0",
|
||||
"limit": "25"
|
||||
};
|
||||
|
||||
final List<InvenTreeModel> results = await InvenTreeStockLocation().list(filters: _filters);
|
||||
|
||||
List<dynamic> items = [];
|
||||
|
||||
for (InvenTreeModel loc in results) {
|
||||
if (loc is InvenTreeStockLocation) {
|
||||
items.add(loc.jsondata);
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
},
|
||||
validator: (value) {
|
||||
if (selectedLocation == null) {
|
||||
return L10().selectLocation;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
onSuggestionSelected: (suggestion) {
|
||||
selectedLocation = suggestion as InvenTreeStockLocation;
|
||||
_selectedController.text = selectedLocation!.pathstring;
|
||||
},
|
||||
onSaved: (value) {
|
||||
},
|
||||
itemBuilder: (context, suggestion) {
|
||||
var location = suggestion as InvenTreeStockLocation;
|
||||
|
||||
return ListTile(
|
||||
title: Text("${location.pathstring}"),
|
||||
subtitle: Text("${location.description}"),
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
label: L10().stockLocation,
|
||||
hint: L10().searchLocation,
|
||||
onChanged: null,
|
||||
itemAsString: (dynamic location) {
|
||||
return location['pathstring'];
|
||||
},
|
||||
onSaved: (dynamic location) {
|
||||
if (location == null) {
|
||||
location_pk = null;
|
||||
} else {
|
||||
location_pk = location['pk'];
|
||||
}
|
||||
},
|
||||
isFilteredOnline: true,
|
||||
showSearchBox: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -394,7 +435,10 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
ListTile(
|
||||
title: Text(L10().stockLocation),
|
||||
subtitle: Text("${item.locationPathString}"),
|
||||
leading: FaIcon(FontAwesomeIcons.mapMarkerAlt),
|
||||
leading: FaIcon(
|
||||
FontAwesomeIcons.mapMarkerAlt,
|
||||
color: COLOR_CLICK,
|
||||
),
|
||||
onTap: () {
|
||||
if (item.locationId > 0) {
|
||||
InvenTreeStockLocation().get(item.locationId).then((var loc) {
|
||||
@ -463,9 +507,10 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text("${item.link}"),
|
||||
leading: FaIcon(FontAwesomeIcons.link),
|
||||
trailing: Text(""),
|
||||
onTap: null,
|
||||
leading: FaIcon(FontAwesomeIcons.link, color: COLOR_CLICK),
|
||||
onTap: () {
|
||||
item.openLink();
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -474,7 +519,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().testResults),
|
||||
leading: FaIcon(FontAwesomeIcons.tasks),
|
||||
leading: FaIcon(FontAwesomeIcons.tasks, color: COLOR_CLICK),
|
||||
trailing: Text("${item.testResultCount}"),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
@ -489,6 +534,18 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
if (item.hasPurchasePrice) {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().purchasePrice),
|
||||
leading: FaIcon(FontAwesomeIcons.dollarSign),
|
||||
trailing: Text(item.purchasePrice),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// TODO - Is this stock item linked to a PurchaseOrder?
|
||||
|
||||
// TODO - Re-enable stock item history display
|
||||
if (false && item.trackingItemCount > 0) {
|
||||
tiles.add(
|
||||
@ -510,8 +567,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().notes),
|
||||
leading: FaIcon(FontAwesomeIcons.stickyNote),
|
||||
trailing: Text(""),
|
||||
leading: FaIcon(FontAwesomeIcons.stickyNote, color: COLOR_CLICK),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
@ -527,7 +583,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
return tiles;
|
||||
}
|
||||
|
||||
List<Widget> actionTiles() {
|
||||
List<Widget> actionTiles(BuildContext context) {
|
||||
List<Widget> tiles = [];
|
||||
|
||||
tiles.add(headerTile());
|
||||
@ -554,7 +610,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().countStock),
|
||||
leading: FaIcon(FontAwesomeIcons.checkCircle),
|
||||
leading: FaIcon(FontAwesomeIcons.checkCircle, color: COLOR_CLICK),
|
||||
onTap: _countStockDialog,
|
||||
trailing: Text(item.quantityString),
|
||||
)
|
||||
@ -563,7 +619,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().removeStock),
|
||||
leading: FaIcon(FontAwesomeIcons.minusCircle),
|
||||
leading: FaIcon(FontAwesomeIcons.minusCircle, color: COLOR_CLICK),
|
||||
onTap: _removeStockDialog,
|
||||
)
|
||||
);
|
||||
@ -571,7 +627,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().addStock),
|
||||
leading: FaIcon(FontAwesomeIcons.plusCircle),
|
||||
leading: FaIcon(FontAwesomeIcons.plusCircle, color: COLOR_CLICK),
|
||||
onTap: _addStockDialog,
|
||||
)
|
||||
);
|
||||
@ -580,8 +636,8 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().transferStock),
|
||||
leading: FaIcon(FontAwesomeIcons.exchangeAlt),
|
||||
onTap: _transferStockDialog,
|
||||
leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
|
||||
onTap: () { _transferStockDialog(context); },
|
||||
)
|
||||
);
|
||||
|
||||
@ -589,7 +645,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().scanIntoLocation),
|
||||
leading: FaIcon(FontAwesomeIcons.exchangeAlt),
|
||||
leading: FaIcon(FontAwesomeIcons.exchangeAlt, color: COLOR_CLICK),
|
||||
trailing: FaIcon(FontAwesomeIcons.qrcode),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
@ -607,7 +663,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().barcodeAssign),
|
||||
leading: FaIcon(FontAwesomeIcons.barcode),
|
||||
leading: FaIcon(FontAwesomeIcons.barcode, color: COLOR_CLICK),
|
||||
trailing: FaIcon(FontAwesomeIcons.qrcode),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
@ -623,7 +679,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
tiles.add(
|
||||
ListTile(
|
||||
title: Text(L10().barcodeUnassign),
|
||||
leading: FaIcon(FontAwesomeIcons.barcode),
|
||||
leading: FaIcon(FontAwesomeIcons.barcode, color: COLOR_CLICK),
|
||||
onTap: () {
|
||||
_unassignBarcode(context);
|
||||
}
|
||||
@ -665,7 +721,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
|
||||
return ListView(
|
||||
children: ListTile.divideTiles(
|
||||
context: context,
|
||||
tiles: actionTiles()
|
||||
tiles: actionTiles(context)
|
||||
).toList()
|
||||
);
|
||||
default:
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:inventree/app_colors.dart';
|
||||
import 'package:inventree/inventree/part.dart';
|
||||
import 'package:inventree/inventree/stock.dart';
|
||||
import 'package:inventree/inventree/model.dart';
|
||||
@ -84,7 +85,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
||||
),
|
||||
CheckBoxField(
|
||||
label: L10().result,
|
||||
hint: L10().testPassedOrFailed,
|
||||
helperText: L10().testPassedOrFailed,
|
||||
initial: true,
|
||||
onSaved: (value) => _result = value ?? false,
|
||||
),
|
||||
@ -207,7 +208,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
||||
String _value = "";
|
||||
String _notes = "";
|
||||
|
||||
FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: Color.fromRGBO(0, 0, 250, 1));
|
||||
FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: COLOR_BLUE);
|
||||
bool _valueRequired = false;
|
||||
bool _attachmentRequired = false;
|
||||
|
||||
@ -229,11 +230,11 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
|
||||
|
||||
if (_result == true) {
|
||||
_icon = FaIcon(FontAwesomeIcons.checkCircle,
|
||||
color: Color.fromRGBO(0, 250, 0, 0.8)
|
||||
color: COLOR_SUCCESS,
|
||||
);
|
||||
} else if (_result == false) {
|
||||
_icon = FaIcon(FontAwesomeIcons.timesCircle,
|
||||
color: Color.fromRGBO(250, 0, 0, 0.8)
|
||||
color: COLOR_DANGER,
|
||||
);
|
||||
}
|
||||
|
||||
|
35
pubspec.lock
35
pubspec.lock
@ -127,6 +127,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
dropdown_search:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dropdown_search
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.3"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -167,27 +174,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
flutter_keyboard_visibility:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.0.2"
|
||||
flutter_keyboard_visibility_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
flutter_keyboard_visibility_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -226,13 +212,6 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_typeahead:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_typeahead
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -7,7 +7,7 @@ description: InvenTree stock management
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 0.2.10+18
|
||||
version: 0.3.1+19
|
||||
|
||||
environment:
|
||||
sdk: ">=2.12.0 <3.0.0"
|
||||
@ -30,7 +30,6 @@ dependencies:
|
||||
font_awesome_flutter: ^9.1.0 # FontAwesome icon set
|
||||
flutter_speed_dial: ^3.0.5 # FAB menu elements
|
||||
sentry_flutter: 5.0.0 # Error reporting
|
||||
flutter_typeahead: ^3.1.0 # Auto-complete input field
|
||||
image_picker: ^0.8.0 # Select or take photos
|
||||
url_launcher: 6.0.0 # Open link in system browser
|
||||
flutter_markdown: ^0.6.2 # Rendering markdown
|
||||
@ -40,6 +39,7 @@ dependencies:
|
||||
one_context: ^1.1.0 # Dialogs without requiring context
|
||||
infinite_scroll_pagination: ^3.1.0 # Let the server do all the work!
|
||||
audioplayers: ^0.19.0 # Play audio files
|
||||
dropdown_search: 0.6.3 # Dropdown autocomplete form fields
|
||||
path:
|
||||
|
||||
dev_dependencies:
|
||||
|
Loading…
x
Reference in New Issue
Block a user