Added basic Shopping List and Product list
including the ability to add item into the shopping cart and remove them. The click events have been implemented, however the routes do not exist yet. NOTE: The shopping list item info sheet is still missing the unit type and value and some more advanced options, like deleting the item
This commit is contained in:
parent
8706122590
commit
47387bb395
7 changed files with 724 additions and 27 deletions
|
@ -315,8 +315,6 @@ class Room {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ShoppingListItem {}
|
|
||||||
|
|
||||||
class RoomMember {
|
class RoomMember {
|
||||||
final String id;
|
final String id;
|
||||||
final String serverTag;
|
final String serverTag;
|
||||||
|
@ -372,6 +370,13 @@ class RoomCategory {
|
||||||
name: json['title'],
|
name: json['title'],
|
||||||
color: colorFromString(json['color']));
|
color: colorFromString(json['color']));
|
||||||
}
|
}
|
||||||
|
factory RoomCategory.other(BuildContext context) {
|
||||||
|
return RoomCategory(
|
||||||
|
id: -1,
|
||||||
|
name: AppLocalizations.of(context)!.categoryNameOther,
|
||||||
|
color: Colors.grey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static List<ColorSwatch<int>> listColors() {
|
static List<ColorSwatch<int>> listColors() {
|
||||||
return [
|
return [
|
||||||
|
@ -463,3 +468,91 @@ String colorIdFromColor(ColorSwatch<int> color) {
|
||||||
|
|
||||||
return 'purple';
|
return 'purple';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RoomProduct {
|
||||||
|
int id;
|
||||||
|
String name;
|
||||||
|
String description;
|
||||||
|
// category ID
|
||||||
|
// or null for category: "other"
|
||||||
|
int? category;
|
||||||
|
// unitID
|
||||||
|
int defaultUnit;
|
||||||
|
// NOTE: has to be string,
|
||||||
|
// as it may hold plain text,
|
||||||
|
// integers or doubles
|
||||||
|
String defaultValue;
|
||||||
|
String? ean;
|
||||||
|
// parent product ID
|
||||||
|
int? parent;
|
||||||
|
|
||||||
|
RoomProduct(
|
||||||
|
{required this.id,
|
||||||
|
required this.name,
|
||||||
|
this.description = '',
|
||||||
|
this.category = -1,
|
||||||
|
this.defaultUnit = 0,
|
||||||
|
this.defaultValue = '',
|
||||||
|
this.ean,
|
||||||
|
this.parent});
|
||||||
|
|
||||||
|
factory RoomProduct.fromJSON(dynamic json) {
|
||||||
|
return RoomProduct(
|
||||||
|
id: json['listProdID'],
|
||||||
|
name: json['title'],
|
||||||
|
description: json['description'],
|
||||||
|
category: json['category'],
|
||||||
|
defaultUnit: json['defUnit'],
|
||||||
|
defaultValue: json['defValue'],
|
||||||
|
ean: json['ean'],
|
||||||
|
parent: json['parent']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RoomItem {
|
||||||
|
int id;
|
||||||
|
int state;
|
||||||
|
String name;
|
||||||
|
String description;
|
||||||
|
// may link to a category
|
||||||
|
// null for other
|
||||||
|
int? category;
|
||||||
|
|
||||||
|
int unit;
|
||||||
|
String value;
|
||||||
|
// may link to a product
|
||||||
|
int? link;
|
||||||
|
|
||||||
|
RoomItem(
|
||||||
|
{required this.id,
|
||||||
|
required this.name,
|
||||||
|
this.description = '',
|
||||||
|
this.state = 0,
|
||||||
|
this.category = -1,
|
||||||
|
this.unit = 0,
|
||||||
|
this.value = '',
|
||||||
|
this.link});
|
||||||
|
|
||||||
|
factory RoomItem.fromJSON(dynamic json) {
|
||||||
|
return RoomItem(
|
||||||
|
id: json['listItemID'],
|
||||||
|
name: json['title'],
|
||||||
|
description: json['description'],
|
||||||
|
category: json['listCatID'],
|
||||||
|
state: json['state'],
|
||||||
|
unit: json['unit'],
|
||||||
|
value: json['value'],
|
||||||
|
link: json['listProdID']);
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomItem clone() {
|
||||||
|
return RoomItem(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
category: category,
|
||||||
|
unit: unit,
|
||||||
|
value: value,
|
||||||
|
link: link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
57
lib/components/category_picker.dart
Normal file
57
lib/components/category_picker.dart
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
// hint and label may differ depending on the screen
|
||||||
|
String? hint;
|
||||||
|
String? label;
|
||||||
|
|
||||||
|
CategoryPicker(
|
||||||
|
{required this.categories,
|
||||||
|
this.selected,
|
||||||
|
this.onSelect,
|
||||||
|
this.hint,
|
||||||
|
this.label,
|
||||||
|
this.enabled = true});
|
||||||
|
|
||||||
|
@override
|
||||||
|
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(
|
||||||
|
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,
|
||||||
|
))
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
19
lib/components/labeled_divider.dart
Normal file
19
lib/components/labeled_divider.dart
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class LabeledDivider extends StatelessWidget {
|
||||||
|
String label;
|
||||||
|
LabeledDivider(this.label);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(child: Divider()),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Text(label)),
|
||||||
|
const Expanded(child: Divider()),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
51
lib/components/product_picker.dart
Normal file
51
lib/components/product_picker.dart
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
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;
|
||||||
|
|
||||||
|
// hint and label may differ depending on the screen
|
||||||
|
String? hint;
|
||||||
|
String? label;
|
||||||
|
|
||||||
|
ProductPicker(
|
||||||
|
{required this.products,
|
||||||
|
this.selected,
|
||||||
|
this.onSelect,
|
||||||
|
this.hint,
|
||||||
|
this.label,
|
||||||
|
this.enabled = true});
|
||||||
|
|
||||||
|
@override
|
||||||
|
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(
|
||||||
|
value: null,
|
||||||
|
label: AppLocalizations.of(context)!.productNameNone,
|
||||||
|
),
|
||||||
|
// entry for every product
|
||||||
|
...products.map((product) => DropdownMenuEntry(
|
||||||
|
value: product.id,
|
||||||
|
label: product.name,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
|
@ -336,5 +336,24 @@
|
||||||
"chooseCategoryColor": "Choose a color for your category",
|
"chooseCategoryColor": "Choose a color for your category",
|
||||||
"inputCategoryNameLabel": "Category Name",
|
"inputCategoryNameLabel": "Category Name",
|
||||||
"inputCategoryNameHint": "Name the category",
|
"inputCategoryNameHint": "Name the category",
|
||||||
"inputCategoryNameHelp": "Categories can be used to sort your shopping list"
|
"inputCategoryNameHelp": "Categories can be used to sort your shopping list",
|
||||||
|
"categoryNameOther": "Other",
|
||||||
|
|
||||||
|
"createProduct": "New Product",
|
||||||
|
"createProductShort": "New",
|
||||||
|
"editProduct": "Edit Product",
|
||||||
|
"editProductShort": "Edit",
|
||||||
|
"shoppingList": "Shopping List",
|
||||||
|
"shoppingCart": "Shopping Cart",
|
||||||
|
|
||||||
|
"itemShowLinkedProductTitle": "View linked Product",
|
||||||
|
"itemShowLinkedProductSubtitle": "Displays additional information about the product",
|
||||||
|
|
||||||
|
"productNameNone": "None",
|
||||||
|
"selectCategoryLabel": "Category",
|
||||||
|
"selectCategoryHint": "Select a category",
|
||||||
|
"selectLinkedProductLabel": "Linked Product",
|
||||||
|
"selectLinkedProductHint": "Link a product to your item",
|
||||||
|
"selectParentProductLabel": "Parent Product",
|
||||||
|
"selectParentProductHint": "Nest products by choosing a parent product"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/room.dart';
|
||||||
|
import 'package:outbag_app/backend/user.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';
|
||||||
|
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
class ShoppingListPage extends StatefulWidget {
|
class ShoppingListPage extends StatefulWidget {
|
||||||
final RoomInfo? info;
|
final RoomInfo? info;
|
||||||
|
@ -12,45 +22,411 @@ class ShoppingListPage extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ShoppingListPageState extends State<ShoppingListPage> {
|
class _ShoppingListPageState extends State<ShoppingListPage> {
|
||||||
List<ShoppingListItem> list = [];
|
List<RoomItem> list = [];
|
||||||
|
List<RoomItem> cart = [];
|
||||||
|
Map<int, int> weights = {};
|
||||||
|
List<RoomCategory> categories = [];
|
||||||
|
List<RoomProduct> products = [];
|
||||||
|
|
||||||
void loadData() async {
|
void fetchItems() {
|
||||||
//bool foundData = false;
|
final user = context.read<User>();
|
||||||
|
|
||||||
// TODO: 1. load data from disk (if available)
|
// TODO: load cached items first
|
||||||
// NOTE: errors do not matter,
|
|
||||||
// hopefully the network request will succeed
|
|
||||||
try {
|
|
||||||
//List<ShoppingListItem> list = await ShoppingListItem.getAllFromDisk();
|
|
||||||
//setState(() {
|
|
||||||
// this.list = list;
|
|
||||||
//})
|
|
||||||
|
|
||||||
//foundData = true;
|
doNetworkRequest(ScaffoldMessenger.of(context),
|
||||||
} catch (_) {}
|
req: () => postWithCreadentials(
|
||||||
|
credentials: user,
|
||||||
|
target: user.server,
|
||||||
|
path: 'getItems',
|
||||||
|
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
|
||||||
|
onOK: (body) async {
|
||||||
|
final resp = body['data']
|
||||||
|
.map<RoomItem>((raw) => RoomItem.fromJSON(raw))
|
||||||
|
.toList();
|
||||||
|
|
||||||
// TODO: 2. load data from web
|
final List<RoomItem> l = [];
|
||||||
|
final List<RoomItem> c = [];
|
||||||
|
|
||||||
// NOTE: might want to close room
|
for (RoomItem item in resp) {
|
||||||
// or show snackbar if no data is available
|
if (item.state == 0) {
|
||||||
|
l.add(item);
|
||||||
|
} else {
|
||||||
|
c.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: cache items
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
list = l;
|
||||||
|
cart = c;
|
||||||
|
|
||||||
|
sortAll();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void sortAll() {
|
||||||
|
for (List<RoomItem> input in [list, cart]) {
|
||||||
|
setState(() {
|
||||||
|
input.sort((a, b) {
|
||||||
|
if (a.category == b.category) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (a.category == null) {
|
||||||
|
// b should be below
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (b.category == null) {
|
||||||
|
// a should be below
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final weightA = weights[a.category];
|
||||||
|
final weightB = weights[b.category];
|
||||||
|
// both could be null now,
|
||||||
|
// so we have to check agein
|
||||||
|
if (weightA == weightB) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (weightA == null) {
|
||||||
|
// b should be below
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (weightB == null) {
|
||||||
|
// a should be below
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return weightA.compareTo(weightB);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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?.id, 'server': widget.room?.serverTag}),
|
||||||
|
onOK: (body) async {
|
||||||
|
final resp = body['data']
|
||||||
|
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Map<int, int> map = {};
|
||||||
|
for (int i = 0; i < resp.length; i++) {
|
||||||
|
map[resp[i].id] = i;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
weights = map;
|
||||||
|
categories = resp;
|
||||||
|
sortAll();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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?.id, 'server': widget.room?.serverTag}),
|
||||||
|
onOK: (body) async {
|
||||||
|
final resp = body['data']
|
||||||
|
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
products = resp;
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
loadData();
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
fetchItems();
|
||||||
|
fetchCategories();
|
||||||
|
fetchProducts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void changeItemState(RoomItem item) {
|
||||||
|
final user = context.read<User>();
|
||||||
|
doNetworkRequest(ScaffoldMessenger.of(context),
|
||||||
|
req: () => postWithCreadentials(
|
||||||
|
credentials: user,
|
||||||
|
target: user.server,
|
||||||
|
path: 'changeItemState',
|
||||||
|
body: {
|
||||||
|
'room': widget.room?.id,
|
||||||
|
'server': widget.room?.serverTag,
|
||||||
|
'listItemID': item.id,
|
||||||
|
'state': item.state
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListView.builder(
|
return Scaffold(
|
||||||
itemBuilder: (ctx, index) {
|
body: ListView(children: [
|
||||||
final item = list[index];
|
LabeledDivider(AppLocalizations.of(context)!.shoppingList),
|
||||||
|
ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
itemCount: list.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = list[index];
|
||||||
|
|
||||||
return ListTile();
|
return ShoppingListItem(
|
||||||
},
|
name: item.name,
|
||||||
itemCount: list.length,
|
description: item.description,
|
||||||
|
category: (item.category != null)
|
||||||
|
? categories[item.category!]
|
||||||
|
: RoomCategory.other(context),
|
||||||
|
key: Key(item.id.toString()),
|
||||||
|
inCart: item.state != 0,
|
||||||
|
onDismiss: () {
|
||||||
|
// move to cart
|
||||||
|
item.state = 1;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
list.removeAt(index);
|
||||||
|
cart.add(item);
|
||||||
|
sortAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
// network request
|
||||||
|
// NOTE: for now we do not care if it is successfull,
|
||||||
|
// because the shopping cart is supposed to work even
|
||||||
|
// without network
|
||||||
|
changeItemState(item);
|
||||||
|
},
|
||||||
|
onTap: () {
|
||||||
|
// TODO: show modal bottom sheet
|
||||||
|
// containing
|
||||||
|
// - item info
|
||||||
|
// - option to view linked product (if available)
|
||||||
|
// - edit item (if allowed)
|
||||||
|
// - delete item (if allowed)
|
||||||
|
// - move to/from shopping cart?
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ShoppingListItemInfo(
|
||||||
|
products: products,
|
||||||
|
categories: categories,
|
||||||
|
item: item,
|
||||||
|
room: widget.room!.id,
|
||||||
|
server: widget.room!.serverTag));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
LabeledDivider(AppLocalizations.of(context)!.shoppingCart),
|
||||||
|
ListView.builder(
|
||||||
|
itemCount: cart.length,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = cart[index];
|
||||||
|
|
||||||
|
return ShoppingListItem(
|
||||||
|
name: item.name,
|
||||||
|
description: item.description,
|
||||||
|
category: (item.category != null)
|
||||||
|
? categories[item.category!]
|
||||||
|
: RoomCategory.other(context),
|
||||||
|
key: Key(item.id.toString()),
|
||||||
|
inCart: item.state != 0,
|
||||||
|
onDismiss: () {
|
||||||
|
// move back to list
|
||||||
|
item.state = 0;
|
||||||
|
setState(() {
|
||||||
|
cart.removeAt(index);
|
||||||
|
list.add(item);
|
||||||
|
sortAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
// network request
|
||||||
|
// NOTE: for now we do not care if it is successfull,
|
||||||
|
// because the shopping cart is supposed to work even
|
||||||
|
// without network
|
||||||
|
changeItemState(item);
|
||||||
|
},
|
||||||
|
onTap: () {
|
||||||
|
// TODO: show modal bottom sheet
|
||||||
|
// containing
|
||||||
|
// - item info
|
||||||
|
// - option to view linked product (if available)
|
||||||
|
// - edit item (if allowed)
|
||||||
|
// - delete item (if allowed)
|
||||||
|
// - move to/from shopping cart?
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ShoppingListItemInfo(
|
||||||
|
products: products,
|
||||||
|
categories: categories,
|
||||||
|
item: item,
|
||||||
|
room: widget.room!.id,
|
||||||
|
server: widget.room!.serverTag));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
floatingActionButton: ((widget.info?.isAdmin ?? false) ||
|
||||||
|
(widget.info?.isOwner ?? false) ||
|
||||||
|
((widget.info?.permissions)! & RoomPermission.editRoomContent !=
|
||||||
|
0))
|
||||||
|
? FloatingActionButton.extended(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: Text(AppLocalizations.of(context)!.newCategoryShort),
|
||||||
|
tooltip: AppLocalizations.of(context)!.newCategoryLong,
|
||||||
|
onPressed: () {
|
||||||
|
// show new category popup
|
||||||
|
context.pushNamed('new-item', params: {
|
||||||
|
'server': widget.room!.serverTag,
|
||||||
|
'id': widget.room!.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShoppingListItem extends StatelessWidget {
|
||||||
|
String name;
|
||||||
|
RoomCategory category;
|
||||||
|
String description;
|
||||||
|
bool inCart;
|
||||||
|
final Key _key;
|
||||||
|
Function()? onDismiss;
|
||||||
|
Function()? onTap;
|
||||||
|
|
||||||
|
ShoppingListItem(
|
||||||
|
{required this.name,
|
||||||
|
required this.category,
|
||||||
|
required this.inCart,
|
||||||
|
required this.description,
|
||||||
|
required key,
|
||||||
|
this.onDismiss,
|
||||||
|
this.onTap})
|
||||||
|
: _key = key;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Dismissible(
|
||||||
|
key: Key('item-$_key'),
|
||||||
|
dismissThresholds: const {
|
||||||
|
// NOTE: value might need updating
|
||||||
|
// maybe we could calculate this using the screen width
|
||||||
|
DismissDirection.horizontal: 500.0,
|
||||||
|
},
|
||||||
|
confirmDismiss: (_) async {
|
||||||
|
if (onDismiss != null) {
|
||||||
|
onDismiss!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep item in list
|
||||||
|
// NOTE: might want to set this to true/variable
|
||||||
|
// if in-shopping-cart items have a second screen
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
background:
|
||||||
|
Icon(!inCart ? Icons.shopping_cart : Icons.remove_shopping_cart),
|
||||||
|
child: ListTile(
|
||||||
|
enabled: !inCart,
|
||||||
|
title: Text(name),
|
||||||
|
subtitle: Text(description),
|
||||||
|
trailing: ActionChip(
|
||||||
|
avatar: Icon(Icons.square_rounded, color: category.color),
|
||||||
|
label: Text(category.name),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (onTap != null) {
|
||||||
|
onTap!();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShoppingListItemInfo extends StatelessWidget {
|
||||||
|
RoomItem item;
|
||||||
|
String server;
|
||||||
|
String room;
|
||||||
|
List<RoomCategory> categories = [];
|
||||||
|
List<RoomProduct> products = [];
|
||||||
|
|
||||||
|
ShoppingListItemInfo(
|
||||||
|
{required this.item,
|
||||||
|
required this.server,
|
||||||
|
required this.room,
|
||||||
|
required this.categories,
|
||||||
|
required this.products});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
return BottomSheet(
|
||||||
|
onClosing: () {},
|
||||||
|
builder: (context) => Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
child: Center(
|
||||||
|
child: Column(children: [
|
||||||
|
Text(item.name, style: textTheme.headlineLarge),
|
||||||
|
Text(item.description, style: textTheme.titleMedium),
|
||||||
|
CategoryPicker(
|
||||||
|
label: AppLocalizations.of(context)!.selectCategoryLabel,
|
||||||
|
categories: categories,
|
||||||
|
selected: item.category,
|
||||||
|
enabled: false),
|
||||||
|
ProductPicker(
|
||||||
|
label:
|
||||||
|
AppLocalizations.of(context)!.selectLinkedProductLabel,
|
||||||
|
products: products,
|
||||||
|
selected: item.link,
|
||||||
|
enabled: false)
|
||||||
|
|
||||||
|
// TODO: show more info
|
||||||
|
]))),
|
||||||
|
...(item.link != null)
|
||||||
|
? [
|
||||||
|
ListTile(
|
||||||
|
title: Text(AppLocalizations.of(context)!
|
||||||
|
.itemShowLinkedProductTitle),
|
||||||
|
subtitle: Text(AppLocalizations.of(context)!
|
||||||
|
.itemShowLinkedProductSubtitle),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
// launch "view-product" page for specific product
|
||||||
|
context.pushNamed('view-product', params: {
|
||||||
|
'server': server,
|
||||||
|
'id': room,
|
||||||
|
'product': item.id.toString()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/room.dart';
|
||||||
|
import 'package:outbag_app/backend/user.dart';
|
||||||
|
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
class RoomProductsPage extends StatefulWidget {
|
class RoomProductsPage extends StatefulWidget {
|
||||||
final RoomInfo? info;
|
final RoomInfo? info;
|
||||||
|
@ -12,8 +19,83 @@ class RoomProductsPage extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RoomProductsPageState extends State<RoomProductsPage> {
|
class _RoomProductsPageState extends State<RoomProductsPage> {
|
||||||
|
List<RoomProduct> products = [];
|
||||||
|
|
||||||
|
void fetchProducts() {
|
||||||
|
final user = context.read<User>();
|
||||||
|
|
||||||
|
doNetworkRequest(ScaffoldMessenger.of(context),
|
||||||
|
req: () => postWithCreadentials(
|
||||||
|
credentials: user,
|
||||||
|
target: user.server,
|
||||||
|
path: 'getProducts',
|
||||||
|
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
|
||||||
|
onOK: (body) async {
|
||||||
|
final resp = body['data']
|
||||||
|
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// TODO: cache products
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
products = resp;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => fetchProducts());
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Text('Products');
|
return Scaffold(
|
||||||
|
body: ListView.builder(
|
||||||
|
itemCount: products.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = products[index];
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(item.name),
|
||||||
|
subtitle: Text(item.description),
|
||||||
|
onTap: () {
|
||||||
|
// NOTE: we could also show a bottom sheet here,
|
||||||
|
// but because we need a seperate page/route either way
|
||||||
|
// (for viewing item-links and exploring the product-tree)
|
||||||
|
// we might as well use the view-product page here
|
||||||
|
// NOTE: This might seem inconsistent,
|
||||||
|
// but you probably wont ever need to read a product description,
|
||||||
|
// where as reading the shopping item description,
|
||||||
|
// might be a good idea
|
||||||
|
context.pushNamed('view-product', params: {
|
||||||
|
'server': widget.room!.serverTag,
|
||||||
|
'id': widget.room!.id,
|
||||||
|
'product': item.id.toString()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
floatingActionButton: ((widget.info?.isAdmin ?? false) ||
|
||||||
|
(widget.info?.isOwner ?? false) ||
|
||||||
|
((widget.info?.permissions)! & RoomPermission.editRoomContent !=
|
||||||
|
0))
|
||||||
|
? FloatingActionButton.extended(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: Text(AppLocalizations.of(context)!.newCategoryShort),
|
||||||
|
tooltip: AppLocalizations.of(context)!.newCategoryLong,
|
||||||
|
onPressed: () {
|
||||||
|
// show new category popup
|
||||||
|
context.pushNamed('new-product', params: {
|
||||||
|
'server': widget.room!.serverTag,
|
||||||
|
'id': widget.room!.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue