actions-test/lib/screens/room/pages/list.dart

690 lines
24 KiB
Dart
Raw Normal View History

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/user.dart';
import 'package:outbag_app/components/category_chip.dart';
import 'package:outbag_app/components/labeled_divider.dart';
import 'package:outbag_app/components/value_unit_input.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 {
final RoomInfo? info;
final Room? room;
const ShoppingListPage(this.room, this.info, {super.key});
@override
State<StatefulWidget> createState() => _ShoppingListPageState();
}
class _ShoppingListPageState extends State<ShoppingListPage> {
List<RoomItem> list = [];
List<RoomItem> cart = [];
Map<int, int> weights = {};
Map<int?, RoomCategory> categories = {};
List<RoomProduct> products = [];
void fetchItems() async {
final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
// load cached items first
final cache = await RoomItem.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
final List<RoomItem> l = [];
final List<RoomItem> c = [];
for (RoomItem item in cache) {
if (item.state == 0) {
l.add(item);
} else {
c.add(item);
}
// cache items
await item.toDisk();
}
if (mounted) {
setState(() {
list = l;
cart = c;
sortAll();
});
}
doNetworkRequest(scaffmgr,
2023-12-22 20:14:36 +01:00
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(
widget.room?.serverTag ?? "", widget.room?.id ?? "", raw))
2023-12-22 20:14:36 +01:00
.toList();
final List<RoomItem> l = [];
final List<RoomItem> c = [];
for (RoomItem item in resp) {
if (item.state == 0) {
l.add(item);
} else {
c.add(item);
}
// cache items
await item.toDisk();
}
2023-12-22 20:14:36 +01:00
if (mounted) {
setState(() {
list = l;
cart = c;
sortAll();
2023-12-22 20:14:36 +01:00
});
}
});
}
void sortAll() {
for (List<RoomItem> input in [list, cart]) {
setState(() {
2023-12-22 20:14:36 +01:00
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;
}
2023-12-22 20:14:36 +01:00
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;
}
2023-12-22 20:14:36 +01:00
return weightA.compareTo(weightB);
});
});
}
}
void fetchCategories() async {
final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
// load cached categories
final resp = await RoomCategory.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
if (mounted) {
Map<int, int> map = {};
Map<int?, RoomCategory> cat = {};
for (int i = 0; i < resp.length; i++) {
map[resp[i].id ?? 0] = i;
cat[resp[i].id ?? 0] = resp[i];
}
if (mounted) {
setState(() {
weights = map;
categories = cat;
sortAll();
});
}
}
doNetworkRequest(scaffmgr,
2023-12-22 20:14:36 +01:00
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(
widget.room?.serverTag ?? "", widget.room?.id ?? "", raw))
2023-12-22 20:14:36 +01:00
.toList();
Map<int, int> map = {};
Map<int?, RoomCategory> cat = {};
for (int i = 0; i < resp.length; i++) {
map[resp[i].id] = i;
cat[resp[i].id] = resp[i];
}
2023-12-22 20:14:36 +01:00
if (mounted) {
setState(() {
weights = map;
categories = cat;
sortAll();
2023-12-22 20:14:36 +01:00
});
}
});
}
void fetchProducts() async {
final user = context.read<User>();
final scaffmgr = ScaffoldMessenger.of(context);
// load cached products first
final cache = await RoomProduct.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
if (mounted) {
setState(() {
products = cache;
});
}
doNetworkRequest(scaffmgr,
2023-12-22 20:14:36 +01:00
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(
widget.room!.serverTag, widget.room!.id, raw))
2023-12-22 20:14:36 +01:00
.toList();
if (mounted) {
setState(() {
products = resp;
2023-12-22 20:14:36 +01:00
});
}
});
}
@override
void initState() {
super.initState();
// wait for background room item changes
RoomItem.listen(widget.room?.serverTag ?? "", widget.room?.id ?? "",
(_) async {
try {
final updated = await RoomItem.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
final List<RoomItem> l = [];
final List<RoomItem> c = [];
for (RoomItem item in updated) {
if (item.state == 0) {
l.add(item);
} else {
c.add(item);
}
}
if (mounted) {
setState(() {
list = l;
cart = c;
sortAll();
});
}
} catch (_) {}
});
// wait for background room product changes
RoomProduct.listen(widget.room?.serverTag ?? "", widget.room?.id ?? "",
(_) async {
try {
final updated = await RoomProduct.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
setState(() {
products = updated;
});
} catch (_) {}
});
// wait for background room category changes
RoomCategory.listen(widget.room?.serverTag ?? "", widget.room?.id ?? "",
(_) async {
try {
final resp = await RoomCategory.list(
widget.room?.serverTag ?? "", widget.room?.id ?? "");
Map<int, int> map = {};
Map<int?, RoomCategory> cat = {};
for (int i = 0; i < resp.length; i++) {
map[resp[i].id ?? 0] = i;
cat[resp[i].id ?? 0] = resp[i];
}
if (mounted) {
setState(() {
weights = map;
categories = cat;
sortAll();
});
}
} catch (_) {}
});
WidgetsBinding.instance.addPostFrameCallback((_) {
2023-12-22 20:14:36 +01:00
fetchItems();
fetchCategories();
fetchProducts();
});
}
void changeItemState(RoomItem item) {
final user = context.read<User>();
doNetworkRequest(ScaffoldMessenger.of(context),
2023-12-22 20:14:36 +01:00
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: Center(
child: Padding(
padding: const EdgeInsets.all(14),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: ListView(children: [
LabeledDivider(AppLocalizations.of(context)!.shoppingList),
ListView.builder(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemCount: list.length,
itemBuilder: (context, index) {
final item = list[index];
final cat = categories[item.category] ??
RoomCategory.other(widget.room?.serverTag ?? "",
widget.room?.id ?? "", context);
return ShoppingListItem(
name: item.name,
server: widget.room!.serverTag,
room: widget.room!.id,
description: item.description,
category: cat,
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,
category: cat,
info: widget.info,
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];
final cat = categories[item.category] ??
RoomCategory.other(widget.room!.serverTag,
widget.room!.id, context);
return ShoppingListItem(
server: widget.room!.serverTag,
room: widget.room!.id,
name: item.name,
description: item.description,
category: cat,
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: () {
// 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,
category: cat,
item: item,
info: widget.info,
room: widget.room!.id,
server: widget.room!.serverTag));
});
},
)
])))),
floatingActionButton: (widget.info != null &&
2023-12-22 20:14:36 +01:00
((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) ||
((widget.info?.permissions)! &
RoomPermission.addShoppingListItems !=
0)))
? FloatingActionButton.extended(
icon: const Icon(Icons.add),
label: Text(AppLocalizations.of(context)!.newItemShort),
tooltip: AppLocalizations.of(context)!.newItemLong,
onPressed: () {
// show new category popup
context.pushNamed('new-item', params: {
'server': widget.room!.serverTag,
'id': widget.room!.id,
});
},
)
: null,
);
}
}
class ShoppingListItem extends StatelessWidget {
final String name;
final RoomCategory category;
final String description;
final bool inCart;
final Key _key;
final Function()? onDismiss;
final Function()? onTap;
final String server;
final String room;
const ShoppingListItem(
2023-12-22 20:14:36 +01:00
{required this.name,
required this.category,
required this.inCart,
required this.description,
required this.server,
required this.room,
required key,
this.onDismiss,
this.onTap})
2023-12-22 20:14:36 +01:00
: _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:
2023-12-22 20:14:36 +01:00
Icon(!inCart ? Icons.shopping_cart : Icons.remove_shopping_cart),
child: Opacity(
2023-12-22 20:14:36 +01:00
opacity: inCart ? 0.5 : 1.0,
child: ListTile(
title: Text(name),
subtitle: Text(description),
trailing: CategoryChip(
server: server,
room: room,
2023-12-22 20:14:36 +01:00
category: category,
),
onTap: () {
if (onTap != null) {
onTap!();
}
},
)),
);
}
}
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(
2023-12-22 20:14:36 +01:00
{super.key,
this.info,
required this.item,
required this.server,
required this.room,
required this.category,
required this.products});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return BottomSheet(
onClosing: () {},
builder: (context) => Column(
children: [
Padding(
2023-12-22 20:14:36 +01:00
padding: const EdgeInsets.all(14),
child: Center(
child: Column(children: [
Text(item.name, style: textTheme.headlineLarge),
Text(item.description, style: textTheme.titleMedium),
CategoryChip(
server: server,
room: room,
2023-12-22 20:14:36 +01:00
category: category,
),
Text(Unit.fromId(item.unit).display(context, item.value))
]))),
...(item.link != null)
2023-12-22 20:14:36 +01:00
? [
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.link.toString()
});
},
)
]
: [],
...(info != null &&
2023-12-22 20:14:36 +01:00
((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-item', params: {
2023-12-22 20:14:36 +01:00
'server': server,
'id': room,
'item': item.id.toString()
});
final navInner = Navigator.of(context);
navInner.pop();
2023-12-22 20:14:36 +01:00
},
),
ListTile(
title:
Text(AppLocalizations.of(context)!.deleteItemTitle),
subtitle: Text(
AppLocalizations.of(context)!.deleteItemSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// show popup
showDialog(
context: context,
builder: (ctx) => AlertDialog(
icon: const Icon(Icons.delete),
title: Text(
AppLocalizations.of(context)!.deleteItem),
content: Text(AppLocalizations.of(context)!
.deleteItemConfirm(item.name)),
actions: [
TextButton(
onPressed: () {
// close popup
Navigator.of(ctx).pop();
},
child: Text(
AppLocalizations.of(context)!.cancel),
),
FilledButton(
onPressed: () async {
// send request
final scaffMgr =
ScaffoldMessenger.of(ctx);
// popup context
final navInner = Navigator.of(ctx);
// bottomsheet context
final nav = Navigator.of(context);
final user = context.read<User>();
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
path: 'deleteItem',
target: user.server,
body: {
'room': room,
'server': server,
'listItemID': item.id
},
credentials: user),
onOK: (_) async {
// remove cached item
await item.removeDisk();
},
after: () {
// close popup
navInner.pop();
// close modal bottom sheet
nav.pop();
});
},
child: Text(AppLocalizations.of(context)!
.deleteItem),
)
],
));
2023-12-22 20:14:36 +01:00
}),
]
: [],
ListTile(
2023-12-22 20:14:36 +01:00
title: Text(item.state == 0
? AppLocalizations.of(context)!.moveItemToCartTitle
: AppLocalizations.of(context)!.removeItemFromCartTitle),
2023-12-22 20:14:36 +01:00
subtitle: Text(item.state == 0
? AppLocalizations.of(context)!.moveItemToCartSubtitle
2023-12-22 20:14:36 +01:00
: 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
}),
onOK: (_) async {
final navInner = Navigator.of(context);
await item.toDisk();
navInner.pop();
});
2023-12-22 20:14:36 +01:00
})
],
),
);
}
}