diff --git a/lib/extensions/widget_extensions.dart b/lib/extensions/widget_extensions.dart index 0586f01a..9cce960f 100644 --- a/lib/extensions/widget_extensions.dart +++ b/lib/extensions/widget_extensions.dart @@ -81,18 +81,20 @@ extension WidgetExtensions on Widget { ).toShadowContainer(context) : this; - Widget toShadowContainer(BuildContext context, {bool showShadow = true,double borderRadius = 14, bool withShadow = true, Color? backgroundColor, double padding = 16, EdgeInsets? paddingObject}) => withShadow - ? Container( - padding: paddingObject ?? EdgeInsets.all(padding), - width: double.infinity, - decoration: ShapeDecoration( - color: backgroundColor ?? AppColor.background(context), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(borderRadius)), - shadows: showShadow ? [boxShadowR14] : null, - ), - child: this, - ) - : this; + Widget toShadowContainer(BuildContext context, + {bool showShadow = true, double borderRadius = 14, bool withShadow = true, Color? backgroundColor, Color borderColor = Colors.transparent, double padding = 16, EdgeInsets? paddingObject}) => + withShadow + ? Container( + padding: paddingObject ?? EdgeInsets.all(padding), + width: double.infinity, + decoration: ShapeDecoration( + color: backgroundColor ?? AppColor.background(context), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(borderRadius), side: BorderSide(color: borderColor)), + shadows: showShadow ? [boxShadowR14] : null, + ), + child: this, + ) + : this; Widget bottomSheetContainer(BuildContext context, {EdgeInsets? padding}) => Container( clipBehavior: Clip.antiAlias, diff --git a/lib/views/pages/device_transfer/search_asset_page.dart b/lib/views/pages/device_transfer/search_asset_page.dart index 4522992c..7b46abb9 100644 --- a/lib/views/pages/device_transfer/search_asset_page.dart +++ b/lib/views/pages/device_transfer/search_asset_page.dart @@ -5,6 +5,7 @@ import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart'; import '../../../models/device/asset.dart'; @@ -20,8 +21,9 @@ import '../../widgets/loaders/no_data_found.dart'; class SearchAssetPage extends StatefulWidget { /// add on route static const String id = "asset_search_page"; + final bool multiSelection; - const SearchAssetPage({Key? key}) : super(key: key); + const SearchAssetPage({Key? key, this.multiSelection = false}) : super(key: key); @override State createState() => _SearchAssetPageState(); @@ -36,6 +38,8 @@ class _SearchAssetPageState extends State { final GlobalKey _formKey = GlobalKey(); bool _isFirst = true; + List selectedAssets = []; + @override void initState() { _searchController = TextEditingController(); @@ -135,20 +139,67 @@ class _SearchAssetPageState extends State { }); } }, - child: ListView.separated( - itemCount: _searchableList.length, - separatorBuilder: (listContext, itemIndex) => 8.height, - padding: const EdgeInsets.all(16), - itemBuilder: (listContext, itemIndex) { - return AssetItemListView( - device: _searchableList[itemIndex], - onPressed: (device) { - Navigator.of(context).pop(); - Navigator.of(context).pop(device); + child: Column( + children: [ + ListView.separated( + itemCount: _searchableList.length, + separatorBuilder: (listContext, itemIndex) => 8.height, + padding: const EdgeInsets.all(16), + itemBuilder: (listContext, itemIndex) { + bool isSelected = selectedAssets.contains(_searchableList[itemIndex].id!); + String title = isSelected ? "UnSelect" : "Select"; + return AssetItemListView( + device: _searchableList[itemIndex], + isSelected: isSelected, + onPressed: (device) { + if (widget.multiSelection) { + if (selectedAssets.contains(device.id!)) { + selectedAssets.remove(device.id!); + } else { + selectedAssets.add(device.id!); + } + setState(() {}); + } else { + Navigator.of(context).pop(); + Navigator.of(context).pop(device); + } + }, + selectButton: Text(title, style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context))), + ); }, - selectButton: Text(context.translation.select, style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context))), - ); - }, + ).expanded, + if (widget.multiSelection && selectedAssets.isNotEmpty) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: AppFilledButton( + label: "Clear", + buttonColor: AppColor.white60, + textColor: AppColor.black10, + loading: false, + onPressed: () async { + selectedAssets.clear(); + setState(() {}); + }, + ), + ), + 12.width, + Expanded( + child: AppFilledButton( + label: context.translation.select, + buttonColor: AppColor.primary10, + loading: false, + onPressed: () async { + Navigator.of(context).pop(); + List assets = _searchableList.where((asset) => selectedAssets.contains(asset.id)).toList(); + Navigator.of(context).pop(assets); + }, + ), + ), + ], + ).toShadowContainer(context), + ], ), ), ) diff --git a/lib/views/pages/user/tasks_request/create_task_view.dart b/lib/views/pages/user/tasks_request/create_task_view.dart index 3672893d..b01f0af8 100644 --- a/lib/views/pages/user/tasks_request/create_task_view.dart +++ b/lib/views/pages/user/tasks_request/create_task_view.dart @@ -1,5 +1,5 @@ -import 'dart:convert'; import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/controllers/providers/api/service_requests_provider.dart'; @@ -29,6 +29,7 @@ import 'package:test_sa/views/widgets/bottom_sheets/pending_request_bottom_sheet import 'package:test_sa/views/widgets/date_and_time/date_picker.dart'; import 'package:test_sa/views/widgets/equipment/asset_picker.dart'; import 'package:test_sa/views/widgets/images/multi_image_picker.dart'; + import '../../../../../../new_views/common_widgets/default_app_bar.dart'; import '../../../../models/new_models/department.dart'; import '../../../../models/new_models/site.dart'; @@ -46,6 +47,8 @@ class _CreateTaskViewState extends State with TickerProviderStat final List _deviceImages = []; final GlobalKey _formKey = GlobalKey(); final GlobalKey _scaffoldKey = GlobalKey(); + + //TODO need to replace with model attribute Asset? _device; PendingAssetServiceRequest? pendingAssetServiceRequest; TaskType? selectedType; @@ -414,45 +417,26 @@ class _CreateTaskViewState extends State with TickerProviderStat Widget scanAssetButton() { return AssetPicker( - device: _device, + deviceList: _deviceList, showLoading: checkPendingRequest, borderColor: AppColor.black20, buttonColor: AppColor.white936, - onPick: (asset) async { - pendingAssetServiceRequest = null; - _device = asset; - _addTaskModel?.assetIds=[asset.id!.toInt()]; - await checkAssetForPendingServiceRequest(asset.id!.toInt()); - if (pendingAssetServiceRequest != null && pendingAssetServiceRequest!.details!.isNotEmpty) { - showPendingRequestBottomSheet(); - } + multiSelection: true, + onAssetRemove: (itemId) { + _deviceList.removeWhere((asset) => asset.id?.toInt() == itemId); + setState(() {}); + }, + onMultiAssetPick: (assetList) { + _deviceList.addAll(assetList); + final seenIds = {}; + _deviceList = _deviceList.where((item) => seenIds.add(item.id)).toList(); + setState(() {}); }, ); } bool checkPendingRequest = false; - void showPendingRequests() { - Navigator.of(context).push(MaterialPageRoute(builder: (_) => PendingServiceRequestScreen(pendingAssetServiceRequest!))); - } - - void showPendingRequestBottomSheet() async { - bool view = (await showModalBottomSheet( - context: context, - isDismissible: false, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - clipBehavior: Clip.antiAliasWithSaveLayer, - builder: (BuildContext context) => PendingRequestBottomSheet(pendingAssetServiceRequest!, _device ?? Asset()), - )) as bool; - if (view) { - showPendingRequests(); - } - } - Future checkAssetForPendingServiceRequest(int assetId) async { checkPendingRequest = true; setState(() {}); diff --git a/lib/views/widgets/equipment/asset_item_listview.dart b/lib/views/widgets/equipment/asset_item_listview.dart index 26ee3ab8..f2aab423 100644 --- a/lib/views/widgets/equipment/asset_item_listview.dart +++ b/lib/views/widgets/equipment/asset_item_listview.dart @@ -11,8 +11,9 @@ class AssetItemListView extends StatelessWidget { final Asset device; final Function(Asset) onPressed; final Widget? selectButton; + final bool isSelected; - const AssetItemListView({Key? key,required this.device,required this.onPressed, this.selectButton}) : super(key: key); + const AssetItemListView({Key? key, required this.device, required this.onPressed, this.selectButton, this.isSelected = false}) : super(key: key); @override Widget build(BuildContext context) { @@ -50,26 +51,27 @@ class AssetItemListView extends StatelessWidget { children: [ "${context.translation.serialNo} : ${device.assetSerialNo}".bodyText(context).expanded, 4.width, - selectButton ?? Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - context.translation.viewDetails, - style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context)), + selectButton ?? + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.translation.viewDetails, + style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context)), + ), + 4.width, + Icon( + Icons.arrow_forward, + color: AppColor.blueStatus(context), + size: 14, + ) + ], ), - 4.width, - Icon( - Icons.arrow_forward, - color: AppColor.blueStatus(context), - size: 14, - ) - ], - ), ], ) ], ).expanded ], - ).toShadowContainer(context,padding: 12).onPress(() => onPressed(device)); + ).toShadowContainer(context, padding: 12, borderColor: isSelected ? AppColor.blueStatus(context) : Colors.transparent).onPress(() => onPressed(device)); } } diff --git a/lib/views/widgets/equipment/asset_picker.dart b/lib/views/widgets/equipment/asset_picker.dart index 4cfbd186..113a5876 100644 --- a/lib/views/widgets/equipment/asset_picker.dart +++ b/lib/views/widgets/equipment/asset_picker.dart @@ -11,22 +11,44 @@ import '../../../new_views/app_style/app_color.dart'; class AssetPicker extends StatelessWidget { final Function(Asset)? onPick; + final Function(List)? onMultiAssetPick; + final Function(int)? onAssetRemove; final Asset? device; + final List deviceList; final bool editable; final bool showAssetInfo; final Color? borderColor; final Color? buttonColor; final bool forPPM; final bool showLoading; + final bool multiSelection; - const AssetPicker({Key? key, this.editable = true, this.device, this.onPick, this.borderColor, this.buttonColor, this.showAssetInfo = true, this.forPPM = false, this.showLoading = false}) - : super(key: key); + const AssetPicker( + {Key? key, + this.editable = true, + this.device, + this.deviceList = const [], + this.onPick, + this.onMultiAssetPick, + this.onAssetRemove, + this.borderColor, + this.buttonColor, + this.showAssetInfo = true, + this.multiSelection = false, + this.forPPM = false, + this.showLoading = false}) + : assert( + multiSelection == false || onMultiAssetPick != null, + 'Cannot use multiple asset picker with single onPick Method\n' + 'Use onMultiAssetPick method instead.', + ), + super(key: key); @override Widget build(BuildContext context) { return Column( children: [ - if (device == null) + if (device == null && deviceList.isEmpty) Container( height: 50, alignment: Alignment.center, @@ -45,12 +67,24 @@ class AssetPicker extends StatelessWidget { ], ), ).onPress(() async { - Asset? device = await Navigator.of(context).push(MaterialPageRoute( - builder: (context) => AssetScanQr( - title: context.translation.assetScan, - ))) as Asset?; - if (device != null) { - onPick!(device); + if (multiSelection) { + List? device = await Navigator.of(context).push(MaterialPageRoute( + builder: (context) => AssetScanQr( + title: context.translation.assetScan, + multiSelection: multiSelection, + ))) as List?; + if (device?.isNotEmpty ?? false) { + onMultiAssetPick!(device!); + } + } else { + Asset? device = await Navigator.of(context).push(MaterialPageRoute( + builder: (context) => AssetScanQr( + title: context.translation.assetScan, + multiSelection: multiSelection, + ))) as Asset?; + if (device != null) { + onPick!(device); + } } }) else @@ -71,71 +105,104 @@ class AssetPicker extends StatelessWidget { ], ), ).onPress(() async { - Asset? device = await Navigator.of(context).push(MaterialPageRoute( - builder: (context) => AssetScanQr( - title: context.translation.assetScan, - ))) as Asset?; - if (device != null) { - onPick!(device); + if (multiSelection) { + List? device = await Navigator.of(context).push(MaterialPageRoute( + builder: (context) => AssetScanQr( + title: context.translation.assetScan, + multiSelection: multiSelection, + ))) as List?; + if (device?.isNotEmpty ?? false) { + onMultiAssetPick!(device!); + } + } else { + Asset? device = await Navigator.of(context).push(MaterialPageRoute( + builder: (context) => AssetScanQr( + title: context.translation.assetScan, + multiSelection: multiSelection, + ))) as Asset?; + if (device != null) { + onPick!(device); + } } }), - if (device != null && showAssetInfo) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: showLoading ? Colors.white : const Color(0xffF4F6FC), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color(0xff212936).withOpacity(.03), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - Text( - device?.modelDefinition?.assetName?.cleanupWhitespace.capitalizeFirstOfEach ?? "", - style: TextStyle( - fontSize: 14.toScreenWidth, - fontWeight: FontWeight.w500, - fontStyle: FontStyle.normal, - color: Colors.black87, - decoration: TextDecoration.none, - ), - ).toShimmer(isShow: showLoading).expanded, - const Icon( - Icons.info, - color: Color(0xff7D859A), - size: 20, - ), - ], - ), - 8.height, - "${context.translation.assetNo}: ${device!.assetNumber}".bodyText2(context).toShimmer(isShow: showLoading), - 2.height, - // "${context.translation.manufacture}: ${device.modelDefinition?.manufacturerName}".bodyText(context), - "${context.translation.model}: ${device!.modelDefinition?.modelName}".bodyText2(context).toShimmer(isShow: showLoading), - // "${context.translation.serialNumber}: ${device.assetNumber}".bodyText(context), - // const Divider().defaultStyle(context), - // "${context.translation.department}: ${device.department?.departmentName}".bodyText(context), - // "${context.translation.site}: ${device.site?.custName}".bodyText(context), - ], - ), - ).onPress(() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - clipBehavior: Clip.antiAliasWithSaveLayer, - builder: (BuildContext context) => AssetDetailBottomSheet(device), - ); - }).paddingOnly(top: 16), + if (device != null && showAssetInfo) _assetInfoView(device!, context).paddingOnly(top: 16), + if (deviceList.isNotEmpty && showAssetInfo) + ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.only(top: 16), + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (cxt, index) => _assetInfoView(deviceList[index], context), + separatorBuilder: (cxt, index) => 12.height, + itemCount: deviceList.length) ], ); } + + Widget _assetInfoView(Asset device, BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: showLoading ? Colors.white : const Color(0xffF4F6FC), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xff212936).withOpacity(.03), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Text( + device.modelDefinition?.assetName?.cleanupWhitespace.capitalizeFirstOfEach ?? "", + style: TextStyle( + fontSize: 14.toScreenWidth, + fontWeight: FontWeight.w500, + fontStyle: FontStyle.normal, + color: Colors.black87, + decoration: TextDecoration.none, + ), + ).toShimmer(isShow: showLoading).expanded, + const Icon( + Icons.info, + color: Color(0xff7D859A), + size: 20, + ).onPress(() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + clipBehavior: Clip.antiAliasWithSaveLayer, + builder: (BuildContext context) => AssetDetailBottomSheet(device), + ); + }), + if (multiSelection) ...[ + 4.width, + const Icon( + Icons.delete_rounded, + color: Color(0xffF63939), + size: 20, + ).onPress(() { + onAssetRemove!(device.id!.toInt()); + }), + ] + ], + ), + 8.height, + "${context.translation.assetNo}: ${device!.assetNumber}".bodyText2(context).toShimmer(isShow: showLoading), + 2.height, + // "${context.translation.manufacture}: ${device.modelDefinition?.manufacturerName}".bodyText(context), + "${context.translation.model}: ${device!.modelDefinition?.modelName}".bodyText2(context).toShimmer(isShow: showLoading), + // "${context.translation.serialNumber}: ${device.assetNumber}".bodyText(context), + // const Divider().defaultStyle(context), + // "${context.translation.department}: ${device.department?.departmentName}".bodyText(context), + // "${context.translation.site}: ${device.site?.custName}".bodyText(context), + ], + ), + ); + } } diff --git a/lib/views/widgets/qr/asset_scan_qr.dart b/lib/views/widgets/qr/asset_scan_qr.dart index 13362630..889932ab 100644 --- a/lib/views/widgets/qr/asset_scan_qr.dart +++ b/lib/views/widgets/qr/asset_scan_qr.dart @@ -6,6 +6,7 @@ import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; +import 'package:test_sa/models/device/asset.dart'; import '../../../controllers/providers/api/devices_provider.dart'; import '../../../models/device/asset_search.dart'; @@ -16,8 +17,9 @@ import '../../pages/device_transfer/search_asset_page.dart'; class AssetScanQr extends StatefulWidget { static const String id = "/asset-scan-qr"; - const AssetScanQr({Key? key, required this.title}) : super(key: key); + const AssetScanQr({Key? key, required this.title, this.multiSelection = false}) : super(key: key); final String title; + final bool multiSelection; @override _AssetScanQrState createState() => _AssetScanQrState(); @@ -51,7 +53,7 @@ class _AssetScanQrState extends State { _pickManually() async { _controller?.pauseCamera(); - await Navigator.push(context, MaterialPageRoute(builder: (context) => const SearchAssetPage())).then((value) => _controller?.resumeCamera()); + await Navigator.push(context, MaterialPageRoute(builder: (context) => SearchAssetPage(multiSelection: widget.multiSelection))).then((value) => _controller?.resumeCamera()); } _getDevice(String result, {bool isQr = false}) async { @@ -81,7 +83,11 @@ class _AssetScanQrState extends State { _scanDone = true; final result = await _getDevice(scanData.code!, isQr: true); if (result.isNotEmpty) { - Navigator.of(context).pop(result[0]); + if (widget.multiSelection) { + Navigator.of(context).pop([result[0]]); + } else { + Navigator.of(context).pop(result[0]); + } } else { _scanDone = false; }