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:
Jakob Meier 2023-04-05 19:04:18 +02:00
parent 456ed2836d
commit 192c5e3a8c
No known key found for this signature in database
GPG key ID: 66BDC7E6A01A6152
4 changed files with 381 additions and 5 deletions

View file

@ -403,5 +403,26 @@
"viewProductChildrenTitle": "View children",
"viewProductChildrenSubtitle": "If other products specify this product as their parent, they are listed here",
"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"
}

View file

@ -5,6 +5,7 @@ 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/items/edit.dart';
import 'package:outbag_app/screens/room/products/edit.dart';
import 'package:outbag_app/screens/room/products/view.dart';
@ -248,6 +249,7 @@ class _OutbagAppState extends State {
EditRoomPermissionSetPage(
state.params['server'] ?? '',
state.params['id'] ?? '')),
GoRoute(
name: 'new-category',
path: 'new-category',
@ -261,6 +263,7 @@ class _OutbagAppState extends State {
state.params['server'] ?? '',
state.params['id'] ?? '',
id: int.tryParse(state.params['category'] ?? ''))),
GoRoute(
name: 'new-product',
path: 'new-product',
@ -284,6 +287,21 @@ class _OutbagAppState extends State {
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),
)
])
]),
]),

View 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)),
);
}
}

View file

@ -239,6 +239,7 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
builder: (context) => ShoppingListItemInfo(
products: products,
category: cat,
info: widget.info,
item: item,
room: widget.room!.id,
server: widget.room!.serverTag));
@ -290,6 +291,7 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
products: products,
category: cat,
item: item,
info: widget.info,
room: widget.room!.id,
server: widget.room!.serverTag));
});
@ -360,7 +362,7 @@ class ShoppingListItem extends StatelessWidget {
background:
Icon(!inCart ? Icons.shopping_cart : Icons.remove_shopping_cart),
child: Opacity(
opacity: inCart?0.5:1.0,
opacity: inCart ? 0.5 : 1.0,
child: ListTile(
title: Text(name),
subtitle: Text(description),
@ -381,11 +383,13 @@ class ShoppingListItemInfo extends StatelessWidget {
final RoomItem item;
final String server;
final String room;
final RoomInfo? info;
final RoomCategory category;
final List<RoomProduct> products;
const ShoppingListItemInfo(
{super.key,
this.info,
required this.item,
required this.server,
required this.room,
@ -405,7 +409,9 @@ class ShoppingListItemInfo extends StatelessWidget {
child: Column(children: [
Text(item.name, style: textTheme.headlineLarge),
Text(item.description, style: textTheme.titleMedium),
CategoryChip(category: category,),
CategoryChip(
category: category,
),
Text(Unit.fromId(item.unit).display(context, item.value))
]))),
...(item.link != null)
@ -421,12 +427,65 @@ class ShoppingListItemInfo extends StatelessWidget {
context.pushNamed('view-product', params: {
'server': server,
'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
}));
})
],
),
);