diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 96504e7..d9e2fba 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -44,6 +44,12 @@ + + + + + + + diff --git a/lib/infrastructure/service/app_navigation_observer.dart b/lib/infrastructure/service/app_navigation_observer.dart index 44357cd..3fe440f 100644 --- a/lib/infrastructure/service/app_navigation_observer.dart +++ b/lib/infrastructure/service/app_navigation_observer.dart @@ -16,7 +16,6 @@ class CustomNavigationObserver extends NavigatorObserver { @override void didPush(Route route, Route? previousRoute) async { - super.didPush(route, previousRoute); final routeName = route.settings.name; if (!_isWorkDone && (routeName == ChickenRoutes.init || routeName == ChickenRoutes.auth)) { _isWorkDone = true; @@ -31,6 +30,7 @@ class CustomNavigationObserver extends NavigatorObserver { _isWorkDone = true; await setupLiveStockDI(); } + super.didPush(route, previousRoute); tLog('CustomNavigationObserver: didPush - $routeName'); } diff --git a/lib/presentation/pages/modules/logic.dart b/lib/presentation/pages/modules/logic.dart index d35b096..8985caf 100644 --- a/lib/presentation/pages/modules/logic.dart +++ b/lib/presentation/pages/modules/logic.dart @@ -1,7 +1,12 @@ +import 'package:rasadyar_chicken/presentation/routes/routes.dart'; import 'package:rasadyar_core/core.dart'; +import 'package:rasadyar_inspection/inspection.dart'; +import 'package:rasadyar_livestock/injection/live_stock_di.dart'; +import 'package:rasadyar_livestock/presentation/routes/app_pages.dart'; class ModulesLogic extends GetxController { TokenStorageService tokenService = Get.find(); + RxBool isLoading = false.obs; List moduleList = [ ModuleModel(title: 'بازرسی', icon: Assets.icons.inspection.path, module: Module.inspection), @@ -25,4 +30,23 @@ class ModulesLogic extends GetxController { tokenService.saveModule(module); tokenService.appModule.value = module; } + + Future navigateToModule(Module module) async { + if (module == Module.inspection) { + Get.offAllNamed(InspectionRoutes.init); + } else if (module == Module.liveStocks) { + await setupLiveStockDI(); + Get.offAllNamed(LiveStockRoutes.init); + } else if (module == Module.chicken) { + Get.offAllNamed(ChickenRoutes.init); + } + } + + void onTapCard(Module module, int index) async { + isLoading.value = true; + selectedIndex.value = index; + saveModule(module); + await Future.delayed(Duration(milliseconds: 800)); // Simulate loading delay + navigateToModule(module); + } } diff --git a/lib/presentation/pages/modules/view.dart b/lib/presentation/pages/modules/view.dart index 880050a..6ff320a 100644 --- a/lib/presentation/pages/modules/view.dart +++ b/lib/presentation/pages/modules/view.dart @@ -1,7 +1,6 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:rasadyar_chicken/chicken.dart'; import 'package:rasadyar_core/core.dart'; -import 'package:rasadyar_inspection/inspection.dart'; import 'logic.dart'; @@ -16,39 +15,38 @@ class ModulesPage extends GetView { centerTitle: true, backgroundColor: AppColor.blueNormal, ), - body: GridView.builder( - padding: EdgeInsets.symmetric(horizontal: 10, vertical: 20), - - itemBuilder: (context, index) { - final module = controller.moduleList[index]; - return CardIcon( - title: module.title, - icon: module.icon, - onTap: () { - controller.selectedIndex.value = index; - controller.saveModule(module.module); - - // Navigate to the appropriate route based on the selected module - switch (module.module) { - case Module.inspection: - Get.toNamed(InspectionRoutes.init); - break; - case Module.liveStocks: - //TODO: Implement liveStocks module navigation - case Module.chicken: - Get.toNamed(ChickenRoutes.init); - break; - } + body: Stack( + fit: StackFit.expand, + alignment: Alignment.center, + children: [ + GridView.builder( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 20), + itemBuilder: (context, index) { + final module = controller.moduleList[index]; + return CardIcon( + title: module.title, + icon: module.icon, + onTap: () => controller.onTapCard(module.module, index), + ); }, - ); - }, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - mainAxisSpacing: 10, - crossAxisSpacing: 10, - ), - physics: BouncingScrollPhysics(), - itemCount: controller.moduleList.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + ), + physics: BouncingScrollPhysics(), + itemCount: controller.moduleList.length, + ), + ObxValue((loading) { + if (!controller.isLoading.value) return SizedBox.shrink(); + return Container( + color: Colors.grey.withValues(alpha: 0.5), + child: Center( + child: CupertinoActivityIndicator(color: AppColor.greenNormal, radius: 30), + ), + ); + }, controller.isLoading), + ], ), ); } diff --git a/packages/core/lib/core.dart b/packages/core/lib/core.dart index c881e08..1539941 100644 --- a/packages/core/lib/core.dart +++ b/packages/core/lib/core.dart @@ -8,6 +8,7 @@ export 'package:dio/dio.dart'; export 'package:flutter_localizations/flutter_localizations.dart'; export 'package:flutter_map/flutter_map.dart'; export 'package:flutter_map_animations/flutter_map_animations.dart'; +export 'package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart'; export 'package:flutter_rating_bar/flutter_rating_bar.dart'; export 'package:flutter_screenutil/flutter_screenutil.dart'; export 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -21,6 +22,7 @@ export 'package:get/get.dart' hide FormData, MultipartFile, Response; export 'package:get_it/get_it.dart'; //local storage export 'package:hive_ce_flutter/hive_flutter.dart'; +export 'package:image_cropper/image_cropper.dart'; ///image picker export 'package:image_picker/image_picker.dart'; //encryption @@ -36,7 +38,6 @@ export 'package:pretty_dio_logger/pretty_dio_logger.dart'; export 'package:rasadyar_core/presentation/common/common.dart'; export 'package:rasadyar_core/presentation/utils/utils.dart'; export 'package:rasadyar_core/presentation/widget/widget.dart'; -export 'package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart'; //models export 'data/model/model.dart'; @@ -57,5 +58,4 @@ export 'utils/map_utils.dart'; export 'utils/network/network.dart'; export 'utils/route_utils.dart'; export 'utils/separator_input_formatter.dart'; - export 'utils/utils.dart'; diff --git a/packages/core/lib/infrastructure/remote/app_interceptor.dart b/packages/core/lib/infrastructure/remote/app_interceptor.dart index 8af53d5..5685f57 100644 --- a/packages/core/lib/infrastructure/remote/app_interceptor.dart +++ b/packages/core/lib/infrastructure/remote/app_interceptor.dart @@ -10,7 +10,7 @@ class AppInterceptor extends Interceptor { final RefreshTokenCallback? refreshTokenCallback; final SaveTokenCallback saveTokenCallback; final ClearTokenCallback clearTokenCallback; - late final Dio dio; + late Dio dio; dynamic authArguments; static Completer? _refreshCompleter; static bool _isRefreshing = false; @@ -44,7 +44,7 @@ class AppInterceptor extends Interceptor { @override Future onError(DioException err, ErrorInterceptorHandler handler) async { - if (err.response?.statusCode == 401) { + if (err.response?.statusCode == 401 && err.response?.data['detail'] != "No active account found with the given credentials") { final retryResult = await _handleUnauthorizedError(err); if (retryResult != null) { handler.resolve(retryResult); @@ -104,6 +104,7 @@ class AppInterceptor extends Interceptor { return dio.fetch(newOptions); } + //TODO void _handleRefreshFailure() { ApiHandler.cancelAllRequests("Token refresh failed"); diff --git a/packages/core/lib/infrastructure/remote/dio_remote.dart b/packages/core/lib/infrastructure/remote/dio_remote.dart index 473ca3e..184595a 100644 --- a/packages/core/lib/infrastructure/remote/dio_remote.dart +++ b/packages/core/lib/infrastructure/remote/dio_remote.dart @@ -12,6 +12,7 @@ class DioRemote implements IHttpClient { Future init() async { dio = Dio(BaseOptions(baseUrl: baseUrl ?? '')); if (interceptors != null) { + interceptors!.dio = dio; dio.interceptors.add(interceptors!); } diff --git a/packages/core/pubspec.lock b/packages/core/pubspec.lock index 9eea597..936d0e5 100644 --- a/packages/core/pubspec.lock +++ b/packages/core/pubspec.lock @@ -725,6 +725,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + image_cropper: + dependency: "direct main" + description: + name: image_cropper + sha256: "4e9c96c029eb5a23798da1b6af39787f964da6ffc78fd8447c140542a9f7c6fc" + url: "https://pub.dev" + source: hosted + version: "9.1.0" + image_cropper_for_web: + dependency: transitive + description: + name: image_cropper_for_web + sha256: fd81ebe36f636576094377aab32673c4e5d1609b32dec16fad98d2b71f1250a9 + url: "https://pub.dev" + source: hosted + version: "6.1.0" + image_cropper_platform_interface: + dependency: transitive + description: + name: image_cropper_platform_interface + sha256: "6ca6b81769abff9a4dcc3bbd3d75f5dfa9de6b870ae9613c8cd237333a4283af" + url: "https://pub.dev" + source: hosted + version: "7.1.0" image_picker: dependency: "direct main" description: diff --git a/packages/core/pubspec.yaml b/packages/core/pubspec.yaml index f87b92b..90dd599 100644 --- a/packages/core/pubspec.yaml +++ b/packages/core/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: ##image_picker image_picker: ^1.1.2 - + image_cropper: ^9.1.0 #UI cupertino_icons: ^1.0.8 diff --git a/packages/livestock/lib/data/common/dio_exception_handeler.dart b/packages/livestock/lib/data/common/dio_exception_handeler.dart index 6f5e233..e3f34d2 100644 --- a/packages/livestock/lib/data/common/dio_exception_handeler.dart +++ b/packages/livestock/lib/data/common/dio_exception_handeler.dart @@ -32,22 +32,16 @@ class DioErrorHandler { _errorSnackBar( error.response?.data.keys.first == 'is_user' ? 'کاربر با این شماره تلفن وجود ندارد' - : error.response?.data[error.response?.data.keys.first] ?? - 'خطا در برقراری ارتباط با سرور', + : '${error.response?.statusCode} - ${error.response?.data[error.response?.data.keys.first]}' ?? + 'خطا در برقراری ارتباط با سرور', ), ); } GetSnackBar _errorSnackBar(String message) { return GetSnackBar( - titleText: Text( - 'خطا', - style: AppFonts.yekan14.copyWith(color: Colors.white), - ), - messageText: Text( - message, - style: AppFonts.yekan12.copyWith(color: Colors.white), - ), + titleText: Text('خطا', style: AppFonts.yekan14.copyWith(color: Colors.white)), + messageText: Text(message, style: AppFonts.yekan12.copyWith(color: Colors.white)), backgroundColor: AppColor.error, margin: EdgeInsets.symmetric(horizontal: 12, vertical: 8), borderRadius: 12, diff --git a/packages/livestock/lib/data/data_source/remote/auth/auth_remote_imp.dart b/packages/livestock/lib/data/data_source/remote/auth/auth_remote_imp.dart index f67a63d..e4f227b 100644 --- a/packages/livestock/lib/data/data_source/remote/auth/auth_remote_imp.dart +++ b/packages/livestock/lib/data/data_source/remote/auth/auth_remote_imp.dart @@ -16,7 +16,6 @@ class AuthRemoteDataSourceImp extends AuthRemoteDataSource { '${_BASE_URL}login/', data: authRequest, fromJson: AuthResponseModel.fromJson, - headers: {'Content-Type': 'application/json'}, ); return res.data; } diff --git a/packages/livestock/lib/injection/live_stock_di.dart b/packages/livestock/lib/injection/live_stock_di.dart index 10dd71e..954be4c 100644 --- a/packages/livestock/lib/injection/live_stock_di.dart +++ b/packages/livestock/lib/injection/live_stock_di.dart @@ -11,27 +11,17 @@ GetIt get diLiveStock => GetIt.instance; Future setupLiveStockDI() async { diLiveStock.registerSingleton(DioErrorHandler()); - final tokenService = Get.find(); - if (tokenService.baseurl.value == null) { await tokenService.saveBaseUrl('https://api.dam.rasadyar.net/'); } - - diLiveStock.registerLazySingleton( - () => AuthRemoteDataSourceImp(diLiveStock.get()), - ); - - diLiveStock.registerLazySingleton( - () => AuthRepositoryImp(diLiveStock.get()), - ); - - + // First register AppInterceptor with lazy callbacks diLiveStock.registerLazySingleton( - () => AppInterceptor( + () => AppInterceptor( refreshTokenCallback: () async { + // Use lazy access to avoid circular dependency final authRepository = diLiveStock.get(); final hasAuthenticated = await authRepository.hasAuthenticated(); if (hasAuthenticated) { @@ -53,13 +43,26 @@ Future setupLiveStockDI() async { ), ); - + // Register DioRemote with the interceptor diLiveStock.registerLazySingleton( - () => DioRemote( + () => DioRemote( baseUrl: tokenService.baseurl.value, - interceptors: diLiveStock.get(), + // interceptors: diLiveStock.get(), ), ); + // Initialize DioRemote await diLiveStock.get().init(); + + // Now register the data source and repository + diLiveStock.registerLazySingleton( + () => AuthRemoteDataSourceImp(diLiveStock.get()), + ); + + diLiveStock.registerLazySingleton( + () => AuthRepositoryImp(diLiveStock.get()), + ); + + diLiveStock.registerLazySingleton(() => ImagePicker()); + await diLiveStock.allReady(); } diff --git a/packages/livestock/lib/presentation/page/auth/logic.dart b/packages/livestock/lib/presentation/page/auth/logic.dart index 2431423..755fe07 100644 --- a/packages/livestock/lib/presentation/page/auth/logic.dart +++ b/packages/livestock/lib/presentation/page/auth/logic.dart @@ -122,7 +122,7 @@ class AuthLogic extends GetxController with GetTickerProviderStateMixin { final loginRequestModel = _buildLoginRequest(); isLoading.value = true; await safeCall( - call: () async => authRepository.login(authRequest: loginRequestModel.toJson()), + call: () async => await authRepository.login(authRequest: loginRequestModel.toJson()), onSuccess: (result) async { await tokenStorageService.saveModule(_module); await tokenStorageService.saveRefreshToken(result?.refresh ?? ''); diff --git a/packages/livestock/lib/presentation/page/auth/view.dart b/packages/livestock/lib/presentation/page/auth/view.dart index 2c5d35c..f0a71b4 100644 --- a/packages/livestock/lib/presentation/page/auth/view.dart +++ b/packages/livestock/lib/presentation/page/auth/view.dart @@ -17,27 +17,29 @@ class AuthPage extends GetView { children: [ Assets.vec.bgAuthSvg.svg(fit: BoxFit.fill), - Padding( - padding: EdgeInsets.symmetric(horizontal: 10.r), - child: FadeTransition( - opacity: controller.textAnimation, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 12, - children: [ - Text( - 'به سامانه رصدیار خوش آمدید!', - textAlign: TextAlign.right, - style: AppFonts.yekan25Bold.copyWith(color: Colors.white), - ), - Text( - 'سامانه رصد و پایش زنجیره تامین، تولید و توزیع کالا های اساسی', - textAlign: TextAlign.center, - style: AppFonts.yekan16.copyWith(color: Colors.white), - ), - ], + Center( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 10.r), + child: FadeTransition( + opacity: controller.textAnimation, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 12, + children: [ + Text( + 'به سامانه رصدیار خوش آمدید!', + textAlign: TextAlign.right, + style: AppFonts.yekan25Bold.copyWith(color: Colors.white), + ), + Text( + 'سامانه رصد و پایش زنجیره تامین، تولید و توزیع کالا های اساسی', + textAlign: TextAlign.center, + style: AppFonts.yekan16.copyWith(color: Colors.white), + ), + ], + ), ), ), ), diff --git a/packages/livestock/lib/presentation/page/map/logic.dart b/packages/livestock/lib/presentation/page/map/logic.dart index 04370f3..f68b238 100644 --- a/packages/livestock/lib/presentation/page/map/logic.dart +++ b/packages/livestock/lib/presentation/page/map/logic.dart @@ -1,8 +1,7 @@ import 'package:rasadyar_core/core.dart'; +import 'package:rasadyar_livestock/presentation/widgets/base_page/logic.dart'; class MapLogic extends GetxController { - var ss = Get.find(); - - + BaseLogic baseLogic = Get.find(); } diff --git a/packages/livestock/lib/presentation/page/map/view.dart b/packages/livestock/lib/presentation/page/map/view.dart index 451a95e..670308d 100644 --- a/packages/livestock/lib/presentation/page/map/view.dart +++ b/packages/livestock/lib/presentation/page/map/view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:rasadyar_core/core.dart'; import 'package:rasadyar_livestock/presentation/page/map/widget/map_widget/view.dart'; +import 'package:rasadyar_livestock/presentation/widgets/base_page/view.dart'; import 'logic.dart'; @@ -9,6 +10,158 @@ class MapPage extends GetView { @override Widget build(BuildContext context) { - return Scaffold(body: Stack(children: [MapWidget()])); + return BasePage( + hasSearch: true, + hasFilter: true, + hasBack: false, + defaultSearch: false, + filteringWidget: filterWidget(showIndex: 3.obs, filterIndex: 5.obs), + widgets: [MapWidget()], + ); + } + + Widget filterWidget({required RxInt filterIndex, required RxInt showIndex}) { + return BaseBottomSheet( + height: Get.height * 0.5, + child: Container(color: Colors.red), + ); + } + + BaseBottomSheet searchWidget() { + return BaseBottomSheet( + height: Get.height * 0.85, + rootChild: Column( + spacing: 8, + children: [ + Row( + spacing: 12, + children: [ + Expanded( + child: RTextField( + height: 40, + borderColor: AppColor.blackLight, + suffixIcon: ObxValue( + (data) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: (data.value == null) + ? Assets.vec.searchSvg.svg( + width: 10, + height: 10, + colorFilter: ColorFilter.mode(AppColor.blueNormal, BlendMode.srcIn), + ) + : IconButton( + onPressed: () { + controller.baseLogic.searchTextController.clear(); + controller.baseLogic.searchValue.value = null; + controller.baseLogic.isSearchSelected.value = false; + //controller.mapLogic.hasFilterOrSearch.value = false; + //controller.searchedPoultryLocation.value = Resource.initial(); + }, + enableFeedback: true, + padding: EdgeInsets.zero, + iconSize: 24, + splashRadius: 50, + icon: Assets.vec.closeCircleSvg.svg( + width: 20, + height: 20, + colorFilter: ColorFilter.mode(AppColor.blueNormal, BlendMode.srcIn), + ), + ), + ), + controller.baseLogic.searchValue, + ), + hintText: 'جستجو کنید ...', + hintStyle: AppFonts.yekan16.copyWith(color: AppColor.blueNormal), + filledColor: Colors.white, + filled: true, + controller: controller.baseLogic.searchTextController, + onChanged: (val) => controller.baseLogic.searchValue.value = val, + ), + ), + GestureDetector( + onTap: () { + Get.back(); + }, + child: Assets.vec.mapSvg.svg( + width: 24.w, + height: 24.h, + colorFilter: ColorFilter.mode(AppColor.blueNormal, BlendMode.srcIn), + ), + ), + ], + ), + /* Expanded( + child: ObxValue((rxData) { + final resource = rxData.value; + final status = resource.status; + final items = resource.data; + final message = resource.message ?? 'خطا در بارگذاری'; + + if (status == ResourceStatus.initial) { + return Center(child: Text('ابتدا جستجو کنید')); + } + + if (status == ResourceStatus.loading) { + return const Center(child: CircularProgressIndicator()); + } + + if (status == ResourceStatus.error) { + return Center(child: Text(message)); + } + + if (items == null || items.isEmpty) { + return Center(child: EmptyWidget()); + } + + return ListView.separated( + itemCount: items.length, + separatorBuilder: (context, index) => SizedBox(height: 8), + itemBuilder: (context, index) { + final item = items[index]; // اگر item استفاده نمیشه، می‌تونه حذف بشه + return ListItem2( + index: index, + labelColor: AppColor.blueLight, + labelIcon: Assets.vec.cowSvg.path, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + item.unitName ?? 'N/A', + style: AppFonts.yekan10.copyWith(color: AppColor.blueNormal), + ), + Text( + item.user?.fullname ?? '', + style: AppFonts.yekan12.copyWith(color: AppColor.darkGreyDarkHover), + ), + ], + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'جوجه ریزی فعال', + style: AppFonts.yekan10.copyWith(color: AppColor.blueNormal), + ), + Text( + (item.hatching != null && item.hatching!.isNotEmpty) + ? 'دارد' + : 'ندراد', + style: AppFonts.yekan12.copyWith(color: AppColor.darkGreyDarkHover), + ), + ], + ), + ], + ), + ); + }, + ); + }, controller.searchedPoultryLocation), + ),*/ + ], + ), + ); } } diff --git a/packages/livestock/lib/presentation/page/map/widget/map_widget/view.dart b/packages/livestock/lib/presentation/page/map/widget/map_widget/view.dart index bd2226b..30c7a42 100644 --- a/packages/livestock/lib/presentation/page/map/widget/map_widget/view.dart +++ b/packages/livestock/lib/presentation/page/map/widget/map_widget/view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:rasadyar_core/core.dart'; +import 'package:rasadyar_livestock/presentation/routes/app_pages.dart'; import 'logic.dart'; @@ -172,8 +173,40 @@ class MapWidget extends GetView { .map( (element) => Marker( point: element, - child: FaIcon(FontAwesomeIcons.locationPin, color: AppColor.error), + child: IconButton( + onPressed: () { + Get.bottomSheet( + detailsBottomSheet(), + isScrollControlled: true, + isDismissible: true, + ignoreSafeArea: false, + ); + }, + icon: FaIcon(FontAwesomeIcons.locationPin, color: AppColor.error), + ), ), ) .toList(); + + Widget detailsBottomSheet() { + return BaseBottomSheet( + height: 250.h, + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 20, + children: [ + Text('مشخصات محل', style: AppFonts.yekan16Bold), + // Add more details here + RElevated( + text: 'ایجاد بازرسی', + width: Get.width, + height: 40.h, + onPressed: () { + Get.toNamed(LiveStockRoutes.requestTagging); + }, + ), + ], + ), + ); + } } diff --git a/packages/livestock/lib/presentation/page/request_tagging/logic.dart b/packages/livestock/lib/presentation/page/request_tagging/logic.dart index de94cdb..f8875fc 100644 --- a/packages/livestock/lib/presentation/page/request_tagging/logic.dart +++ b/packages/livestock/lib/presentation/page/request_tagging/logic.dart @@ -1,10 +1,32 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:rasadyar_core/core.dart'; -import 'package:rasadyar_livestock/presentation/page/root/logic.dart'; +import 'package:rasadyar_livestock/injection/live_stock_di.dart'; class RequestTaggingLogic extends GetxController { + RxInt currentIndex = 0.obs; + final int maxStep = 3; + + RxBool nextButtonEnabled = true.obs; + + final TextEditingController phoneController = TextEditingController(); + final TextEditingController fullNameController = TextEditingController(); + final TextEditingController addressController = TextEditingController(); + + ImagePicker imagePicker = diLiveStock.get(); + Rxn rancherImage = Rxn(null); + + @override + void onInit() { + super.onInit(); + setUpTextControllerListeners(); + setUpNextButtonListeners(); + ever(rancherImage, (callback) { + setUpNextButtonListeners(); + }); + } -final TextEditingController phoneController = TextEditingController(); @override void onReady() { super.onReady(); @@ -14,4 +36,68 @@ final TextEditingController phoneController = TextEditingController(); void onClose() { super.onClose(); } + + void onNext() { + if (currentIndex.value < maxStep) { + if (currentIndex.value == 0) {} + currentIndex.value++; + } + } + + void onPrevious() { + if (currentIndex.value > 0) { + currentIndex.value--; + } + } + + void setUpNextButtonListeners() { + if (currentIndex.value == 0) { + nextButtonEnabled.value = + phoneController.text.isNotEmpty && + fullNameController.text.isNotEmpty && + addressController.text.isNotEmpty && + rancherImage.value != null; + + return; + } + } + + void setUpTextControllerListeners() { + phoneController.addListener(setUpNextButtonListeners); + fullNameController.addListener(setUpNextButtonListeners); + addressController.addListener(setUpNextButtonListeners); + } + + Future pickImage() async { + rancherImage.value = await imagePicker.pickImage( + source: ImageSource.camera, + imageQuality: 60, + maxWidth: 1080, + maxHeight: 720, + ); + + getFileSizeInKB(rancherImage.value?.path ?? '', tag: 'Picked'); + } + + Future cropImage() async { + if (rancherImage.value == null) return; + + final CroppedFile? cropped = await ImageCropper().cropImage( + sourcePath: rancherImage.value!.path, + maxWidth: 1080, + maxHeight: 720, + compressQuality: 60, + ); + if (cropped == null) return; + rancherImage.value = XFile(cropped.path); + + getFileSizeInKB(rancherImage.value?.path ?? '', tag: 'Cropped'); + } + + void getFileSizeInKB(String filePath, {String? tag}) { + final file = File(filePath); + final bytes = file.lengthSync(); + var size = (bytes / 1024).ceil(); + iLog('${tag ?? 'Picked'} image Size: $size'); + } } diff --git a/packages/livestock/lib/presentation/page/request_tagging/view.dart b/packages/livestock/lib/presentation/page/request_tagging/view.dart index 0ff57ed..73ed45c 100644 --- a/packages/livestock/lib/presentation/page/request_tagging/view.dart +++ b/packages/livestock/lib/presentation/page/request_tagging/view.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:rasadyar_core/core.dart'; @@ -19,91 +21,272 @@ class RequestTaggingPage extends GetView { ), body: Padding( padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15), - child: Column( - children: [ - RTextField( - controller: controller.phoneController, - label: 'تلفن دامدار', + child: ObxValue((index) { + return Column( + children: [ + Expanded(child: _buildStep(index.value)), + nextOrPreviousWidget(), + ], + ); + }, controller.currentIndex), + ), + ); + } + + Row nextOrPreviousWidget() { + return Row( + spacing: 10, + children: [ + ObxValue( + (data) => Expanded( + flex: 2, + child: RElevated( + height: 40.h, + enabled: data.value, + onPressed: controller.onNext, + child: Text('بعدی'), + backgroundColor: AppColor.blueNormal, ), + ), + controller.nextButtonEnabled, + ), + Expanded( + child: ROutlinedElevated( + enabled: controller.currentIndex.value > 0, + onPressed: controller.onPrevious, + child: Text('قبلی'), + borderColor: AppColor.error, + ), + ), + ], + ); + } - SizedBox( - width: Get.width, - height: 356, - child: Card( - color: Colors.white, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - Expanded( - child: Container( - width: Get.width, + Widget _buildStep(int index) { + switch (index) { + case 0: + return firstStepWidget(); + default: + return Center( + child: Text( + 'مرحله $index در دست توسعه است', + style: AppFonts.yekan16.copyWith(color: AppColor.redNormal), + textDirection: TextDirection.rtl, + ), + ); + } + } - decoration: BoxDecoration( - color: AppColor.lightGreyNormal, - borderRadius: BorderRadius.circular(8), - ), - child: Center( - child: Assets.images.placeHolder.image( - height: 150, - width: 200, - ), - ), - ), + Widget firstStepWidget() { + return Form( + child: Column( + spacing: 16, + children: [ + Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border.all(color: AppColor.lightGreyNormal, width: 1), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + spacing: 10, + children: [ + Row(children: [Text('اطلاعات دامدار', style: AppFonts.yekan16Bold)]), + RTextField(controller: controller.fullNameController, label: 'نام ونام خانوادگی'), + RTextField(controller: controller.phoneController, label: 'تلفن'), + RTextField(controller: controller.addressController, label: 'ادرس'), + ], + ), + ), + + SizedBox( + width: Get.width, + height: 356.h, + child: Container( + padding: EdgeInsets.all(8.r), + decoration: BoxDecoration( + border: Border.all(color: AppColor.lightGreyNormal, width: 1.w), + borderRadius: BorderRadius.circular(8.r), + ), + child: Column( + spacing: 8, + children: [ + Row(children: [Text('تصویر دامدار', style: AppFonts.yekan16Bold)]), + Expanded( + child: Container( + width: Get.width, + decoration: BoxDecoration( + color: AppColor.lightGreyNormal, + borderRadius: BorderRadius.circular(8.r), ), - SizedBox(height: 15), - Container( - width: Get.width, - height: 40, - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: AppColor.blueNormal, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - ' تصویر گله', - style: AppFonts.yekan14.copyWith( - color: Colors.white, - ), - ), - Icon( - CupertinoIcons.arrow_up_doc, - color: Colors.white, - ), - ], - ), - ), + child: Center( + child: ObxValue((tmpImage) { + if (tmpImage.value == null) { + return Assets.vec.placeHolderSvg.svg(height: 150.h, width: 200.w); + } else { + return Image.file(File(tmpImage.value!.path), fit: BoxFit.cover); + } + }, controller.rancherImage), ), - ], + ), ), - ), + GestureDetector( + onTap: () async { + await controller.pickImage(); + await showCropDialog(); + }, + child: Container( + width: Get.width, + height: 40.h, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: AppColor.blueNormal, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Padding( + padding: EdgeInsets.all(10.r), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(' دوربین', style: AppFonts.yekan14.copyWith(color: Colors.white)), + Icon(CupertinoIcons.arrow_up_doc, color: Colors.white), + ], + ), + ), + ), + ), + ], ), ), + ), - Spacer(), + Spacer(), + /* RElevated( + text: 'ارسال تصویر گله', + onPressed: () { + Get.toNamed(LiveStockRoutes.tagging); + }, + height: 40, + isFullWidth: true, + backgroundColor: AppColor.greenNormal, + textStyle: AppFonts.yekan16.copyWith(color: Colors.white), + ),*/ + ], + ), + ); + } + Future showCropDialog() async { + await Get.dialog( + Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'آیا نیازی به برش تصویر دارید؟', + style: AppFonts.yekan16Bold, + textAlign: TextAlign.center, + ), - RElevated( - text: 'ارسال تصویر گله', - onPressed: () { - Get.toNamed(LiveStockRoutes.tagging); - }, - height: 40, - isFullWidth: true, - backgroundColor: AppColor.greenNormal, - textStyle: AppFonts.yekan16.copyWith(color: Colors.white), - ), - ], + const SizedBox(height: 24), + + Row( + mainAxisAlignment: MainAxisAlignment.end, + spacing: 12.w, + children: [ + RElevated( + height: 40.h, + onPressed: () async { + Get.back(); + await controller.cropImage(); + }, + child: const Text('بله'), + ), + ROutlinedElevated( + onPressed: () => Get.back(), + child: const Text('خیر'), + borderColor: AppColor.error, + ), + ], + ), + ], + ), ), ), ); } + + Column secondStepWidget() { + return Column( + children: [ + RTextField(controller: controller.phoneController, label: 'تلفن دامدار'), + + SizedBox( + width: Get.width, + height: 356, + child: Card( + color: Colors.white, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Expanded( + child: Container( + width: Get.width, + + decoration: BoxDecoration( + color: AppColor.lightGreyNormal, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Assets.images.placeHolder.image(height: 150, width: 200), + ), + ), + ), + SizedBox(height: 15), + Container( + width: Get.width, + height: 40, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: AppColor.blueNormal, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(' تصویر گله', style: AppFonts.yekan14.copyWith(color: Colors.white)), + Icon(CupertinoIcons.arrow_up_doc, color: Colors.white), + ], + ), + ), + ), + ], + ), + ), + ), + ), + + Spacer(), + + RElevated( + text: 'ارسال تصویر گله', + onPressed: () { + Get.toNamed(LiveStockRoutes.tagging); + }, + height: 40, + isFullWidth: true, + backgroundColor: AppColor.greenNormal, + textStyle: AppFonts.yekan16.copyWith(color: Colors.white), + ), + ], + ); + } } diff --git a/packages/livestock/lib/presentation/page/root/logic.dart b/packages/livestock/lib/presentation/page/root/logic.dart index 4bb8c4c..7496d7a 100644 --- a/packages/livestock/lib/presentation/page/root/logic.dart +++ b/packages/livestock/lib/presentation/page/root/logic.dart @@ -26,7 +26,7 @@ class RootLogic extends GetxController { ProfilePage(), ]; - RxInt currentIndex = 1.obs; + RxInt currentIndex = 0.obs; @override void onReady() { diff --git a/packages/livestock/lib/presentation/routes/app_pages.dart b/packages/livestock/lib/presentation/routes/app_pages.dart index fa24e46..411f5c0 100644 --- a/packages/livestock/lib/presentation/routes/app_pages.dart +++ b/packages/livestock/lib/presentation/routes/app_pages.dart @@ -11,6 +11,7 @@ import 'package:rasadyar_livestock/presentation/page/root/logic.dart'; import 'package:rasadyar_livestock/presentation/page/root/view.dart'; import 'package:rasadyar_livestock/presentation/page/tagging/logic.dart'; import 'package:rasadyar_livestock/presentation/page/tagging/view.dart'; +import 'package:rasadyar_livestock/presentation/widgets/base_page/logic.dart'; import 'package:rasadyar_livestock/presentation/widgets/captcha/logic.dart'; part 'app_routes.dart'; @@ -38,6 +39,7 @@ sealed class LiveStockPages { Get.lazyPut(() => ProfileLogic()); Get.lazyPut(() => ProfileLogic()); Get.lazyPut(() => MapWidgetLogic()); + Get.lazyPut(() => BaseLogic()); }), children: [ /*GetPage( diff --git a/packages/livestock/lib/presentation/widgets/app_bar/i_app_bar.dart b/packages/livestock/lib/presentation/widgets/app_bar/i_app_bar.dart new file mode 100644 index 0000000..2d776c5 --- /dev/null +++ b/packages/livestock/lib/presentation/widgets/app_bar/i_app_bar.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:rasadyar_core/core.dart'; +import 'package:rasadyar_livestock/presentation/widgets/base_page/logic.dart'; + +RAppBar liveStockAppBar({ + bool hasBack = true, + bool hasFilter = true, + bool hasSearch = true, + bool isBase = false, + VoidCallback? onBackPressed, + GestureTapCallback? onFilterTap, + GestureTapCallback? onSearchTap, +}) { + return RAppBar( + hasBack: isBase == true ? false : hasBack, + onBackPressed: onBackPressed, + leadingWidth: 155, + leading: Row( + mainAxisSize: MainAxisSize.min, + spacing: 6, + children: [ + Text('رصددام', style: AppFonts.yekan16Bold.copyWith(color: Colors.white)), + Assets.vec.appBarInspectionSvg.svg(width: 24, height: 24), + ], + ), + additionalActions: [ + if (!isBase && hasSearch) searchWidget(onSearchTap), + SizedBox(width: 8), + if (!isBase && hasFilter) filterWidget(onFilterTap), + SizedBox(width: 8), + ], + ); +} + +GestureDetector filterWidget(GestureTapCallback? onFilterTap) { + return GestureDetector( + onTap: onFilterTap, + child: Stack( + alignment: Alignment.topRight, + children: [ + Assets.vec.filterOutlineSvg.svg( + width: 20, + height: 20, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + Obx(() { + final controller = Get.find(); + return Visibility( + visible: controller.isFilterSelected.value, + child: Container( + width: 8, + height: 8, + decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle), + ), + ); + }), + ], + ), + ); +} + +GestureDetector searchWidget(GestureTapCallback? onSearchTap) { + return GestureDetector( + onTap: onSearchTap, + child: Stack( + alignment: Alignment.topRight, + children: [ + Assets.vec.searchSvg.svg( + width: 24, + height: 24, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + Obx(() { + final controller = Get.find(); + return Visibility( + visible: controller.searchValue.value != null, + child: Container( + width: 8, + height: 8, + decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle), + ), + ); + }), + ], + ), + ); +} diff --git a/packages/livestock/lib/presentation/widgets/base_page/logic.dart b/packages/livestock/lib/presentation/widgets/base_page/logic.dart new file mode 100644 index 0000000..79ac095 --- /dev/null +++ b/packages/livestock/lib/presentation/widgets/base_page/logic.dart @@ -0,0 +1,25 @@ +import 'package:flutter/cupertino.dart'; +import 'package:rasadyar_core/core.dart'; + +class BaseLogic extends GetxController { + final RxBool isFilterSelected = false.obs; + final RxBool isSearchSelected = false.obs; + final TextEditingController searchTextController = TextEditingController(); + final RxnString searchValue = RxnString(); + + void setSearchCallback(void Function(String)? onSearchChanged) { + debounce(searchValue, (val) { + if (val != null && val.trim().isNotEmpty) { + onSearchChanged?.call(val); + } + }, time: const Duration(milliseconds: 600)); + } + + void toggleFilter() { + isFilterSelected.value = !isFilterSelected.value; + } + + void toggleSearch() { + isSearchSelected.value = !isSearchSelected.value; + } +} diff --git a/packages/livestock/lib/presentation/widgets/base_page/view.dart b/packages/livestock/lib/presentation/widgets/base_page/view.dart new file mode 100644 index 0000000..adeea02 --- /dev/null +++ b/packages/livestock/lib/presentation/widgets/base_page/view.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:rasadyar_core/core.dart'; +import 'package:rasadyar_livestock/presentation/widgets/app_bar/i_app_bar.dart'; +import 'package:rasadyar_livestock/presentation/widgets/search.dart'; + +import 'logic.dart'; + +class BasePage extends StatefulWidget { + const BasePage({ + super.key, + this.routes, + required this.widgets, + this.routesWidget, + this.floatingActionButtonLocation, + this.floatingActionButton, + this.onSearchChanged, + this.hasBack = true, + this.hasFilter = true, + this.hasSearch = true, + this.isBase = false, + this.defaultSearch = true, + this.onBackPressed, + this.onFilterTap, + this.onSearchTap, + this.filteringWidget, + }); + + final List? routes; + final Widget? routesWidget; + final bool defaultSearch; + final List widgets; + final FloatingActionButtonLocation? floatingActionButtonLocation; + final Widget? floatingActionButton; + final Widget? filteringWidget; + final void Function(String?)? onSearchChanged; + final bool hasBack; + final bool hasFilter; + final bool hasSearch; + final bool isBase; + final VoidCallback? onBackPressed; + final GestureTapCallback? onFilterTap; + final GestureTapCallback? onSearchTap; + + @override + State createState() => _BasePageState(); +} + +class _BasePageState extends State { + BaseLogic get controller => Get.find(); + Worker? filterWorker; + bool _isBottomSheetOpen = false; + + @override + void initState() { + super.initState(); + /* filterWorker = ever(controller.isFilterSelected, (bool isSelected) { + if (!mounted) return; + + if (isSelected && widget.filteringWidget != null) { + // بررسی اینکه آیا bottomSheet از قبل باز است یا نه + if (_isBottomSheetOpen) { + controller.isFilterSelected.value = false; + return; + } + + // بررسی اینکه آیا route فعلی current است یا نه + if (ModalRoute.of(context)?.isCurrent != true) { + controller.isFilterSelected.value = false; + return; + } + + _isBottomSheetOpen = true; + Get.bottomSheet( + widget.filteringWidget!, + isScrollControlled: true, + isDismissible: true, + enableDrag: true, + ).then((_) { + // تنظیم مقدار به false بعد از بسته شدن bottomSheet + if (mounted) { + _isBottomSheetOpen = false; + controller.isFilterSelected.value = false; + } + }); + } + });*/ + } + + @override + void dispose() { + filterWorker?.dispose(); + super.dispose(); + } + + void _onFilterTap() { + if (widget.hasFilter && widget.filteringWidget != null) { + final currentRoute = ModalRoute.of(context); + if (currentRoute?.isCurrent != true) { + return; + } + + Get.bottomSheet( + widget.filteringWidget!, + isScrollControlled: true, + isDismissible: true, + enableDrag: true, + ); + } + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) => widget.onBackPressed, + child: Scaffold( + backgroundColor: AppColor.bgLight, + appBar: liveStockAppBar( + hasBack: widget.isBase ? false : widget.hasBack, + onBackPressed: widget.onBackPressed, + hasFilter: widget.hasFilter, + hasSearch: widget.hasSearch, + isBase: widget.isBase, + onFilterTap: widget.hasFilter ? _onFilterTap : null, + onSearchTap: widget.hasSearch ? () => controller.toggleSearch() : null, + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + //widget.routesWidget != null ? widget.routesWidget! : buildPageRoute(widget.routes!), + if (!widget.isBase && widget.hasSearch && widget.defaultSearch) ...{ + SearchWidget(onSearchChanged: widget.onSearchChanged), + }, + ...widget.widgets, + ], + ), + floatingActionButtonLocation: + widget.floatingActionButtonLocation ?? FloatingActionButtonLocation.startFloat, + floatingActionButton: widget.floatingActionButton, + ), + ); + } +} diff --git a/packages/livestock/lib/presentation/widgets/search.dart b/packages/livestock/lib/presentation/widgets/search.dart new file mode 100644 index 0000000..753e624 --- /dev/null +++ b/packages/livestock/lib/presentation/widgets/search.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:rasadyar_core/core.dart'; + +import 'base_page/logic.dart'; + + +class SearchWidget extends StatefulWidget { + const SearchWidget({super.key, this.onSearchChanged}); + + final void Function(String?)? onSearchChanged; + + @override + State createState() => _SearchWidgetState(); +} + +class _SearchWidgetState extends State { + late final BaseLogic controller; + final TextEditingController textEditingController = TextEditingController(); + + @override + void initState() { + super.initState(); + controller = Get.find(); + controller.setSearchCallback(widget.onSearchChanged); + } + + @override + Widget build(BuildContext context) { + return ObxValue((data) { + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + height: data.value ? 40 : 0, + child: Visibility( + visible: data.value, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: RTextField( + height: 40, + borderColor: AppColor.blackLight, + suffixIcon: ObxValue( + (data) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: (data.value == null) + ? Assets.vec.searchSvg.svg( + width: 10, + height: 10, + colorFilter: ColorFilter.mode(AppColor.blueNormal, BlendMode.srcIn), + ) + : IconButton( + onPressed: () { + textEditingController.clear(); + controller.searchValue.value = null; + controller.isSearchSelected.value = false; + widget.onSearchChanged?.call(null); + }, + enableFeedback: true, + padding: EdgeInsets.zero, + iconSize: 24, + splashRadius: 50, + icon: Assets.vec.closeCircleSvg.svg( + width: 20, + height: 20, + colorFilter: ColorFilter.mode(AppColor.blueNormal, BlendMode.srcIn), + ), + ), + ), + controller.searchValue, + ), + hintText: 'جستجو کنید ...', + hintStyle: AppFonts.yekan16.copyWith(color: AppColor.blueNormal), + filledColor: Colors.white, + filled: true, + controller: textEditingController, + onChanged: (val) => controller.searchValue.value = val, + ), + ), + ), + ); + }, controller.isSearchSelected); + } +} diff --git a/pubspec.lock b/pubspec.lock index 8351850..c98f20e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -749,6 +749,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + image_cropper: + dependency: transitive + description: + name: image_cropper + sha256: "4e9c96c029eb5a23798da1b6af39787f964da6ffc78fd8447c140542a9f7c6fc" + url: "https://pub.dev" + source: hosted + version: "9.1.0" + image_cropper_for_web: + dependency: transitive + description: + name: image_cropper_for_web + sha256: fd81ebe36f636576094377aab32673c4e5d1609b32dec16fad98d2b71f1250a9 + url: "https://pub.dev" + source: hosted + version: "6.1.0" + image_cropper_platform_interface: + dependency: transitive + description: + name: image_cropper_platform_interface + sha256: "6ca6b81769abff9a4dcc3bbd3d75f5dfa9de6b870ae9613c8cd237333a4283af" + url: "https://pub.dev" + source: hosted + version: "7.1.0" image_picker: dependency: transitive description: