2023-03-22 21:16:00 +01:00
|
|
|
import 'package:flutter/material.dart';
|
2023-04-04 10:29:29 +02:00
|
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import 'package:outbag_app/backend/permissions.dart';
|
|
|
|
import 'package:outbag_app/backend/request.dart';
|
2023-03-22 21:16:00 +01:00
|
|
|
import 'package:outbag_app/backend/room.dart';
|
2023-04-04 10:29:29 +02:00
|
|
|
import 'package:outbag_app/backend/user.dart';
|
2023-04-04 20:28:26 +02:00
|
|
|
import 'package:outbag_app/components/category_chip.dart';
|
2023-04-04 10:29:29 +02:00
|
|
|
import 'package:outbag_app/components/category_picker.dart';
|
|
|
|
import 'package:outbag_app/components/labeled_divider.dart';
|
|
|
|
import 'package:outbag_app/components/product_picker.dart';
|
2023-04-05 10:22:43 +02:00
|
|
|
import 'package:outbag_app/components/value_unit_input.dart';
|
2023-04-04 10:29:29 +02:00
|
|
|
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
|
|
|
import 'package:provider/provider.dart';
|
|
|
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
2023-03-22 21:16:00 +01:00
|
|
|
|
|
|
|
class ShoppingListPage extends StatefulWidget {
|
|
|
|
final RoomInfo? info;
|
|
|
|
final Room? room;
|
|
|
|
|
|
|
|
const ShoppingListPage(this.room, this.info, {super.key});
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<StatefulWidget> createState() => _ShoppingListPageState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _ShoppingListPageState extends State<ShoppingListPage> {
|
2023-04-04 10:29:29 +02:00
|
|
|
List<RoomItem> list = [];
|
|
|
|
List<RoomItem> cart = [];
|
|
|
|
Map<int, int> weights = {};
|
2023-04-05 10:07:42 +02:00
|
|
|
Map<int?, RoomCategory> categories = {};
|
2023-04-04 10:29:29 +02:00
|
|
|
List<RoomProduct> products = [];
|
2023-03-22 21:16:00 +01:00
|
|
|
|
2023-04-04 10:29:29 +02:00
|
|
|
void fetchItems() {
|
|
|
|
final user = context.read<User>();
|
2023-03-22 21:16:00 +01:00
|
|
|
|
2023-04-04 10:29:29 +02:00
|
|
|
// TODO: load cached items first
|
2023-03-22 21:16:00 +01:00
|
|
|
|
2023-04-04 10:29:29 +02:00
|
|
|
doNetworkRequest(ScaffoldMessenger.of(context),
|
|
|
|
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();
|
2023-03-22 21:16:00 +01:00
|
|
|
|
2023-04-04 10:29:29 +02:00
|
|
|
final List<RoomItem> l = [];
|
|
|
|
final List<RoomItem> c = [];
|
2023-03-22 21:16:00 +01:00
|
|
|
|
2023-04-04 10:29:29 +02:00
|
|
|
for (RoomItem item in resp) {
|
|
|
|
if (item.state == 0) {
|
|
|
|
l.add(item);
|
|
|
|
} else {
|
|
|
|
c.add(item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: cache items
|
|
|
|
|
2023-04-05 08:49:46 +02:00
|
|
|
if (mounted) {
|
|
|
|
setState(() {
|
|
|
|
list = l;
|
|
|
|
cart = c;
|
2023-04-04 10:29:29 +02:00
|
|
|
|
2023-04-05 08:49:46 +02:00
|
|
|
sortAll();
|
|
|
|
});
|
|
|
|
}
|
2023-04-04 10:29:29 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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 = {};
|
2023-04-05 10:07:42 +02:00
|
|
|
Map<int?, RoomCategory> cat = {};
|
2023-04-04 10:29:29 +02:00
|
|
|
for (int i = 0; i < resp.length; i++) {
|
|
|
|
map[resp[i].id] = i;
|
2023-04-05 10:07:42 +02:00
|
|
|
cat[resp[i].id] = resp[i];
|
2023-04-04 10:29:29 +02:00
|
|
|
}
|
2023-04-05 08:49:46 +02:00
|
|
|
|
|
|
|
if (mounted) {
|
|
|
|
setState(() {
|
|
|
|
weights = map;
|
2023-04-05 10:07:42 +02:00
|
|
|
categories = cat;
|
2023-04-05 08:49:46 +02:00
|
|
|
sortAll();
|
|
|
|
});
|
|
|
|
}
|
2023-04-04 10:29:29 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
2023-04-05 08:49:46 +02:00
|
|
|
if (mounted) {
|
|
|
|
setState(() {
|
|
|
|
products = resp;
|
|
|
|
});
|
|
|
|
}
|
2023-04-04 10:29:29 +02:00
|
|
|
});
|
2023-03-22 21:16:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
|
|
|
|
2023-04-04 10:29:29 +02:00
|
|
|
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
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Scaffold(
|
|
|
|
body: ListView(children: [
|
|
|
|
LabeledDivider(AppLocalizations.of(context)!.shoppingList),
|
|
|
|
ListView.builder(
|
|
|
|
shrinkWrap: true,
|
|
|
|
physics: const ClampingScrollPhysics(),
|
|
|
|
itemCount: list.length,
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
final item = list[index];
|
2023-04-05 10:07:42 +02:00
|
|
|
final cat =
|
|
|
|
categories[item.category] ?? RoomCategory.other(context);
|
2023-04-04 10:29:29 +02:00
|
|
|
return ShoppingListItem(
|
|
|
|
name: item.name,
|
|
|
|
description: item.description,
|
2023-04-05 09:23:41 +02:00
|
|
|
category: cat,
|
2023-04-04 10:29:29 +02:00
|
|
|
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,
|
2023-04-05 10:07:42 +02:00
|
|
|
category: cat,
|
2023-04-04 10:29:29 +02:00
|
|
|
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];
|
2023-04-05 10:07:42 +02:00
|
|
|
final cat =
|
|
|
|
categories[item.category] ?? RoomCategory.other(context);
|
2023-04-04 10:29:29 +02:00
|
|
|
|
|
|
|
return ShoppingListItem(
|
|
|
|
name: item.name,
|
|
|
|
description: item.description,
|
2023-04-05 10:07:42 +02:00
|
|
|
category: cat,
|
2023-04-04 10:29:29 +02:00
|
|
|
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: () {
|
2023-04-05 10:07:42 +02:00
|
|
|
// show modal bottom sheet
|
2023-04-04 10:29:29 +02:00
|
|
|
// 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,
|
2023-04-05 10:07:42 +02:00
|
|
|
category: cat,
|
2023-04-04 10:29:29 +02:00
|
|
|
item: item,
|
|
|
|
room: widget.room!.id,
|
|
|
|
server: widget.room!.serverTag));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
)
|
|
|
|
]),
|
2023-04-05 09:23:41 +02:00
|
|
|
floatingActionButton: (widget.info != null &&
|
|
|
|
((widget.info?.isAdmin ?? false) ||
|
|
|
|
(widget.info?.isOwner ?? false) ||
|
|
|
|
((widget.info?.permissions)! &
|
|
|
|
RoomPermission.addShoppingListItems !=
|
|
|
|
0)))
|
2023-04-04 10:29:29 +02:00
|
|
|
? FloatingActionButton.extended(
|
|
|
|
icon: const Icon(Icons.add),
|
2023-04-04 20:28:26 +02:00
|
|
|
label: Text(AppLocalizations.of(context)!.newItemShort),
|
|
|
|
tooltip: AppLocalizations.of(context)!.newItemLong,
|
2023-04-04 10:29:29 +02:00
|
|
|
onPressed: () {
|
|
|
|
// show new category popup
|
|
|
|
context.pushNamed('new-item', params: {
|
|
|
|
'server': widget.room!.serverTag,
|
|
|
|
'id': widget.room!.id,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
)
|
|
|
|
: null,
|
|
|
|
);
|
2023-03-22 21:16:00 +01:00
|
|
|
}
|
2023-04-04 10:29:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
class ShoppingListItem extends StatelessWidget {
|
2023-04-05 10:22:43 +02:00
|
|
|
final String name;
|
|
|
|
final RoomCategory category;
|
|
|
|
final String description;
|
|
|
|
final bool inCart;
|
2023-04-04 10:29:29 +02:00
|
|
|
final Key _key;
|
2023-04-05 10:22:43 +02:00
|
|
|
final Function()? onDismiss;
|
|
|
|
final Function()? onTap;
|
2023-04-04 10:29:29 +02:00
|
|
|
|
2023-04-05 10:22:43 +02:00
|
|
|
const ShoppingListItem(
|
2023-04-04 10:29:29 +02:00
|
|
|
{required this.name,
|
|
|
|
required this.category,
|
|
|
|
required this.inCart,
|
|
|
|
required this.description,
|
|
|
|
required key,
|
|
|
|
this.onDismiss,
|
|
|
|
this.onTap})
|
|
|
|
: _key = key;
|
2023-03-22 21:16:00 +01:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2023-04-04 10:29:29 +02:00
|
|
|
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!();
|
|
|
|
}
|
2023-03-22 21:16:00 +01:00
|
|
|
|
2023-04-04 10:29:29 +02:00
|
|
|
// keep item in list
|
|
|
|
// NOTE: might want to set this to true/variable
|
|
|
|
// if in-shopping-cart items have a second screen
|
|
|
|
return true;
|
2023-03-22 21:16:00 +01:00
|
|
|
},
|
2023-04-04 10:29:29 +02:00
|
|
|
background:
|
|
|
|
Icon(!inCart ? Icons.shopping_cart : Icons.remove_shopping_cart),
|
2023-04-05 10:07:42 +02:00
|
|
|
child: Opacity(
|
|
|
|
opacity: inCart?0.5:1.0,
|
|
|
|
child: ListTile(
|
2023-04-05 10:22:43 +02:00
|
|
|
title: Text(name),
|
|
|
|
subtitle: Text(description),
|
|
|
|
trailing: CategoryChip(
|
|
|
|
category: category,
|
|
|
|
),
|
|
|
|
onTap: () {
|
|
|
|
if (onTap != null) {
|
|
|
|
onTap!();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
)),
|
2023-04-04 10:29:29 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class ShoppingListItemInfo extends StatelessWidget {
|
2023-04-05 10:07:42 +02:00
|
|
|
final RoomItem item;
|
|
|
|
final String server;
|
|
|
|
final String room;
|
|
|
|
final RoomCategory category;
|
|
|
|
final List<RoomProduct> products;
|
|
|
|
|
|
|
|
const ShoppingListItemInfo(
|
|
|
|
{super.key,
|
|
|
|
required this.item,
|
2023-04-04 10:29:29 +02:00
|
|
|
required this.server,
|
|
|
|
required this.room,
|
2023-04-05 10:07:42 +02:00
|
|
|
required this.category,
|
2023-04-04 10:29:29 +02:00
|
|
|
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),
|
2023-04-05 10:07:42 +02:00
|
|
|
CategoryChip(category: category,),
|
2023-04-05 10:22:43 +02:00
|
|
|
Text(Unit.fromId(item.unit).display(context, item.value))
|
2023-04-04 10:29:29 +02:00
|
|
|
]))),
|
|
|
|
...(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()
|
|
|
|
});
|
|
|
|
},
|
|
|
|
)
|
|
|
|
]
|
|
|
|
: []
|
|
|
|
],
|
|
|
|
),
|
2023-03-22 21:16:00 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|