Added Combined new/edit and view product screen
Similarily to categories and rooms, the edit product screen is reused as a new-room screen, which is especially easy, because the user is unable to select the product id themselves. NOTE: the dynamic value-unit input is still missing some "subunits" The view product screen has links to the edit product page, the view parent page (if available) and a not yet functional view children screen. NOTE: The parent product display should be restricted in width, and the screen is missing value/unit information.
This commit is contained in:
parent
47387bb395
commit
5cd21c8adf
11 changed files with 1101 additions and 185 deletions
|
@ -357,7 +357,7 @@ class RoomInfo {
|
|||
}
|
||||
|
||||
class RoomCategory {
|
||||
final int id;
|
||||
final int? id;
|
||||
final String name;
|
||||
final ColorSwatch<int> color;
|
||||
|
||||
|
@ -372,10 +372,9 @@ class RoomCategory {
|
|||
}
|
||||
factory RoomCategory.other(BuildContext context) {
|
||||
return RoomCategory(
|
||||
id: -1,
|
||||
id: null,
|
||||
name: AppLocalizations.of(context)!.categoryNameOther,
|
||||
color: Colors.grey
|
||||
);
|
||||
color: Colors.grey);
|
||||
}
|
||||
|
||||
static List<ColorSwatch<int>> listColors() {
|
||||
|
|
16
lib/components/category_chip.dart
Normal file
16
lib/components/category_chip.dart
Normal file
|
@ -0,0 +1,16 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:outbag_app/backend/room.dart';
|
||||
|
||||
class CategoryChip extends StatelessWidget {
|
||||
final RoomCategory? category;
|
||||
|
||||
const CategoryChip({super.key, this.category});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ActionChip(
|
||||
avatar: Icon(Icons.square_rounded, color: category?.color ?? RoomCategory.other(context).color),
|
||||
label: Text(category?.name ?? RoomCategory.other(context).name),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,19 +1,19 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:outbag_app/backend/room.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class CategoryPicker extends StatelessWidget {
|
||||
List<RoomCategory> categories = [];
|
||||
int? selected;
|
||||
bool enabled = true;
|
||||
Function(int?)? onSelect;
|
||||
final List<RoomCategory> categories;
|
||||
final int? selected;
|
||||
final bool enabled;
|
||||
final Function(int?)? onSelect;
|
||||
|
||||
// hint and label may differ depending on the screen
|
||||
String? hint;
|
||||
String? label;
|
||||
final String? hint;
|
||||
final String? label;
|
||||
|
||||
CategoryPicker(
|
||||
{required this.categories,
|
||||
const CategoryPicker(
|
||||
{super.key,
|
||||
required this.categories,
|
||||
this.selected,
|
||||
this.onSelect,
|
||||
this.hint,
|
||||
|
@ -24,34 +24,35 @@ class CategoryPicker extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: DropdownMenu<int?>(
|
||||
initialSelection: selected,
|
||||
enabled: enabled,
|
||||
hintText: hint,
|
||||
label: (label!=null)?Text(label!):null,
|
||||
onSelected: ((id) {
|
||||
if (onSelect != null) {
|
||||
onSelect!(id);
|
||||
}
|
||||
}),
|
||||
dropdownMenuEntries: [
|
||||
// entry for every categry
|
||||
...categories.map((category) => DropdownMenuEntry(
|
||||
child: DropdownButtonFormField<int?>(
|
||||
hint: hint==null?null:Text(hint!),
|
||||
decoration: InputDecoration(
|
||||
label: label==null?null:Text(label!),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.category)
|
||||
),
|
||||
value: selected,
|
||||
items: [
|
||||
...categories,
|
||||
RoomCategory.other(context)
|
||||
].map((category)=>DropdownMenuItem<int?>(
|
||||
value: category.id,
|
||||
label: category.name,
|
||||
trailingIcon: Icon(
|
||||
Icons.square_rounded,
|
||||
color: category.color,
|
||||
))),
|
||||
// entry for default ("other") category
|
||||
DropdownMenuEntry(
|
||||
value: null,
|
||||
label: AppLocalizations.of(context)!.categoryNameOther,
|
||||
trailingIcon: Icon(
|
||||
Icons.square_rounded,
|
||||
color: RoomCategory.other(context).color,
|
||||
))
|
||||
],
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(category.name),
|
||||
Icon(Icons.square_rounded,
|
||||
color:category.color,
|
||||
size: 32)
|
||||
]
|
||||
),
|
||||
)).toList(),
|
||||
onChanged: enabled?(cid) {
|
||||
if (onSelect != null) {
|
||||
onSelect!(cid);
|
||||
}
|
||||
}:null,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,20 +3,23 @@ import 'package:outbag_app/backend/room.dart';
|
|||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class ProductPicker extends StatelessWidget {
|
||||
List<RoomProduct> products = [];
|
||||
int? selected;
|
||||
bool enabled = true;
|
||||
Function(int?)? onSelect;
|
||||
final List<RoomProduct> products;
|
||||
final int? selected;
|
||||
final bool enabled;
|
||||
final Function(int?)? onSelect;
|
||||
|
||||
// hint and label may differ depending on the screen
|
||||
String? hint;
|
||||
String? label;
|
||||
final String? hint;
|
||||
final String? label;
|
||||
final String? help;
|
||||
|
||||
ProductPicker(
|
||||
{required this.products,
|
||||
const ProductPicker(
|
||||
{super.key,
|
||||
required this.products,
|
||||
this.selected,
|
||||
this.onSelect,
|
||||
this.hint,
|
||||
this.help,
|
||||
this.label,
|
||||
this.enabled = true});
|
||||
|
||||
|
@ -24,28 +27,31 @@ class ProductPicker extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: DropdownMenu<int?>(
|
||||
initialSelection: selected,
|
||||
label: (label!=null)?Text(label!):null,
|
||||
hintText: hint,
|
||||
enabled: enabled,
|
||||
onSelected: ((id) {
|
||||
if (onSelect != null) {
|
||||
onSelect!(id);
|
||||
}
|
||||
}),
|
||||
dropdownMenuEntries: [
|
||||
// entry for no product
|
||||
DropdownMenuEntry(
|
||||
child: DropdownButtonFormField<int?>(
|
||||
hint: hint == null ? null : Text(hint!),
|
||||
decoration: InputDecoration(
|
||||
label: label == null ? null : Text(label!),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.inventory_2),
|
||||
helperText: help),
|
||||
value: selected,
|
||||
items: [
|
||||
// "no product" entry
|
||||
DropdownMenuItem<int?>(
|
||||
value: null,
|
||||
label: AppLocalizations.of(context)!.productNameNone,
|
||||
child: Text(AppLocalizations.of(context)!.productNameNone),
|
||||
),
|
||||
// entry for every product
|
||||
...products.map((product) => DropdownMenuEntry(
|
||||
value: product.id,
|
||||
label: product.name,
|
||||
)),
|
||||
// other products
|
||||
...products.map((product) => DropdownMenuItem<int?>(
|
||||
value: product.id, child: Text(product.name)))
|
||||
],
|
||||
onChanged: enabled
|
||||
? (pid) {
|
||||
if (onSelect != null) {
|
||||
onSelect!(pid);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
331
lib/components/value_unit_input.dart
Normal file
331
lib/components/value_unit_input.dart
Normal file
|
@ -0,0 +1,331 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
enum UnitType { text, amount, mass, volume, length, area }
|
||||
|
||||
class Unit {
|
||||
UnitType type = UnitType.text;
|
||||
|
||||
Unit(this.type);
|
||||
|
||||
factory Unit.fromId(int id) {
|
||||
switch (id) {
|
||||
case 1:
|
||||
return Unit(UnitType.amount);
|
||||
case 2:
|
||||
return Unit(UnitType.mass);
|
||||
case 3:
|
||||
return Unit(UnitType.volume);
|
||||
case 4:
|
||||
return Unit(UnitType.length);
|
||||
case 5:
|
||||
return Unit(UnitType.area);
|
||||
case 0:
|
||||
default:
|
||||
return Unit(UnitType.text);
|
||||
}
|
||||
}
|
||||
|
||||
String name(BuildContext context) {
|
||||
final trans = AppLocalizations.of(context);
|
||||
|
||||
if (type == UnitType.text) {
|
||||
return trans!.unitText;
|
||||
} else if (type == UnitType.amount) {
|
||||
return trans!.unitAmount;
|
||||
} else if (type == UnitType.mass) {
|
||||
return trans!.unitMass;
|
||||
} else if (type == UnitType.volume) {
|
||||
return trans!.unitVolume;
|
||||
} else if (type == UnitType.length) {
|
||||
return trans!.unitLength;
|
||||
} else if (type == UnitType.area) {
|
||||
return trans!.unitArea;
|
||||
}
|
||||
|
||||
return trans!.unitText;
|
||||
}
|
||||
|
||||
int get id {
|
||||
if (type == UnitType.text) {
|
||||
return 0;
|
||||
} else if (type == UnitType.amount) {
|
||||
return 1;
|
||||
} else if (type == UnitType.mass) {
|
||||
return 2;
|
||||
} else if (type == UnitType.volume) {
|
||||
return 3;
|
||||
} else if (type == UnitType.length) {
|
||||
return 4;
|
||||
} else if (type == UnitType.area) {
|
||||
return 5;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static List<Unit> list() {
|
||||
return [
|
||||
Unit.fromId(0),
|
||||
Unit.fromId(1),
|
||||
Unit.fromId(2),
|
||||
Unit.fromId(3),
|
||||
Unit.fromId(4),
|
||||
Unit.fromId(5),
|
||||
];
|
||||
}
|
||||
|
||||
List<SubUnit> get subUnits {
|
||||
if (type == UnitType.text) {
|
||||
// NOTE: plain text does not need subunits
|
||||
return [];
|
||||
} else if (type == UnitType.amount) {
|
||||
// NOTE: amount does not need subunits
|
||||
return [];
|
||||
} else if (type == UnitType.mass) {
|
||||
// TODO: add subunits (with conversion)
|
||||
return [SubUnit.def('kg'), SubUnit.nth('g', 1 / 1000)];
|
||||
} else if (type == UnitType.volume) {
|
||||
// TODO: add subunits (with conversion)
|
||||
return [SubUnit.def('l')];
|
||||
} else if (type == UnitType.length) {
|
||||
// TODO: add subunits (with conversion)
|
||||
return [SubUnit.def('m')];
|
||||
} else if (type == UnitType.area) {
|
||||
// TODO: add subunits (with conversion)
|
||||
return [SubUnit.def('m²')];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
int? get defaultSubUnit {
|
||||
if (type == UnitType.text) {
|
||||
// NOTE: plain text does not need subunits
|
||||
return null;
|
||||
} else if (type == UnitType.amount) {
|
||||
// NOTE: amount does not need subunits
|
||||
return null;
|
||||
} else if (type == UnitType.mass) {
|
||||
return 0;
|
||||
} else if (type == UnitType.volume) {
|
||||
return 0;
|
||||
} else if (type == UnitType.length) {
|
||||
return 0;
|
||||
} else if (type == UnitType.area) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
TextInputType get recommendedType {
|
||||
if (type == UnitType.text) {
|
||||
return TextInputType.text;
|
||||
} else {
|
||||
return TextInputType.number;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SubUnit {
|
||||
final String name;
|
||||
// function to convert from default subunit to this subunit
|
||||
final String Function(String value) convertTo;
|
||||
// function to convert from this subunit to default subunit
|
||||
final String Function(String value) convertFrom;
|
||||
|
||||
const SubUnit({
|
||||
required this.name,
|
||||
required this.convertTo,
|
||||
required this.convertFrom,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (runtimeType != other.runtimeType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return name.hashCode == other.hashCode;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return name.hashCode;
|
||||
}
|
||||
|
||||
factory SubUnit.def(String name) {
|
||||
return SubUnit(name: name, convertTo: (v) => v, convertFrom: (v) => v);
|
||||
}
|
||||
factory SubUnit.nth(String name, double fact) {
|
||||
return SubUnit(
|
||||
name: name,
|
||||
convertTo: (v) {
|
||||
try {
|
||||
final double number = double.parse(v);
|
||||
return (number * fact).toString();
|
||||
} catch (_) {
|
||||
return v;
|
||||
}
|
||||
},
|
||||
convertFrom: (v) {
|
||||
try {
|
||||
final double number = double.parse(v);
|
||||
return (number / fact).toString();
|
||||
} catch (_) {
|
||||
return v;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class DynamicValueUnitInput extends StatefulWidget {
|
||||
final int initialUnit;
|
||||
final String initialValue;
|
||||
final Function(String)? onValueChange;
|
||||
final Function(int)? onUnitChange;
|
||||
|
||||
const DynamicValueUnitInput(
|
||||
{super.key,
|
||||
required this.initialUnit,
|
||||
required this.initialValue,
|
||||
this.onValueChange,
|
||||
this.onUnitChange});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DynamicValueUnitInputState();
|
||||
}
|
||||
|
||||
class _DynamicValueUnitInputState extends State<DynamicValueUnitInput> {
|
||||
Unit unit = Unit.fromId(0);
|
||||
bool enabled = true;
|
||||
TextEditingController controller = TextEditingController();
|
||||
SubUnit? sub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
controller = TextEditingController(text: widget.initialValue);
|
||||
unit = Unit.fromId(widget.initialUnit);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Wrap(children: [
|
||||
// unit type picker
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: DropdownMenu<int>(
|
||||
label: Text(AppLocalizations.of(context)!.selectUnitTypeLabel),
|
||||
enabled: enabled,
|
||||
initialSelection: unit.id,
|
||||
onSelected: (unit) {
|
||||
if (unit == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final u = Unit.fromId(unit);
|
||||
SubUnit? s;
|
||||
if (u.defaultSubUnit != null) {
|
||||
s = u.subUnits[u.defaultSubUnit!];
|
||||
}
|
||||
// NOTE: we could run this here as well,
|
||||
// but at least to me it seems more natural without it
|
||||
// convertSubunit(s);
|
||||
|
||||
if (widget.onUnitChange != null) {
|
||||
widget.onUnitChange!(unit);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
this.unit = u;
|
||||
sub = s;
|
||||
});
|
||||
},
|
||||
dropdownMenuEntries: Unit.list()
|
||||
.map((unit) => DropdownMenuEntry(
|
||||
value: unit.id, label: unit.name(context)))
|
||||
.toList(),
|
||||
)),
|
||||
|
||||
// (optional) subunit selector
|
||||
...(unit.defaultSubUnit != null)
|
||||
? [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: DropdownButtonFormField<SubUnit>(
|
||||
hint: Text(AppLocalizations.of(context)!.selectUnitLabel),
|
||||
decoration: InputDecoration(
|
||||
label:
|
||||
Text(AppLocalizations.of(context)!.selectUnitLabel),
|
||||
border: const OutlineInputBorder()),
|
||||
value: sub,
|
||||
items: unit.subUnits
|
||||
.map((sub) =>
|
||||
DropdownMenuItem(value: sub, child: Text(sub.name)))
|
||||
.toList(),
|
||||
onChanged: (sub) {
|
||||
// there is no way to select nothing
|
||||
if (sub == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub == this.sub) {
|
||||
// no new subunit selected
|
||||
return;
|
||||
}
|
||||
|
||||
convertSubunit(sub);
|
||||
setState(() {
|
||||
this.sub = sub;
|
||||
});
|
||||
},
|
||||
))
|
||||
]
|
||||
: [],
|
||||
|
||||
// value input field
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
keyboardType: unit.recommendedType,
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context)!.inputUnitValueLabel,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (value) {
|
||||
final String intermediate =
|
||||
(sub != null) ? sub!.convertFrom(value) : value;
|
||||
|
||||
if (widget.onValueChange != null) {
|
||||
widget.onValueChange!(intermediate);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
void convertSubunit(SubUnit? newest) {
|
||||
final String old2intermediate =
|
||||
(sub != null) ? sub!.convertFrom(value) : value;
|
||||
final String intermediate2new = (newest != null)
|
||||
? newest.convertTo(old2intermediate)
|
||||
: old2intermediate;
|
||||
|
||||
setState(() {
|
||||
value = intermediate2new;
|
||||
});
|
||||
}
|
||||
|
||||
String get value {
|
||||
return controller.text;
|
||||
}
|
||||
|
||||
set value(String txt) {
|
||||
controller.text = txt;
|
||||
}
|
||||
}
|
|
@ -352,8 +352,46 @@
|
|||
"productNameNone": "None",
|
||||
"selectCategoryLabel": "Category",
|
||||
"selectCategoryHint": "Select a category",
|
||||
"selectCategoryHelp": "Categories determine your shopping list order",
|
||||
"selectLinkedProductLabel": "Linked Product",
|
||||
"selectLinkedProductHint": "Link a product to your item",
|
||||
"selectLinkedProductHint": "Select a linked Product",
|
||||
"selectLinkedProductHelp": "Link a product to your item, to provide more information",
|
||||
"selectParentProductLabel": "Parent Product",
|
||||
"selectParentProductHint": "Nest products by choosing a parent product"
|
||||
"selectParentProductHint": "Select a parent Product",
|
||||
"selectParentProductHelp": "Nest products by choosing a parent product",
|
||||
|
||||
"unitText": "Plain text",
|
||||
"unitAmount": "Amount",
|
||||
"unitMass": "Mass",
|
||||
"unitVolume": "Volume",
|
||||
"unitLength": "Length",
|
||||
"unitArea": "Area",
|
||||
|
||||
"inputUnitValueLabel": "Value",
|
||||
"selectUnitTypeLabel": "Unit Type",
|
||||
"selectUnitLabel": "Unit",
|
||||
|
||||
"inputProductNameLabel":"Name",
|
||||
"inputProductNameHint":"Product name",
|
||||
"inputProductNameHelp":"Give the product a name",
|
||||
"inputProductDescriptionLabel":"Description",
|
||||
"inputProductDescriptionHint":"Product Description",
|
||||
"inputProductDescriptionHelp":"Give a brief description of this product",
|
||||
"inputProductEANLabel":"EAN",
|
||||
"inputProductEANHint":"Product EAN",
|
||||
"inputProductEANHelp":"Easily identify products in the shelf by looking at their ean",
|
||||
|
||||
"newItemShort": "Add",
|
||||
"newItemLong": "Add new Shopping list entry",
|
||||
"newProductShort": "New",
|
||||
"newProductLong": "Create a new product",
|
||||
|
||||
"errorProductNameShouldNotBeEmpty": "Product name shouldn't be empty",
|
||||
|
||||
"viewParentProductTitle": "View parent Product",
|
||||
"viewParentProductSubtitle": "This product is the child of a different product, have a look at it",
|
||||
"viewProductChildrenTitle": "View children",
|
||||
"viewProductChildrenSubtitle": "If other products specify this product as their parent, they are listed here",
|
||||
"editProductTitle": "Edit Product",
|
||||
"editProductSubtitle": "Change product metadata"
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import 'package:outbag_app/backend/themes.dart';
|
|||
import 'package:outbag_app/backend/user.dart';
|
||||
import 'package:outbag_app/backend/request.dart';
|
||||
import 'package:outbag_app/screens/room/categories/edit.dart';
|
||||
import 'package:outbag_app/screens/room/products/edit.dart';
|
||||
import 'package:outbag_app/screens/room/products/view.dart';
|
||||
|
||||
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
||||
|
||||
|
@ -254,11 +256,34 @@ class _OutbagAppState extends State {
|
|||
state.params['id'] ?? '')),
|
||||
GoRoute(
|
||||
name: 'edit-category',
|
||||
path: 'edit-category/:cid',
|
||||
path: 'edit-category/:category',
|
||||
builder: (context, state)=>EditCategoryPage(
|
||||
state.params['server'] ?? '',
|
||||
state.params['id'] ?? '',
|
||||
id: int.tryParse(state.params['cid'] ?? ''))),
|
||||
id: int.tryParse(state.params['category'] ?? ''))),
|
||||
GoRoute(
|
||||
name: 'new-product',
|
||||
path: 'new-product',
|
||||
builder: (context, state)=>EditProductPage(
|
||||
server: state.params['server'] ?? '',
|
||||
room: state.params['id'] ?? '',)),
|
||||
GoRoute(
|
||||
name: 'view-product',
|
||||
path: 'p/:product',
|
||||
builder: (context, state)=>ViewProductPage(
|
||||
server: state.params['server'] ?? '',
|
||||
room: state.params['id'] ?? '',
|
||||
product: int.tryParse(state.params['product'] ?? '') ?? 0),
|
||||
routes: [
|
||||
GoRoute(
|
||||
name: 'edit-product',
|
||||
path: 'edit',
|
||||
builder: (context, state)=>EditProductPage(
|
||||
server: state.params['server'] ?? '',
|
||||
room: state.params['id'] ?? '',
|
||||
product: int.tryParse(state.params['product'] ?? ''))),
|
||||
]
|
||||
),
|
||||
])
|
||||
]),
|
||||
]),
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:outbag_app/backend/permissions.dart';
|
|||
import 'package:outbag_app/backend/request.dart';
|
||||
import 'package:outbag_app/backend/room.dart';
|
||||
import 'package:outbag_app/backend/user.dart';
|
||||
import 'package:outbag_app/components/category_chip.dart';
|
||||
import 'package:outbag_app/components/category_picker.dart';
|
||||
import 'package:outbag_app/components/labeled_divider.dart';
|
||||
import 'package:outbag_app/components/product_picker.dart';
|
||||
|
@ -292,8 +293,8 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
|
|||
0))
|
||||
? FloatingActionButton.extended(
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(AppLocalizations.of(context)!.newCategoryShort),
|
||||
tooltip: AppLocalizations.of(context)!.newCategoryLong,
|
||||
label: Text(AppLocalizations.of(context)!.newItemShort),
|
||||
tooltip: AppLocalizations.of(context)!.newItemLong,
|
||||
onPressed: () {
|
||||
// show new category popup
|
||||
context.pushNamed('new-item', params: {
|
||||
|
@ -351,10 +352,7 @@ class ShoppingListItem extends StatelessWidget {
|
|||
enabled: !inCart,
|
||||
title: Text(name),
|
||||
subtitle: Text(description),
|
||||
trailing: ActionChip(
|
||||
avatar: Icon(Icons.square_rounded, color: category.color),
|
||||
label: Text(category.name),
|
||||
),
|
||||
trailing: CategoryChip(category: category,),
|
||||
onTap: () {
|
||||
if (onTap != null) {
|
||||
onTap!();
|
||||
|
@ -398,8 +396,8 @@ class ShoppingListItemInfo extends StatelessWidget {
|
|||
selected: item.category,
|
||||
enabled: false),
|
||||
ProductPicker(
|
||||
label:
|
||||
AppLocalizations.of(context)!.selectLinkedProductLabel,
|
||||
label:AppLocalizations.of(context)!.selectLinkedProductLabel,
|
||||
help:AppLocalizations.of(context)!.selectLinkedProductHelp,
|
||||
products: products,
|
||||
selected: item.link,
|
||||
enabled: false)
|
||||
|
|
|
@ -85,8 +85,8 @@ class _RoomProductsPageState extends State<RoomProductsPage> {
|
|||
0))
|
||||
? FloatingActionButton.extended(
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(AppLocalizations.of(context)!.newCategoryShort),
|
||||
tooltip: AppLocalizations.of(context)!.newCategoryLong,
|
||||
label: Text(AppLocalizations.of(context)!.newProductShort),
|
||||
tooltip: AppLocalizations.of(context)!.newProductLong,
|
||||
onPressed: () {
|
||||
// show new category popup
|
||||
context.pushNamed('new-product', params: {
|
||||
|
|
287
lib/screens/room/products/edit.dart
Normal file
287
lib/screens/room/products/edit.dart
Normal file
|
@ -0,0 +1,287 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:outbag_app/backend/request.dart';
|
||||
import 'package:outbag_app/backend/room.dart';
|
||||
import 'package:outbag_app/backend/user.dart';
|
||||
import 'package:outbag_app/components/category_picker.dart';
|
||||
import 'package:outbag_app/components/product_picker.dart';
|
||||
import 'package:outbag_app/components/value_unit_input.dart';
|
||||
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
||||
import 'package:outbag_app/tools/snackbar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class EditProductPage extends StatefulWidget {
|
||||
final int? product;
|
||||
final String server;
|
||||
final String room;
|
||||
const EditProductPage(
|
||||
{required this.server, required this.room, this.product, super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _EditProductPageState();
|
||||
}
|
||||
|
||||
class _EditProductPageState extends State<EditProductPage> {
|
||||
// input controllers
|
||||
final _ctrName = TextEditingController();
|
||||
final _ctrDescription = TextEditingController();
|
||||
final _ctrEAN = TextEditingController();
|
||||
int? _ctrCategory;
|
||||
int _ctrUnit = 0;
|
||||
String _ctrValue = '';
|
||||
int? _ctrParent;
|
||||
|
||||
// data cache
|
||||
List<RoomCategory> categories = [];
|
||||
List<RoomProduct> products = [];
|
||||
|
||||
void fetchCategories() {
|
||||
final user = context.read<User>();
|
||||
|
||||
// TODO: load cached categories first
|
||||
|
||||
doNetworkRequest(ScaffoldMessenger.of(context),
|
||||
req: () => postWithCreadentials(
|
||||
credentials: user,
|
||||
target: user.server,
|
||||
path: 'getCategories',
|
||||
body: {'room': widget.room, 'server': widget.server}),
|
||||
onOK: (body) async {
|
||||
final resp = body['data']
|
||||
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
|
||||
.toList();
|
||||
|
||||
setState(() {
|
||||
categories = resp;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void fetchProducts() {
|
||||
final user = context.read<User>();
|
||||
|
||||
// TODO: load cached products first
|
||||
|
||||
doNetworkRequest(ScaffoldMessenger.of(context),
|
||||
req: () => postWithCreadentials(
|
||||
credentials: user,
|
||||
target: user.server,
|
||||
path: 'getProducts',
|
||||
body: {'room': widget.room, 'server': widget.server}),
|
||||
onOK: (body) async {
|
||||
final resp = body['data']
|
||||
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
|
||||
.toList();
|
||||
|
||||
if (widget.product != null) {
|
||||
for (RoomProduct prod in resp) {
|
||||
// load product info
|
||||
// for current product
|
||||
if (prod.id == widget.product) {
|
||||
setState(() {
|
||||
_ctrName.text = prod.name;
|
||||
_ctrDescription.text = prod.description;
|
||||
_ctrEAN.text = prod.ean ?? '';
|
||||
_ctrUnit = prod.defaultUnit;
|
||||
_ctrValue = prod.defaultValue;
|
||||
_ctrParent = prod.parent;
|
||||
_ctrCategory = prod.category;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
products = resp;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
fetchCategories();
|
||||
fetchProducts();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text((widget.product == null)
|
||||
? AppLocalizations.of(context)!.createProduct
|
||||
: AppLocalizations.of(context)!.editProduct),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: TextField(
|
||||
controller: _ctrName,
|
||||
keyboardType: TextInputType.name,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.badge),
|
||||
labelText: AppLocalizations.of(context)!
|
||||
.inputProductNameLabel,
|
||||
hintText: AppLocalizations.of(context)!
|
||||
.inputProductNameHint,
|
||||
helperText: AppLocalizations.of(context)!
|
||||
.inputProductNameHelp,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: TextField(
|
||||
controller: _ctrDescription,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context)!
|
||||
.inputProductDescriptionLabel,
|
||||
hintText: AppLocalizations.of(context)!
|
||||
.inputProductDescriptionHint,
|
||||
helperText: AppLocalizations.of(context)!
|
||||
.inputProductDescriptionHelp,
|
||||
prefixIcon: const Icon(Icons.dns),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: TextField(
|
||||
controller: _ctrEAN,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context)!
|
||||
.inputProductEANLabel,
|
||||
hintText: AppLocalizations.of(context)!
|
||||
.inputProductEANHint,
|
||||
helperText: AppLocalizations.of(context)!
|
||||
.inputProductEANHelp,
|
||||
prefixIcon: const Icon(Icons.qr_code_rounded),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
DynamicValueUnitInput(
|
||||
initialUnit: _ctrUnit,
|
||||
initialValue: _ctrValue,
|
||||
onUnitChange: (unit) {
|
||||
setState(() {
|
||||
_ctrUnit = unit;
|
||||
});
|
||||
},
|
||||
onValueChange: (value) {
|
||||
setState(() {
|
||||
_ctrValue = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
CategoryPicker(
|
||||
label: AppLocalizations.of(context)!
|
||||
.selectCategoryLabel,
|
||||
hint: AppLocalizations.of(context)!
|
||||
.selectCategoryHint,
|
||||
categories: categories,
|
||||
selected: _ctrCategory,
|
||||
onSelect: (cid) {
|
||||
setState(() {
|
||||
_ctrCategory = cid;
|
||||
});
|
||||
},
|
||||
),
|
||||
ProductPicker(
|
||||
label: AppLocalizations.of(context)!
|
||||
.selectParentProductLabel,
|
||||
hint: AppLocalizations.of(context)!
|
||||
.selectParentProductHint,
|
||||
products: products,
|
||||
selected: _ctrParent,
|
||||
onSelect: (pid) {
|
||||
setState(() {
|
||||
_ctrParent = pid;
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
))))),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () async {
|
||||
final scaffMgr = ScaffoldMessenger.of(context);
|
||||
final router = GoRouter.of(context);
|
||||
final trans = AppLocalizations.of(context);
|
||||
final nav = Navigator.of(context);
|
||||
|
||||
if (_ctrName.text.isEmpty) {
|
||||
showSimpleSnackbar(scaffMgr,
|
||||
text: trans!.errorProductNameShouldNotBeEmpty,
|
||||
action: trans.ok);
|
||||
return;
|
||||
}
|
||||
|
||||
final user = context.read<User>();
|
||||
|
||||
if (widget.product == null) {
|
||||
doNetworkRequest(scaffMgr,
|
||||
req: () => postWithCreadentials(
|
||||
credentials: user,
|
||||
target: user.server,
|
||||
path: 'addProduct',
|
||||
body: {
|
||||
'room': widget.room,
|
||||
'server': widget.server,
|
||||
'title': _ctrName.text,
|
||||
'description': _ctrDescription.text,
|
||||
'listCatID': _ctrCategory,
|
||||
'defUnit': _ctrUnit,
|
||||
'defValue': _ctrValue,
|
||||
'ean': _ctrEAN.text,
|
||||
'parent': _ctrParent
|
||||
}),
|
||||
onOK: (_) async {
|
||||
nav.pop();
|
||||
});
|
||||
} else {
|
||||
doNetworkRequest(scaffMgr,
|
||||
req: () => postWithCreadentials(
|
||||
credentials: user,
|
||||
target: user.server,
|
||||
path: 'changeProduct',
|
||||
body: {
|
||||
'listProdID': widget.product,
|
||||
'room': widget.room,
|
||||
'server': widget.server,
|
||||
'title': _ctrName.text,
|
||||
'description': _ctrDescription.text,
|
||||
'listCatID': _ctrCategory,
|
||||
'defUnit': _ctrUnit,
|
||||
'defValue': _ctrValue,
|
||||
'ean': _ctrEAN.text,
|
||||
'parent': _ctrParent
|
||||
}),
|
||||
onOK: (_) async {
|
||||
nav.pop();
|
||||
});
|
||||
}
|
||||
},
|
||||
label: Text(widget.product != null
|
||||
? AppLocalizations.of(context)!.editProductShort
|
||||
: AppLocalizations.of(context)!.createProductShort),
|
||||
icon: Icon(widget.product != null ? Icons.edit : Icons.add)),
|
||||
);
|
||||
}
|
||||
}
|
215
lib/screens/room/products/view.dart
Normal file
215
lib/screens/room/products/view.dart
Normal file
|
@ -0,0 +1,215 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:outbag_app/backend/permissions.dart';
|
||||
import 'package:outbag_app/backend/request.dart';
|
||||
import 'package:outbag_app/backend/room.dart';
|
||||
import 'package:outbag_app/backend/user.dart';
|
||||
import 'package:outbag_app/components/category_chip.dart';
|
||||
import 'package:outbag_app/components/product_picker.dart';
|
||||
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ViewProductPage extends StatefulWidget {
|
||||
final int product;
|
||||
final String server;
|
||||
final String room;
|
||||
const ViewProductPage(
|
||||
{required this.server,
|
||||
required this.room,
|
||||
required this.product,
|
||||
super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ViewProductPageState();
|
||||
}
|
||||
|
||||
class _ViewProductPageState extends State<ViewProductPage> {
|
||||
RoomProduct? product;
|
||||
// data cache
|
||||
List<RoomProduct> products = [];
|
||||
Map<int?, RoomCategory> categories = {};
|
||||
|
||||
RoomInfo? info;
|
||||
|
||||
void fetchInfo() async {
|
||||
final sm = ScaffoldMessenger.of(context);
|
||||
final user = context.read<User>();
|
||||
|
||||
doNetworkRequest(
|
||||
sm,
|
||||
req: () => postWithCreadentials(
|
||||
path: 'getRoomInfo',
|
||||
credentials: user,
|
||||
target: user.server,
|
||||
body: {'room': widget.room, 'server': widget.server}),
|
||||
onOK: (body) async {
|
||||
final info = RoomInfo.fromJSON(body['data']);
|
||||
setState(() {
|
||||
this.info = info;
|
||||
});
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void fetchCategories() {
|
||||
final user = context.read<User>();
|
||||
|
||||
// TODO: load cached categories first
|
||||
|
||||
doNetworkRequest(ScaffoldMessenger.of(context),
|
||||
req: () => postWithCreadentials(
|
||||
credentials: user,
|
||||
target: user.server,
|
||||
path: 'getCategories',
|
||||
body: {'room': widget.room, 'server': widget.server}),
|
||||
onOK: (body) async {
|
||||
final resp = body['data']
|
||||
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
|
||||
.toList();
|
||||
|
||||
Map<int?, RoomCategory> map = {};
|
||||
|
||||
for (RoomCategory cat in resp) {
|
||||
map[cat.id] = cat;
|
||||
}
|
||||
setState(() {
|
||||
categories = map;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void fetchProducts() {
|
||||
final user = context.read<User>();
|
||||
|
||||
// TODO: load cached products first
|
||||
|
||||
doNetworkRequest(ScaffoldMessenger.of(context),
|
||||
req: () => postWithCreadentials(
|
||||
credentials: user,
|
||||
target: user.server,
|
||||
path: 'getProducts',
|
||||
body: {'room': widget.room, 'server': widget.server}),
|
||||
onOK: (body) async {
|
||||
final resp = body['data']
|
||||
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
|
||||
.toList();
|
||||
|
||||
for (RoomProduct prod in resp) {
|
||||
// load product info
|
||||
// for current product
|
||||
if (prod.id == widget.product) {
|
||||
setState(() {
|
||||
product = prod;
|
||||
});
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
products = resp;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
fetchCategories();
|
||||
fetchProducts();
|
||||
fetchInfo();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(product?.name ?? ''),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(children: [
|
||||
// display product into
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(product?.name ?? '', style: textTheme.headlineLarge),
|
||||
Text(product?.description ?? '',
|
||||
style: textTheme.titleMedium),
|
||||
Text(product?.ean ?? ''),
|
||||
CategoryChip(category: categories[product?.category]),
|
||||
ProductPicker(
|
||||
label: AppLocalizations.of(context)!
|
||||
.selectParentProductLabel,
|
||||
products: products,
|
||||
selected: product?.parent,
|
||||
enabled: false)
|
||||
],
|
||||
))),
|
||||
|
||||
// show actions (if allowed / available
|
||||
// edit product button
|
||||
...(info != null &&
|
||||
(info!.isAdmin ||
|
||||
info!.isOwner ||
|
||||
(info!.permissions & RoomPermission.editRoomContent != 0)))
|
||||
? [
|
||||
ListTile(
|
||||
title: Text(AppLocalizations.of(context)!.editProductTitle),
|
||||
subtitle:
|
||||
Text(AppLocalizations.of(context)!.editProductSubtitle),
|
||||
onTap: () {
|
||||
context.pushNamed('edit-product', params: {
|
||||
'server': widget.server,
|
||||
'id': widget.room,
|
||||
'product': widget.product.toString()
|
||||
});
|
||||
},
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
),
|
||||
]
|
||||
: [],
|
||||
// show parent?
|
||||
...(product?.parent != null)
|
||||
? [
|
||||
ListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.viewParentProductTitle),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context)!.viewParentProductSubtitle),
|
||||
onTap: () {
|
||||
context.pushNamed('view-product', params: {
|
||||
'server': widget.server,
|
||||
'id': widget.room,
|
||||
'product': product!.parent.toString()
|
||||
});
|
||||
},
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
),
|
||||
]
|
||||
: [],
|
||||
// show/manage children
|
||||
ListTile(
|
||||
title: Text(AppLocalizations.of(context)!.viewProductChildrenTitle),
|
||||
subtitle:
|
||||
Text(AppLocalizations.of(context)!.viewProductChildrenSubtitle),
|
||||
onTap: () {
|
||||
context.pushNamed('view-product-children', params: {
|
||||
'server': widget.server,
|
||||
'id': widget.room,
|
||||
'product': widget.product.toString()
|
||||
});
|
||||
},
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
),
|
||||
])),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue