Added basic edit/create item screen
NOTE: The create screen needs more UX design, at least some UI improvements to get it closer to the alpha2 screen. IDEA: use Stepper to template item from product NOTE: The delete item button is not implemented
This commit is contained in:
parent
456ed2836d
commit
192c5e3a8c
4 changed files with 381 additions and 5 deletions
|
@ -403,5 +403,26 @@
|
||||||
"viewProductChildrenTitle": "View children",
|
"viewProductChildrenTitle": "View children",
|
||||||
"viewProductChildrenSubtitle": "If other products specify this product as their parent, they are listed here",
|
"viewProductChildrenSubtitle": "If other products specify this product as their parent, they are listed here",
|
||||||
"editProductTitle": "Edit Product",
|
"editProductTitle": "Edit Product",
|
||||||
"editProductSubtitle": "Change product metadata"
|
"editProductSubtitle": "Change product metadata",
|
||||||
|
|
||||||
|
"createItem": "New Item",
|
||||||
|
"createItemShort": "Create",
|
||||||
|
"createItemLong": "Create a new shopping list Item",
|
||||||
|
"editItem": "Edit Item",
|
||||||
|
"editItemShort": "Edit",
|
||||||
|
"editItemLong": "Modify shopping list item",
|
||||||
|
"inputItemNameLabel":"Name",
|
||||||
|
"inputItemNameHint":"Item name",
|
||||||
|
"inputItemNameHelp":"Give the item a name",
|
||||||
|
"inputItemDescriptionLabel":"Description",
|
||||||
|
"inputItemDescriptionHint":"Item Description",
|
||||||
|
"inputItemDescriptionHelp":"Give a brief description of this item",
|
||||||
|
|
||||||
|
"deleteItemTitle": "Delete Item",
|
||||||
|
"deleteItemSubtitle": "Remove the item from the shopping list",
|
||||||
|
|
||||||
|
"moveItemToCartTitle": "Move to Cart",
|
||||||
|
"moveItemToCartSubtitle": "Mark item as in-cart, so others know, you bought it",
|
||||||
|
"removeItemFromCartTitle": "Remove from Cart",
|
||||||
|
"removeItemFromCartSubtitle": "Remove item from shopping cart, so others know, that you still need it"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:outbag_app/backend/themes.dart';
|
||||||
import 'package:outbag_app/backend/user.dart';
|
import 'package:outbag_app/backend/user.dart';
|
||||||
import 'package:outbag_app/backend/request.dart';
|
import 'package:outbag_app/backend/request.dart';
|
||||||
import 'package:outbag_app/screens/room/categories/edit.dart';
|
import 'package:outbag_app/screens/room/categories/edit.dart';
|
||||||
|
import 'package:outbag_app/screens/room/items/edit.dart';
|
||||||
import 'package:outbag_app/screens/room/products/edit.dart';
|
import 'package:outbag_app/screens/room/products/edit.dart';
|
||||||
import 'package:outbag_app/screens/room/products/view.dart';
|
import 'package:outbag_app/screens/room/products/view.dart';
|
||||||
|
|
||||||
|
@ -248,6 +249,7 @@ class _OutbagAppState extends State {
|
||||||
EditRoomPermissionSetPage(
|
EditRoomPermissionSetPage(
|
||||||
state.params['server'] ?? '',
|
state.params['server'] ?? '',
|
||||||
state.params['id'] ?? '')),
|
state.params['id'] ?? '')),
|
||||||
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'new-category',
|
name: 'new-category',
|
||||||
path: 'new-category',
|
path: 'new-category',
|
||||||
|
@ -261,6 +263,7 @@ class _OutbagAppState extends State {
|
||||||
state.params['server'] ?? '',
|
state.params['server'] ?? '',
|
||||||
state.params['id'] ?? '',
|
state.params['id'] ?? '',
|
||||||
id: int.tryParse(state.params['category'] ?? ''))),
|
id: int.tryParse(state.params['category'] ?? ''))),
|
||||||
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'new-product',
|
name: 'new-product',
|
||||||
path: 'new-product',
|
path: 'new-product',
|
||||||
|
@ -284,6 +287,21 @@ class _OutbagAppState extends State {
|
||||||
product: int.tryParse(state.params['product'] ?? ''))),
|
product: int.tryParse(state.params['product'] ?? ''))),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
||||||
|
GoRoute(
|
||||||
|
name: 'new-item',
|
||||||
|
path: 'new-item',
|
||||||
|
builder: (context, state)=>EditItemPage(
|
||||||
|
server: state.params['server'] ?? '',
|
||||||
|
room: state.params['id'] ?? '',)),
|
||||||
|
GoRoute(
|
||||||
|
name: 'edit-item',
|
||||||
|
path: 'i/:item',
|
||||||
|
builder: (context, state)=>EditItemPage(
|
||||||
|
server: state.params['server'] ?? '',
|
||||||
|
room: state.params['id'] ?? '',
|
||||||
|
item: int.tryParse(state.params['item'] ?? '') ?? 0),
|
||||||
|
)
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
|
278
lib/screens/room/items/edit.dart
Normal file
278
lib/screens/room/items/edit.dart
Normal file
|
@ -0,0 +1,278 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.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 EditItemPage extends StatefulWidget {
|
||||||
|
final String room;
|
||||||
|
final String server;
|
||||||
|
|
||||||
|
final int? item;
|
||||||
|
|
||||||
|
const EditItemPage(
|
||||||
|
{super.key, required this.room, required this.server, this.item});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _EditItemPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditItemPageState extends State<EditItemPage> {
|
||||||
|
// input controllers
|
||||||
|
final _ctrName = TextEditingController();
|
||||||
|
final _ctrDescription = TextEditingController();
|
||||||
|
int? _ctrCategory;
|
||||||
|
int _ctrUnit = 0;
|
||||||
|
String _ctrValue = '';
|
||||||
|
int? _ctrLink;
|
||||||
|
|
||||||
|
// data cache
|
||||||
|
List<RoomCategory> categories = [];
|
||||||
|
List<RoomProduct> products = [];
|
||||||
|
RoomItem? item;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
products = resp;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void fetchItem() {
|
||||||
|
final user = context.read<User>();
|
||||||
|
|
||||||
|
// TODO: load cached item first
|
||||||
|
|
||||||
|
doNetworkRequest(ScaffoldMessenger.of(context),
|
||||||
|
req: () => postWithCreadentials(
|
||||||
|
credentials: user,
|
||||||
|
target: user.server,
|
||||||
|
path: 'getItem',
|
||||||
|
body: {
|
||||||
|
'room': widget.room,
|
||||||
|
'server': widget.server,
|
||||||
|
'listItemID': widget.item
|
||||||
|
}),
|
||||||
|
onOK: (body) async {
|
||||||
|
final resp = RoomItem.fromJSON(body['data']);
|
||||||
|
setState(() {
|
||||||
|
item = resp;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
fetchCategories();
|
||||||
|
fetchProducts();
|
||||||
|
|
||||||
|
if (widget.item != null) {
|
||||||
|
fetchItem();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text((widget.item == null)
|
||||||
|
? AppLocalizations.of(context)!.createItem
|
||||||
|
: AppLocalizations.of(context)!.editItem),
|
||||||
|
),
|
||||||
|
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)!
|
||||||
|
.inputItemNameLabel,
|
||||||
|
hintText: AppLocalizations.of(context)!
|
||||||
|
.inputItemNameHint,
|
||||||
|
helperText: AppLocalizations.of(context)!
|
||||||
|
.inputItemNameHelp,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ProductPicker(
|
||||||
|
label: AppLocalizations.of(context)!
|
||||||
|
.selectLinkedProductLabel,
|
||||||
|
hint: AppLocalizations.of(context)!
|
||||||
|
.selectLinkedProductHint,
|
||||||
|
products: products,
|
||||||
|
selected: _ctrLink,
|
||||||
|
onSelect: (pid) {
|
||||||
|
setState(() {
|
||||||
|
_ctrLink = pid;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: TextField(
|
||||||
|
controller: _ctrDescription,
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context)!
|
||||||
|
.inputItemDescriptionLabel,
|
||||||
|
hintText: AppLocalizations.of(context)!
|
||||||
|
.inputItemDescriptionHint,
|
||||||
|
helperText: AppLocalizations.of(context)!
|
||||||
|
.inputItemDescriptionHelp,
|
||||||
|
prefixIcon: const Icon(Icons.dns),
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
))))),
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
onPressed: () async {
|
||||||
|
final scaffMgr = ScaffoldMessenger.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.item == null) {
|
||||||
|
doNetworkRequest(scaffMgr,
|
||||||
|
req: () => postWithCreadentials(
|
||||||
|
credentials: user,
|
||||||
|
target: user.server,
|
||||||
|
path: 'addItem',
|
||||||
|
body: {
|
||||||
|
'room': widget.room,
|
||||||
|
'server': widget.server,
|
||||||
|
'state': 0,
|
||||||
|
'title': _ctrName.text,
|
||||||
|
'description': _ctrDescription.text,
|
||||||
|
'listCatID': _ctrCategory,
|
||||||
|
'unit': _ctrUnit,
|
||||||
|
'value': _ctrValue,
|
||||||
|
'listProdID': _ctrLink
|
||||||
|
}),
|
||||||
|
onOK: (_) async {
|
||||||
|
nav.pop();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
doNetworkRequest(scaffMgr,
|
||||||
|
req: () => postWithCreadentials(
|
||||||
|
credentials: user,
|
||||||
|
target: user.server,
|
||||||
|
path: 'changeItem',
|
||||||
|
body: {
|
||||||
|
'listItemID': widget.item,
|
||||||
|
'room': widget.room,
|
||||||
|
'server': widget.server,
|
||||||
|
'title': _ctrName.text,
|
||||||
|
'description': _ctrDescription.text,
|
||||||
|
'listCatID': _ctrCategory,
|
||||||
|
'defUnit': _ctrUnit,
|
||||||
|
'defValue': _ctrValue,
|
||||||
|
'listProdID': _ctrLink
|
||||||
|
}),
|
||||||
|
onOK: (_) async {
|
||||||
|
nav.pop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label: Text(widget.item != null
|
||||||
|
? AppLocalizations.of(context)!.editItemShort
|
||||||
|
: AppLocalizations.of(context)!.createItemShort),
|
||||||
|
icon: Icon(widget.item != null ? Icons.edit : Icons.add)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -239,6 +239,7 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
|
||||||
builder: (context) => ShoppingListItemInfo(
|
builder: (context) => ShoppingListItemInfo(
|
||||||
products: products,
|
products: products,
|
||||||
category: cat,
|
category: cat,
|
||||||
|
info: widget.info,
|
||||||
item: item,
|
item: item,
|
||||||
room: widget.room!.id,
|
room: widget.room!.id,
|
||||||
server: widget.room!.serverTag));
|
server: widget.room!.serverTag));
|
||||||
|
@ -290,6 +291,7 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
|
||||||
products: products,
|
products: products,
|
||||||
category: cat,
|
category: cat,
|
||||||
item: item,
|
item: item,
|
||||||
|
info: widget.info,
|
||||||
room: widget.room!.id,
|
room: widget.room!.id,
|
||||||
server: widget.room!.serverTag));
|
server: widget.room!.serverTag));
|
||||||
});
|
});
|
||||||
|
@ -381,11 +383,13 @@ class ShoppingListItemInfo extends StatelessWidget {
|
||||||
final RoomItem item;
|
final RoomItem item;
|
||||||
final String server;
|
final String server;
|
||||||
final String room;
|
final String room;
|
||||||
|
final RoomInfo? info;
|
||||||
final RoomCategory category;
|
final RoomCategory category;
|
||||||
final List<RoomProduct> products;
|
final List<RoomProduct> products;
|
||||||
|
|
||||||
const ShoppingListItemInfo(
|
const ShoppingListItemInfo(
|
||||||
{super.key,
|
{super.key,
|
||||||
|
this.info,
|
||||||
required this.item,
|
required this.item,
|
||||||
required this.server,
|
required this.server,
|
||||||
required this.room,
|
required this.room,
|
||||||
|
@ -405,7 +409,9 @@ class ShoppingListItemInfo extends StatelessWidget {
|
||||||
child: Column(children: [
|
child: Column(children: [
|
||||||
Text(item.name, style: textTheme.headlineLarge),
|
Text(item.name, style: textTheme.headlineLarge),
|
||||||
Text(item.description, style: textTheme.titleMedium),
|
Text(item.description, style: textTheme.titleMedium),
|
||||||
CategoryChip(category: category,),
|
CategoryChip(
|
||||||
|
category: category,
|
||||||
|
),
|
||||||
Text(Unit.fromId(item.unit).display(context, item.value))
|
Text(Unit.fromId(item.unit).display(context, item.value))
|
||||||
]))),
|
]))),
|
||||||
...(item.link != null)
|
...(item.link != null)
|
||||||
|
@ -421,12 +427,65 @@ class ShoppingListItemInfo extends StatelessWidget {
|
||||||
context.pushNamed('view-product', params: {
|
context.pushNamed('view-product', params: {
|
||||||
'server': server,
|
'server': server,
|
||||||
'id': room,
|
'id': room,
|
||||||
'product': item.id.toString()
|
'product': item.link.toString()
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
: []
|
: [],
|
||||||
|
...(info != null &&
|
||||||
|
((info?.isAdmin ?? false) ||
|
||||||
|
(info?.isOwner ?? false) ||
|
||||||
|
((info?.permissions)! &
|
||||||
|
RoomPermission.addShoppingListItems !=
|
||||||
|
0)))
|
||||||
|
? [
|
||||||
|
ListTile(
|
||||||
|
title: Text(AppLocalizations.of(context)!.editItem),
|
||||||
|
subtitle: Text(AppLocalizations.of(context)!.editItemLong),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
context.pushNamed('edit-product', params: {
|
||||||
|
'server': server,
|
||||||
|
'id': room,
|
||||||
|
'item': item.id.toString()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title:
|
||||||
|
Text(AppLocalizations.of(context)!.deleteItemTitle),
|
||||||
|
subtitle: Text(
|
||||||
|
AppLocalizations.of(context)!.deleteItemSubtitle),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
// TODO: show confirm dialog
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
ListTile(
|
||||||
|
title: Text(item.state == 0
|
||||||
|
? AppLocalizations.of(context)!.moveItemToCartTitle
|
||||||
|
: AppLocalizations.of(context)!.moveItemToCartSubtitle),
|
||||||
|
subtitle: Text(item.state == 0
|
||||||
|
? AppLocalizations.of(context)!.removeItemFromCartTitle
|
||||||
|
: AppLocalizations.of(context)!.removeItemFromCartSubtitle),
|
||||||
|
onTap: () {
|
||||||
|
// flip state
|
||||||
|
item.state = (item.state - 1).abs();
|
||||||
|
final user = context.read<User>();
|
||||||
|
doNetworkRequest(ScaffoldMessenger.of(context),
|
||||||
|
req: () => postWithCreadentials(
|
||||||
|
credentials: user,
|
||||||
|
target: user.server,
|
||||||
|
path: 'changeItemState',
|
||||||
|
body: {
|
||||||
|
'room': room,
|
||||||
|
'server': server,
|
||||||
|
'listItemID': item.id,
|
||||||
|
'state': item.state
|
||||||
|
}));
|
||||||
|
})
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue