diff --git a/lib/business_logic/item_action.dart b/lib/business_logic/item_action.dart index 5c689d9..239441f 100644 --- a/lib/business_logic/item_action.dart +++ b/lib/business_logic/item_action.dart @@ -263,7 +263,7 @@ class PrintAction extends ItemAction { handle(BuildContext context, InventoryItem item) { showDialog( context: context, - builder: (context) => PrintDialog(item: item), + builder: (context) => PrintDialog(items: [item]), ); } diff --git a/lib/main.dart b/lib/main.dart index c5c151c..b5e1396 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -58,9 +58,9 @@ class _MyAppState extends State { stream: _authStream, builder: (context, snapshot) { final user = snapshot.data; - + var userEmailValid = user?.email?.endsWith('vehikl.com') ?? false; - + if (!userEmailValid) { return SignInPage( error: user == null @@ -68,15 +68,16 @@ class _MyAppState extends State { : 'Please log in with your Vehikl email', ); } - + return FutureBuilder( future: user!.getIdTokenResult(true), builder: (context, snap) { if (!snap.hasData) return Scaffold(body: Container()); - + final isAdmin = snap.data?.claims?['role'] == 'admin'; return Provider( - create: (context) => isAdmin ? UserRole.admin : UserRole.user, + create: (context) => + isAdmin ? UserRole.admin : UserRole.user, child: HomePage(), ); }, diff --git a/lib/pages/home_page/inventory_view/filters/search_field.dart b/lib/pages/home_page/inventory_view/filters/search_field.dart index 8309452..36fb257 100644 --- a/lib/pages/home_page/inventory_view/filters/search_field.dart +++ b/lib/pages/home_page/inventory_view/filters/search_field.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; class SearchField extends StatelessWidget { - final void Function(String) onChanged; + final void Function(String)? onChanged; final TextEditingController searchFieldController; const SearchField({ @@ -24,12 +24,13 @@ class SearchField extends StatelessWidget { icon: Icon( searchFieldController.text.isEmpty ? Icons.search : Icons.clear), onPressed: () { - onChanged(''); + onChanged?.call(''); searchFieldController.clear(); }, ), ), onChanged: onChanged, + enabled: onChanged != null, ); } } diff --git a/lib/pages/home_page/inventory_view/inventory_view.dart b/lib/pages/home_page/inventory_view/inventory_view.dart index fe42086..55baf54 100644 --- a/lib/pages/home_page/inventory_view/inventory_view.dart +++ b/lib/pages/home_page/inventory_view/inventory_view.dart @@ -4,12 +4,11 @@ import 'package:spare_parts/entities/inventory_item.dart'; import 'package:spare_parts/pages/home_page/inventory_view/filters/available_items_filter.dart'; import 'package:spare_parts/pages/home_page/inventory_view/filters/search_field.dart'; import 'package:spare_parts/pages/home_page/inventory_view/filters/user_filter.dart'; +import 'package:spare_parts/pages/home_page/inventory_view/inventory_view_list.dart'; import 'package:spare_parts/services/repositories/repositories.dart'; import 'package:spare_parts/utilities/constants.dart'; -import 'package:spare_parts/widgets/empty_list_state.dart'; import 'package:spare_parts/widgets/error_container.dart'; import 'package:spare_parts/widgets/inputs/multiselect_button.dart'; -import 'package:spare_parts/widgets/inventory_list_item.dart'; import 'package:spare_parts/widgets/inventory_list_item_loading.dart'; class InventoryView extends StatefulWidget { @@ -25,6 +24,7 @@ class _InventoryViewState extends State { late bool _showOnlyAvailableItems; String _searchQuery = ''; final searchFieldController = TextEditingController(); + bool _inSelectionMode = false; bool get isAdmin => context.read() == UserRole.admin; @@ -47,6 +47,10 @@ class _InventoryViewState extends State { setState(() => _showOnlyAvailableItems = !_showOnlyAvailableItems); } + void _handleSelectionModeChanged(bool inSelectionMode) { + setState(() => _inSelectionMode = inSelectionMode); + } + @override Widget build(BuildContext context) { final inventoryItemRepository = context.read(); @@ -61,44 +65,48 @@ class _InventoryViewState extends State { padding: const EdgeInsets.all(8.0), child: SearchField( searchFieldController: searchFieldController, - onChanged: (value) => setState(() { - _searchQuery = value; - }), + onChanged: _inSelectionMode + ? null + : (value) => setState(() { + _searchQuery = value; + }), ), ), ), ], ), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - MultiselectButton( - buttonLabel: 'Item Types', - values: itemTypes.keys.toList(), - selectedValues: _selectedItemTypes, - icon: Icons.filter_list, - leadingBuilder: (itemType) => - Icon(itemTypes[itemType] ?? itemTypes['Other']!), - onConfirm: _handleTypesFilterChanged, - ), - if (isAdmin) ...[ - SizedBox(width: 10), - UserFilter( + if (!_inSelectionMode) ...[ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + MultiselectButton( + buttonLabel: 'Item Types', + values: itemTypes.keys.toList(), + selectedValues: _selectedItemTypes, icon: Icons.filter_list, - selectedUsers: _selectedBorrowers, - onChanged: _handleBorrowersFilterChanged, - ), - SizedBox(width: 10), - AvailableItemsFilter( - value: _showOnlyAvailableItems, - onPressed: _handleAvailableItemsFilterChanged, + leadingBuilder: (itemType) => + Icon(itemTypes[itemType] ?? itemTypes['Other']!), + onConfirm: _handleTypesFilterChanged, ), + if (isAdmin) ...[ + SizedBox(width: 10), + UserFilter( + icon: Icons.filter_list, + selectedUsers: _selectedBorrowers, + onChanged: _handleBorrowersFilterChanged, + ), + SizedBox(width: 10), + AvailableItemsFilter( + value: _showOnlyAvailableItems, + onPressed: _handleAvailableItemsFilterChanged, + ), + ], ], - ], + ), ), - ), - Divider(), + Divider(), + ], Expanded( child: StreamBuilder>( stream: inventoryItemRepository.getItemsStream( @@ -128,27 +136,10 @@ class _InventoryViewState extends State { final items = snapshot.data!; - if (items.isEmpty) { - return EmptyListState( - message: "No inventory items to display...", - ); - } - - final filteredItems = items.where((item) { - List properties = [item.name, item.borrower?.name]; - return properties.where((property) => property != null).any( - (property) => property! - .toLowerCase() - .contains(_searchQuery.toLowerCase())); - }).toList(); - - filteredItems.sort(); - - return ListView( - children: filteredItems - .map((item) => - InventoryListItem(item: item, showBorrower: true)) - .toList(), + return InventoryViewList( + items: items, + searchQuery: _searchQuery, + onSelectionModeChanged: _handleSelectionModeChanged, ); }, ), diff --git a/lib/pages/home_page/inventory_view/inventory_view_list.dart b/lib/pages/home_page/inventory_view/inventory_view_list.dart new file mode 100644 index 0000000..f34b247 --- /dev/null +++ b/lib/pages/home_page/inventory_view/inventory_view_list.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:spare_parts/entities/inventory_item.dart'; +import 'package:spare_parts/pages/home_page/inventory_view/selection_actions.dart'; +import 'package:spare_parts/widgets/dialogs/print_dialog/print_dialog_mobile.dart' + if (dart.library.html) 'package:spare_parts/widgets/dialogs/print_dialog/print_dialog_web.dart'; +import 'package:spare_parts/widgets/empty_list_state.dart'; +import 'package:spare_parts/widgets/inventory_list_item.dart'; + +class InventoryViewList extends StatefulWidget { + final List items; + final String? searchQuery; + final void Function(bool)? onSelectionModeChanged; + + const InventoryViewList({ + super.key, + required this.items, + this.searchQuery, + this.onSelectionModeChanged, + }); + + @override + State createState() => _InventoryViewListState(); +} + +class _InventoryViewListState extends State { + final List _selectedItemIds = []; + bool get _inSelectionMode => _selectedItemIds.isNotEmpty; + + void _handleSelectItem(String itemId) { + final previousListEmpty = _selectedItemIds.isEmpty; + setState(() { + if (_selectedItemIds.contains(itemId)) { + _selectedItemIds.remove(itemId); + } else { + _selectedItemIds.add(itemId); + } + }); + if (_selectedItemIds.isEmpty != previousListEmpty) { + widget.onSelectionModeChanged?.call(_inSelectionMode); + } + } + + void _handleSelectAll() { + setState(() { + _selectedItemIds.clear(); + _selectedItemIds.addAll(widget.items.map((item) => item.id)); + }); + } + + void _handleDeselectAll() { + setState(() { + _selectedItemIds.clear(); + }); + widget.onSelectionModeChanged?.call(false); + } + + void _handlePrintAll() { + final selectedItems = widget.items + .where((item) => _selectedItemIds.contains(item.id)) + .toList(); + showDialog( + context: context, + builder: (_) => PrintDialog(items: selectedItems), + ); + } + + @override + Widget build(BuildContext context) { + if (widget.items.isEmpty) { + return EmptyListState( + message: "No inventory items to display...", + ); + } + + final filteredItems = widget.items.where((item) { + if (widget.searchQuery == null) { + return true; + } + + List properties = [item.name, item.borrower?.name]; + return properties.where((property) => property != null).any((property) => + property!.toLowerCase().contains(widget.searchQuery!.toLowerCase())); + }).toList(); + + filteredItems.sort(); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_inSelectionMode) ...[ + SelectionActions( + onSelectAll: _handleSelectAll, + onDeselectAll: _handleDeselectAll, + onPrintAll: _handlePrintAll, + ), + Divider(), + ], + ListView( + shrinkWrap: true, + children: filteredItems + .map((item) => InventoryListItem( + item: item, + showBorrower: true, + selectable: _inSelectionMode, + selected: _selectedItemIds.contains(item.id), + onSelected: _handleSelectItem, + )) + .toList(), + ), + ], + ); + } +} diff --git a/lib/pages/home_page/inventory_view/selection_actions.dart b/lib/pages/home_page/inventory_view/selection_actions.dart new file mode 100644 index 0000000..985a510 --- /dev/null +++ b/lib/pages/home_page/inventory_view/selection_actions.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class SelectionActions extends StatelessWidget { + final void Function() onSelectAll; + final void Function() onDeselectAll; + final void Function() onPrintAll; + + const SelectionActions({ + Key? key, + required this.onSelectAll, + required this.onDeselectAll, + required this.onPrintAll, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + TextButton(onPressed: onSelectAll, child: Text('Select All')), + SizedBox(width: 8), + TextButton(onPressed: onDeselectAll, child: Text('Deselect All')), + Spacer(), + ElevatedButton(onPressed: onPrintAll, child: Text('Print Labels')), + ], + ); + } +} diff --git a/lib/widgets/dialogs/print_dialog/print_dialog_mobile.dart b/lib/widgets/dialogs/print_dialog/print_dialog_mobile.dart index 781c920..ff84f19 100644 --- a/lib/widgets/dialogs/print_dialog/print_dialog_mobile.dart +++ b/lib/widgets/dialogs/print_dialog/print_dialog_mobile.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:spare_parts/entities/inventory_item.dart'; class PrintDialog extends StatelessWidget { - final InventoryItem item; - - const PrintDialog({super.key, required this.item}); + final List items; + + const PrintDialog({super.key, required this.items}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/dialogs/print_dialog/print_dialog_web.dart b/lib/widgets/dialogs/print_dialog/print_dialog_web.dart index 3c55f98..2eaa65d 100644 --- a/lib/widgets/dialogs/print_dialog/print_dialog_web.dart +++ b/lib/widgets/dialogs/print_dialog/print_dialog_web.dart @@ -3,13 +3,15 @@ library print_dialog_web; import 'package:flutter/material.dart'; import 'package:spare_parts/entities/inventory_item.dart'; import 'package:spare_parts/services/print_service.dart'; +import 'package:spare_parts/widgets/buttons/cancel_button.dart'; import 'package:spare_parts/widgets/dialogs/print_dialog/print_dialog_body.dart'; +import 'package:spare_parts/widgets/title_text.dart'; import '../../../services/dymo_service.dart'; class PrintDialog extends StatefulWidget { - final InventoryItem item; - const PrintDialog({super.key, required this.item}); + final List items; + const PrintDialog({super.key, required this.items}); @override State createState() => _PrintDialogState(); @@ -25,40 +27,79 @@ class _PrintDialogState extends State { initialization = initAsync(); } + void _handlePrintItems() { + for (final item in widget.items) { + PrintService.printQRCode(selectedPrinter!.name, item); + } + } + @override Widget build(BuildContext context) { return AlertDialog( - title: const Text('Print Label'), - content: FutureBuilder( - future: initialization, - builder: (context, snapshot) { - return snapshot.connectionState == ConnectionState.waiting - ? Column( - mainAxisSize: MainAxisSize.min, - children: [ - CircularProgressIndicator(), - ], - ) - : PrintDialogBody( + title: Text('Print Label${widget.items.length > 1 ? 's' : ''}'), + content: SizedBox( + width: 400, + child: FutureBuilder( + future: initialization, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(), + ], + ); + } + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.items.length > 1) + TitleText('Printing labels for ${widget.items.length} items'), + ConstrainedBox( + constraints: BoxConstraints(maxHeight: 225), + child: GridView.count( + crossAxisCount: 2, + mainAxisSpacing: 20, + crossAxisSpacing: 20, + children: widget.items + .map((item) => Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: + BorderRadius.all(Radius.circular(20)), + border: Border.all(color: Colors.white), + ), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Property of Vehikl Inc.'), + Icon(Icons.qr_code_2, size: 128), + Text(item.nameForPrinting), + ], + ), + ), + )) + .toList(), + ), + ), + PrintDialogBody( selectedPrinter: selectedPrinter, onPrinterSelected: (printer) => setState(() => selectedPrinter = selectedPrinter == printer ? null : printer), - ); - }, + ), + ], + ); + }, + ), ), actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), - ), + CancelButton(), ElevatedButton( - onPressed: selectedPrinter == null - ? null - : () => PrintService.printQRCode( - selectedPrinter!.name, - widget.item, - ), + onPressed: selectedPrinter == null ? null : _handlePrintItems, child: const Text('Print'), ), ], diff --git a/lib/widgets/error_container.dart b/lib/widgets/error_container.dart index 45a8913..d76143d 100644 --- a/lib/widgets/error_container.dart +++ b/lib/widgets/error_container.dart @@ -12,7 +12,7 @@ class ErrorContainer extends StatelessWidget { color: Theme.of(context).colorScheme.error, ), padding: const EdgeInsets.all(16), - child: Text(error), + child: SelectableText(error), ); } } diff --git a/lib/widgets/inventory_list_item.dart b/lib/widgets/inventory_list_item.dart index 806531a..0d74c6c 100644 --- a/lib/widgets/inventory_list_item.dart +++ b/lib/widgets/inventory_list_item.dart @@ -11,13 +11,26 @@ import 'package:spare_parts/widgets/item_icon.dart'; class InventoryListItem extends StatelessWidget { final bool showBorrower; final InventoryItem item; + final bool selectable; + final bool selected; + final void Function(String itemId)? onSelected; const InventoryListItem({ Key? key, required this.item, this.showBorrower = false, + this.selectable = false, + this.selected = false, + this.onSelected, }) : super(key: key); + void _handleLongPress(BuildContext context, InventoryItem item) { + final isAdmin = context.read() == UserRole.admin; + if (!isAdmin) return; + + onSelected?.call(item.id); + } + @override Widget build(BuildContext context) { final userRole = context.read(); @@ -39,12 +52,25 @@ class InventoryListItem extends StatelessWidget { return Provider.value( value: userRole, child: ListTile( - leading: ItemIcon(item: item), + onLongPress: () => _handleLongPress(context, item), + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (selectable) ...[ + if (selected) + Icon(Icons.check_box) + else + Icon(Icons.check_box_outline_blank), + SizedBox(width: 8), + ], + ItemIcon(item: item), + ], + ), title: Text(item.name), subtitle: !showBorrower || item.borrower?.name == null ? null : Text(item.borrower!.name!), - onTap: openContainer, + onTap: selectable ? () => onSelected!(item.id) : openContainer, trailing: ItemActionsButton(item: item), ), ); diff --git a/pubspec.lock b/pubspec.lock index 20defa4..4dfa455 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "64.0.0" _flutterfire_internals: dependency: transitive description: name: _flutterfire_internals - sha256: "1a5e13736d59235ce0139621b4bbe29bc89839e202409081bc667eb3cd20674c" + sha256: eb0ac20f704799b986049fbb3c1c16421eca319a1b872378d669513e12452ba5 url: "https://pub.dev" source: hosted - version: "1.3.5" + version: "1.3.14" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.2.0" animations: dependency: "direct main" description: @@ -93,34 +93,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: d912852cce27c9e80a93603db721c267716894462e7033165178b91138587972 + sha256: "64e12b0521812d1684b1917bc80945625391cb9bdd4312536b1d69dcb6133ed8" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" build_runner: dependency: "direct main" description: name: build_runner - sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.7" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "7.2.11" built_collection: dependency: transitive description: @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: built_value - sha256: ff627b645b28fb8bdb69e645f910c2458fd6b65f6585c3a53e0626024897dedf + sha256: "69acb7007eb2a31dc901512bfe0f7b767168be34cb734835d54c070bfa74c1b2" url: "https://pub.dev" source: hosted - version: "8.6.2" + version: "8.8.0" cel: dependency: transitive description: @@ -173,58 +173,58 @@ packages: dependency: "direct main" description: name: cloud_firestore - sha256: "0ff0baec167e308df192398dbd81ec13c1799635885c6aa6ed9ab8b5ed61f52c" + sha256: "8fa76be13bf33c7e1521794d8f43e94abb83515201667d00a1e0063351867200" url: "https://pub.dev" source: hosted - version: "4.9.1" + version: "4.13.3" cloud_firestore_platform_interface: dependency: transitive description: name: cloud_firestore_platform_interface - sha256: "5749b81aea93afdce220e02d34369162010d210011054ac494b2c38c4e9ebeb7" + sha256: "5683de3ed02dded4857882493a7f359dff8cb9941ef36fa707f1b1cbde054838" url: "https://pub.dev" source: hosted - version: "5.16.0" + version: "6.0.7" cloud_firestore_web: dependency: transitive description: name: cloud_firestore_web - sha256: fef99ad0599e983092adb1bb01f14a596dba601a7a8efaaffd7b2721d64e2c51 + sha256: "29ed06af42c224e2ada69344b6d42e3d4c5e37f3931ea790733e859c0e5bfbf8" url: "https://pub.dev" source: hosted - version: "3.7.0" + version: "3.8.7" cloud_functions: dependency: "direct main" description: name: cloud_functions - sha256: "6c5ce6c78bcf92b422b58e76dc33cd46b16cf421fc4d439fc77b7d44a74dc80d" + sha256: "06760abdb6ec9291c61347b401ab58ab376b9945e038c7ddb223ba6135791cb2" url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.5.6" cloud_functions_platform_interface: dependency: transitive description: name: cloud_functions_platform_interface - sha256: "2d6bf40c9b9db65f89e97f0e74833794191df4398f7e92a6b1dd3201ec70cfb7" + sha256: c193ec0d47e19a5b7650c5e3029cab92a3229644c04cdafdf291dc37b8e81cc0 url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.5.9" cloud_functions_web: dependency: transitive description: name: cloud_functions_web - sha256: "3937c217f90a51e251c37fc15a4c1be563b50d1f4268284f3ebf83ed70ba1ff8" + sha256: a4997cb7243a110d1cada99d61868b479e0b7f6d998993e4876c14cf3f8110b5 url: "https://pub.dev" source: hosted - version: "4.6.0" + version: "4.6.9" code_builder: dependency: transitive description: name: code_builder - sha256: "315a598c7fbe77f22de1c9da7cfd6fd21816312f16ffa124453b4fc679e540f1" + sha256: b2151ce26a06171005b379ecff6e08d34c470180ffe16b8e14b6d52be292b55f url: "https://pub.dev" source: hosted - version: "4.6.0" + version: "4.8.0" collection: dependency: transitive description: @@ -253,18 +253,18 @@ packages: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.4" dynamic_color: dependency: "direct main" description: name: dynamic_color - sha256: "96bff3df72e3d428bda2b874c7a521e8c86f592cae626ea594922fcc8d166e0c" + sha256: "8b8bd1d798bd393e11eddeaa8ae95b12ff028bf7d5998fc5d003488cd5f4ce2f" url: "https://pub.dev" source: hosted - version: "1.6.7" + version: "1.6.8" equatable: dependency: transitive description: @@ -285,18 +285,18 @@ packages: dependency: "direct main" description: name: fake_cloud_firestore - sha256: "48e4dc3f4ed7448c7f7f9c7697e78b835d5954029f8edb1bc1afa4bb518f5f17" + sha256: "91c484048400465a85bf2cd476f8354dbcb8ecfe6f901fc525b829d55caa7f69" url: "https://pub.dev" source: hosted - version: "2.4.1+1" + version: "2.4.4" fake_firebase_security_rules: dependency: transitive description: name: fake_firebase_security_rules - sha256: d35f2eb7850e752397e886c803425d71df78820a870f0849a465e22bfd26119b + sha256: "7a9011d42d99848ece92784fed5a20167ad76da66de2c1102a2bc053e66b0305" url: "https://pub.dev" source: hosted - version: "0.5.2" + version: "0.5.3" file: dependency: transitive description: @@ -309,50 +309,50 @@ packages: dependency: "direct main" description: name: firebase_auth - sha256: "6d9be853426ab686d68076b8007ac29b2c31e7d549444a45b5c3fe1abc249fb0" + sha256: "869ff488c7b467e273d7be223f52d3d026576b6e1da92dcd136ff627ae0a8c67" url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.15.0" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface - sha256: "2946cfdc17f925fa9771dd0ba3ce9dd2d019100a8685d0557c161f7786ea9b14" + sha256: ecf9f78ae1a7a1297de01ec975e9e2cfe5b543589b27cc5969849d9a8dc46999 url: "https://pub.dev" source: hosted - version: "6.18.0" + version: "7.0.6" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: d8972d754702a3f4881184706b8056e2837d0dae91613a43b988c960b8e0d988 + sha256: "96f89e2340cdf373109cb29afec401c170aa2d98fb0833687793c8017e36f435" url: "https://pub.dev" source: hosted - version: "5.8.0" + version: "5.8.9" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: c78132175edda4bc532a71e01a32964e4b4fcf53de7853a422d96dac3725f389 + sha256: d301561d614487688d797717bef013a264c517d1d09e4c5c1325c3a64c835efb url: "https://pub.dev" source: hosted - version: "2.15.1" + version: "2.24.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2 + sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "5.0.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "4cf4d2161530332ddc3c562f19823fb897ff37a9a774090d28df99f47370e973" + sha256: "10159d9ee42c79f4548971d92f3f0fcd5791f6738cda3583a4e3b2c8b244c018" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.9.0" fixnum: dependency: transitive description: @@ -370,10 +370,10 @@ packages: dependency: "direct main" description: name: flutter_animate - sha256: "62f346340a96192070e31e3f2a1bd30a28530f1fe8be978821e06cd56b74d6d2" + sha256: "1dbc1aabfb8ec1e9d9feed2b675c21fb6b0a11f99be53ec3bc0f1901af6a8eb7" url: "https://pub.dev" source: hosted - version: "4.2.0+1" + version: "4.3.0" flutter_lints: dependency: "direct dev" description: @@ -420,10 +420,10 @@ packages: dependency: transitive description: name: google_sign_in_android - sha256: "8d76099cb220d4f10c7e3c24492814c733f48ecb574c45c0ccadf5d5e50b012d" + sha256: "6031f59074a337fdd81be821aba84cee3a41338c6e958499a5cd34d3e1db80ef" url: "https://pub.dev" source: hosted - version: "6.1.19" + version: "6.1.20" google_sign_in_ios: dependency: transitive description: @@ -516,10 +516,10 @@ packages: dependency: transitive description: name: logger - sha256: "7ad7215c15420a102ec687bb320a7312afd449bac63bfb1c60d9787c27b9767f" + sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "2.0.2+1" logging: dependency: transitive description: @@ -580,18 +580,18 @@ packages: dependency: "direct main" description: name: mockito - sha256: "7d5b53bcd556c1bc7ffbe4e4d5a19c3e112b7e925e9e172dd7c6ad0630812616" + sha256: "4b693867cee1853c9d1d7ecc1871f27f39b2ef2c13c0d8d8507dfe5bebd8aaf1" url: "https://pub.dev" source: hosted - version: "5.4.2" + version: "5.4.3" more: dependency: transitive description: name: more - sha256: ad9207a4abefaff7099e8da509ba2adab4cfa4203c2ebbfc35070f88e18b7c82 + sha256: a06e9e78bd1446612c9fbb033612176f252bcb9ab905a778202d9f8212000c5a url: "https://pub.dev" source: hosted - version: "3.10.2" + version: "4.0.1" nested: dependency: transitive description: @@ -620,10 +620,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.7" pool: dependency: transitive description: @@ -636,10 +636,10 @@ packages: dependency: "direct main" description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.1" pub_semver: dependency: transitive description: @@ -668,10 +668,10 @@ packages: dependency: transitive description: name: rx - sha256: efe1fb4170741401370e365a76602a631b584097d6f44d40c40cbc185b65644d + sha256: "3dbb26d92cf356fe2346abf636dd54eb611dfa99428ed989ea6ba268c7fa80ae" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.3.0" rxdart: dependency: transitive description: diff --git a/test/pages/home_page/inventory_view/inventory_view_list_test.dart b/test/pages/home_page/inventory_view/inventory_view_list_test.dart new file mode 100644 index 0000000..7f9eeda --- /dev/null +++ b/test/pages/home_page/inventory_view/inventory_view_list_test.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spare_parts/entities/inventory_item.dart'; +import 'package:spare_parts/pages/home_page/inventory_view/inventory_view_list.dart'; +import 'package:spare_parts/pages/home_page/inventory_view/selection_actions.dart'; +import 'package:spare_parts/utilities/constants.dart'; +import 'package:spare_parts/widgets/dialogs/print_dialog/print_dialog_mobile.dart'; + +import '../../../helpers/test_helpers.dart'; + +void main() { + final chairItem = InventoryItem( + id: 'Chair#123', + name: 'The Great Chair', + type: 'Chair', + ); + final deskItem = InventoryItem( + id: 'Desk#145', + name: "The Great Desk", + type: 'Desk', + ); + + group('when selecting items', () { + testWidgets('as a user can not select an item', (tester) async { + await pumpPage( + Scaffold(body: InventoryViewList(items: [chairItem, deskItem])), + tester, + userRole: UserRole.user, + ); + + final deskListTile = find.ancestor( + of: find.text(deskItem.name), + matching: find.byType(ListTile), + ); + + await tester.longPress(deskListTile); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.check_box), findsNothing); + expect(find.byType(SelectionActions), findsNothing); + }); + + testWidgets('can select an item', (tester) async { + await pumpPage( + Scaffold(body: InventoryViewList(items: [chairItem, deskItem])), + tester, + userRole: UserRole.admin, + ); + + final deskListTile = find.ancestor( + of: find.text(deskItem.name), + matching: find.byType(ListTile), + ); + + await tester.longPress(deskListTile); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.check_box), findsOneWidget); + expect(find.byIcon(Icons.check_box_outline_blank), findsOneWidget); + }); + + testWidgets('can select all items', (tester) async { + await pumpPage( + Scaffold(body: InventoryViewList(items: [chairItem, deskItem])), + tester, + userRole: UserRole.admin, + ); + + final deskListTile = find.ancestor( + of: find.text(deskItem.name), + matching: find.byType(ListTile), + ); + + await tester.longPress(deskListTile); + await tester.pumpAndSettle(); + + var selectAllButton = find.text('Select All'); + await tester.tap(selectAllButton); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.check_box), findsNWidgets(2)); + expect(find.byIcon(Icons.check_box_outline_blank), findsNothing); + }); + + testWidgets('can deselect all items', (tester) async { + await pumpPage( + Scaffold(body: InventoryViewList(items: [chairItem, deskItem])), + tester, + userRole: UserRole.admin, + ); + + final deskListTile = find.ancestor( + of: find.text(deskItem.name), + matching: find.byType(ListTile), + ); + + await tester.longPress(deskListTile); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.check_box), findsOneWidget); + + var selectAllButton = find.text('Deselect All'); + await tester.tap(selectAllButton); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.check_box), findsNothing); + expect(find.byIcon(Icons.check_box_outline_blank), findsNothing); + }); + + testWidgets('shows a printing dialog for the selected items', (tester) async { + await pumpPage( + Scaffold(body: InventoryViewList(items: [chairItem, deskItem])), + tester, + userRole: UserRole.admin, + ); + + final deskListTile = find.ancestor( + of: find.text(deskItem.name), + matching: find.byType(ListTile), + ); + + await tester.longPress(deskListTile); + await tester.pumpAndSettle(); + + final chairListTile = find.ancestor( + of: find.text(chairItem.name), + matching: find.byType(ListTile), + ); + + await tester.press(chairListTile); + await tester.pumpAndSettle(); + + var printAllButton = find.text('Print Labels'); + await tester.tap(printAllButton); + await tester.pumpAndSettle(); + + // TODO: assert items in the print dialog + expect(find.byType(PrintDialog), findsOneWidget); + }); + }); +} diff --git a/test/pages/home_page/inventory_view/inventory_view_test.dart b/test/pages/home_page/inventory_view/inventory_view_test.dart index bea851f..f3a3364 100644 --- a/test/pages/home_page/inventory_view/inventory_view_test.dart +++ b/test/pages/home_page/inventory_view/inventory_view_test.dart @@ -976,4 +976,33 @@ void main() { expect(listItems, findsOneWidget); }); }); + + group('Selecting items', () { + testWidgets('disables search', (WidgetTester tester) async { + final deskItem = InventoryItem(id: 'Desk#145', type: 'Desk'); + await firestore + .collection('items') + .doc(deskItem.id) + .set(deskItem.toFirestore()); + + await pumpPage( + Scaffold(body: InventoryView()), + tester, + userRole: UserRole.admin, + firestore: firestore, + ); + + final searchField = find.byKey(Key('search')); + + final deskListTile = find.ancestor( + of: find.text(deskItem.name), + matching: find.byType(ListTile), + ); + + await tester.longPress(deskListTile); + await tester.pumpAndSettle(); + + expect(tester.widget(searchField).enabled, isFalse); + }); + }); } diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 29d0b2a..269767b 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,18 @@ #include "generated_plugin_registrant.h" +#include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + CloudFirestorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); DynamicColorPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); + FirebaseAuthPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 906607c..03dfff6 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,9 @@ # list(APPEND FLUTTER_PLUGIN_LIST + cloud_firestore dynamic_color + firebase_auth firebase_core )