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/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 { final RoomInfo? info; final Room? room; const ShoppingListPage(this.room, this.info, {super.key}); @override State createState() => _ShoppingListPageState(); } class _ShoppingListPageState extends State { List list = []; List cart = []; Map weights = {}; List categories = []; List products = []; void fetchItems() { final user = context.read(); // TODO: load cached items first 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((raw) => RoomItem.fromJSON(raw)) .toList(); final List l = []; final List c = []; for (RoomItem item in resp) { if (item.state == 0) { l.add(item); } else { c.add(item); } } // TODO: cache items if (mounted) { setState(() { list = l; cart = c; sortAll(); }); } }); } void sortAll() { for (List 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(); // 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((raw) => RoomCategory.fromJSON(raw)) .toList(); Map map = {}; for (int i = 0; i < resp.length; i++) { map[resp[i].id] = i; } if (mounted) { setState(() { weights = map; categories = resp; sortAll(); }); } }); } 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?.id, 'server': widget.room?.serverTag}), onOK: (body) async { final resp = body['data'] .map((raw) => RoomProduct.fromJSON(raw)) .toList(); if (mounted) { setState(() { products = resp; }); } }); } @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { fetchItems(); fetchCategories(); fetchProducts(); }); } void changeItemState(RoomItem item) { final user = context.read(); 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]; RoomCategory cat = RoomCategory.other(context); try { cat = categories[item.category!]; }catch(_){} return ShoppingListItem( name: item.name, 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, 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 != null && ((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 { 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: CategoryChip( category: category, ), onTap: () { if (onTap != null) { onTap!(); } }, ), ); } } class ShoppingListItemInfo extends StatelessWidget { RoomItem item; String server; String room; List categories = []; List 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, help: AppLocalizations.of(context)!.selectLinkedProductHelp, 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() }); }, ) ] : [] ], ), ); } }