diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b046ea4..a0977c8 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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" } diff --git a/lib/main.dart b/lib/main.dart index 6f64ad6..a14791b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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), + ) ]) ]), ]), diff --git a/lib/screens/room/items/edit.dart b/lib/screens/room/items/edit.dart new file mode 100644 index 0000000..15fa1b3 --- /dev/null +++ b/lib/screens/room/items/edit.dart @@ -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 createState() => _EditItemPageState(); +} + +class _EditItemPageState extends State { + // input controllers + final _ctrName = TextEditingController(); + final _ctrDescription = TextEditingController(); + int? _ctrCategory; + int _ctrUnit = 0; + String _ctrValue = ''; + int? _ctrLink; + + // data cache + List categories = []; + List products = []; + RoomItem? item; + + void fetchCategories() { + final user = context.read(); + + // 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((raw) => RoomCategory.fromJSON(raw)) + .toList(); + + setState(() { + categories = resp; + }); + }); + } + + void fetchProducts() { + final user = context.read(); + + // 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((raw) => RoomProduct.fromJSON(raw)) + .toList(); + + setState(() { + products = resp; + }); + }); + } + + void fetchItem() { + final user = context.read(); + + // 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(); + + 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)), + ); + } +} diff --git a/lib/screens/room/pages/list.dart b/lib/screens/room/pages/list.dart index 0d3a9a9..0cd62b0 100644 --- a/lib/screens/room/pages/list.dart +++ b/lib/screens/room/pages/list.dart @@ -239,6 +239,7 @@ class _ShoppingListPageState extends State { 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 { 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 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(); + doNetworkRequest(ScaffoldMessenger.of(context), + req: () => postWithCreadentials( + credentials: user, + target: user.server, + path: 'changeItemState', + body: { + 'room': room, + 'server': server, + 'listItemID': item.id, + 'state': item.state + })); + }) ], ), );