From c8c056f96deb98fd8af79a94480669769ac4a60b Mon Sep 17 00:00:00 2001
From: Oliver Walters <oliver.henry.walters@gmail.com>
Date: Wed, 10 Feb 2021 23:51:38 +1100
Subject: [PATCH] Add snackbar with icon

- stock adjust
- part edit
- location edit
---
 lib/inventree/stock.dart          |  9 +++++
 lib/widget/dialogs.dart           | 34 +++++++++++++++++-
 lib/widget/location_display.dart  | 58 ++++++++++++++++++++++++++++---
 lib/widget/part_detail.dart       | 44 +++++++++--------------
 lib/widget/refreshable_state.dart |  3 ++
 lib/widget/snacks.dart            | 33 ++++++++++++++++++
 lib/widget/stock_detail.dart      |  7 ++--
 7 files changed, 154 insertions(+), 34 deletions(-)
 create mode 100644 lib/widget/snacks.dart

diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart
index 516672f4..022d0642 100644
--- a/lib/inventree/stock.dart
+++ b/lib/inventree/stock.dart
@@ -323,6 +323,15 @@ class InvenTreeStockItem extends InvenTreeModel {
 
   double get quantity => double.tryParse(jsondata['quantity'].toString() ?? '0');
 
+  String get quantityString {
+
+    if (quantity.toInt() == quantity) {
+      return quantity.toInt().toString();
+    } else {
+      return quantity.toString();
+    }
+  }
+
   int get locationId => jsondata['location'] as int ?? -1;
 
   bool isSerialized() => serialNumber != null && quantity.toInt() == 1;
diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart
index 803476ab..3baa1c44 100644
--- a/lib/widget/dialogs.dart
+++ b/lib/widget/dialogs.dart
@@ -161,7 +161,39 @@ void hideProgressDialog(BuildContext context) {
   Navigator.pop(context);
 }
 
-void showFormDialog(BuildContext context, String title, {GlobalKey<FormState> key, List<Widget> fields, List<Widget> actions}) {
+void showFormDialog(BuildContext context, String title, {GlobalKey<FormState> key, List<Widget> fields, List<Widget> actions, Function callback}) {
+
+  // Undefined actions = OK + Cancel
+  if (actions == null) {
+    actions = <Widget>[
+      FlatButton(
+        child: Text(I18N.of(context).cancel),
+        onPressed: () {
+          // Close the form
+          Navigator.pop(context);
+        }
+      ),
+      FlatButton(
+        child: Text(I18N.of(context).save),
+        onPressed: () {
+          if (key.currentState.validate()) {
+            key.currentState.save();
+
+            // Close the dialog
+            Navigator.pop(context);
+
+            // Callback
+            if (callback != null) {
+              callback();
+            }
+            
+
+          }
+        }
+      )
+    ];
+  }
+
   showDialog(
     context: context,
     builder: (BuildContext context) {
diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart
index 5498793a..8d6b7b7c 100644
--- a/lib/widget/location_display.dart
+++ b/lib/widget/location_display.dart
@@ -1,13 +1,18 @@
 import 'package:InvenTree/api.dart';
 import 'package:InvenTree/inventree/stock.dart';
 import 'package:InvenTree/preferences.dart';
+
+import 'package:InvenTree/widget/refreshable_state.dart';
+import 'package:InvenTree/widget/fields.dart';
+import 'package:InvenTree/widget/dialogs.dart';
+import 'package:InvenTree/widget/snacks.dart';
 import 'package:InvenTree/widget/stock_detail.dart';
+
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/foundation.dart';
 import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:InvenTree/widget/refreshable_state.dart';
 
 class LocationDisplayWidget extends StatefulWidget {
 
@@ -25,6 +30,8 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
 
   final InvenTreeStockLocation location;
 
+  final _editLocationKey = GlobalKey<FormState>();
+
   @override
   String getAppBarTitle(BuildContext context) { return "Stock Location"; }
 
@@ -33,13 +40,53 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
     return <Widget>[
       IconButton(
         icon: FaIcon(FontAwesomeIcons.edit),
-        tooltip: "Edit",
-        // TODO - Edit stock location
-        onPressed: null,
+        tooltip: I18N.of(context).edit,
+        onPressed: _editLocationDialog,
       )
     ];
   }
 
+  void _editLocation(Map<String, String> values) async {
+
+    final bool result = await location.update(context, values: values);
+
+    showSnackIcon(
+      refreshableKey,
+      result ? "Location edited" : "Location editing failed",
+      success: result
+    );
+
+    refresh();
+  }
+
+  void _editLocationDialog() {
+    // Values which an be edited
+    var _name;
+    var _description;
+
+    showFormDialog(context, I18N.of(context).editLocation,
+      key: _editLocationKey,
+      callback: () {
+        _editLocation({
+          "name": _name,
+          "description": _description
+        });
+      },
+      fields: <Widget> [
+        StringField(
+          label: I18N.of(context).name,
+          initial: location.name,
+          onSaved: (value) => _name = value,
+        ),
+        StringField(
+          label: I18N.of(context).description,
+          initial: location.description,
+          onSaved: (value) => _description = value,
+        )
+      ]
+    );
+  }
+
   _LocationDisplayState(this.location) {}
 
   List<InvenTreeStockLocation> _sublocations = List<InvenTreeStockLocation>();
@@ -67,6 +114,9 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
 
     int pk = location?.pk ?? -1;
 
+    // Reload location information
+    await location.reload(context);
+
     // Request a list of sub-locations under this one
     await InvenTreeStockLocation().list(context, filters: {"parent": "$pk"}).then((var locs) {
       _sublocations.clear();
diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart
index 64cedbe9..6c34fc53 100644
--- a/lib/widget/part_detail.dart
+++ b/lib/widget/part_detail.dart
@@ -1,4 +1,6 @@
+import 'dart:io';
 
+import 'package:InvenTree/widget/snacks.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
@@ -74,9 +76,13 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
 
   void _savePart(Map<String, String> values) async {
 
-    Navigator.of(context).pop();
+    final bool result = await part.update(context, values: values);
 
-    var response = await part.update(context, values: values);
+    showSnackIcon(
+      refreshableKey,
+      result ? "Part edited" : "Part editing failed",
+      success: result
+    );
 
     refresh();
   }
@@ -96,34 +102,18 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
     var _name;
     var _description;
     var _ipn;
-    var _revision;
     var _keywords;
 
     showFormDialog(context, I18N.of(context).editPart,
       key: _editPartKey,
-      actions: <Widget>[
-        FlatButton(
-          child: Text(I18N.of(context).cancel),
-          onPressed: () {
-            Navigator.pop(context);
-          },
-        ),
-        FlatButton(
-          child: Text(I18N.of(context).save),
-          onPressed: () {
-            if (_editPartKey.currentState.validate()) {
-              _editPartKey.currentState.save();
-
-              _savePart({
-                "name": _name,
-                "description": _description,
-                "IPN": _ipn,
-                "keywords": _keywords,
-              });
-            }
-          },
-        ),
-      ],
+      callback: () {
+        _savePart({
+          "name": _name,
+          "description": _description,
+          "IPN": _ipn,
+          "keywords": _keywords
+        });
+      },
       fields: <Widget>[
         StringField(
           label: I18N.of(context).name,
@@ -163,7 +153,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
             onTap: () {
               Navigator.push(
                 context,
-                MaterialPageRoute(builder: (context) => FullScreenWidget(part.name, part.image))
+                MaterialPageRoute(builder: (context) => FullScreenWidget(part.fullname, part.image))
               );
             }),
         )
diff --git a/lib/widget/refreshable_state.dart b/lib/widget/refreshable_state.dart
index 7bf1bbde..53ed0a8e 100644
--- a/lib/widget/refreshable_state.dart
+++ b/lib/widget/refreshable_state.dart
@@ -8,6 +8,8 @@ import 'package:InvenTree/widget/drawer.dart';
 
 abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
 
+  final refreshableKey = GlobalKey<ScaffoldState>();
+
   // Storage for context once "Build" is called
   BuildContext context;
 
@@ -80,6 +82,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
     this.context = context;
 
     return Scaffold(
+      key: refreshableKey,
       appBar: getAppBar(context),
       drawer: getDrawer(context),
       floatingActionButton: getFab(context),
diff --git a/lib/widget/snacks.dart b/lib/widget/snacks.dart
new file mode 100644
index 00000000..9b2f3cad
--- /dev/null
+++ b/lib/widget/snacks.dart
@@ -0,0 +1,33 @@
+
+/*
+ * Display a snackbar with:
+ *
+ * a) Text on the left
+ * b) Icon on the right
+ *
+ * | Text          <icon> |
+ */
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:font_awesome_flutter/font_awesome_flutter.dart';
+
+void showSnackIcon(GlobalKey<ScaffoldState> key, String text, {IconData icon, bool success}) {
+
+  // If icon not specified, use the success status
+  if (icon == null) {
+    icon = (success == true) ? FontAwesomeIcons.checkCircle : FontAwesomeIcons.timesCircle;
+  }
+
+  key.currentState.showSnackBar(
+    SnackBar(
+      content: Row(
+        children: [
+          Text(text),
+          Spacer(),
+          FaIcon(icon)
+        ]
+      ),
+    )
+  );
+}
\ No newline at end of file
diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart
index dc4beddd..2acca4c7 100644
--- a/lib/widget/stock_detail.dart
+++ b/lib/widget/stock_detail.dart
@@ -1,4 +1,5 @@
 
+import 'dart:io';
 
 import 'package:InvenTree/barcode.dart';
 import 'package:InvenTree/inventree/stock.dart';
@@ -8,6 +9,7 @@ import 'package:InvenTree/widget/fields.dart';
 import 'package:InvenTree/widget/location_display.dart';
 import 'package:InvenTree/widget/part_detail.dart';
 import 'package:InvenTree/widget/refreshable_state.dart';
+import 'package:InvenTree/widget/snacks.dart';
 import 'package:InvenTree/widget/stock_item_test_results.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
@@ -22,6 +24,7 @@ import 'package:flutter_typeahead/flutter_typeahead.dart';
 import 'package:flutter/services.dart';
 import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 import 'package:flutter_speed_dial/flutter_speed_dial.dart';
+import 'package:http/http.dart';
 
 class StockDetailWidget extends StatefulWidget {
 
@@ -71,10 +74,10 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
     var response = await item.addStock(quantity, notes: _notesController.text);
     _notesController.clear();
 
+    _stockUpdateMessage(response);
+
     // TODO - Handle error cases
     refresh();
-
-    // TODO - Display a snackbar here indicating the action was successful (or otherwise)
   }
 
   void _addStockDialog() async {