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",
|
||||
"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"
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
])
|
||||
]),
|
||||
]),
|
||||
|
|
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(
|
||||
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
|
||||
}));
|
||||
})
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue