diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 33e7f2f6..9605d872 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1838,5 +1838,6 @@ "aiDisclaimerPrescription": "سيتم مشاركة بيانات نتائج الوصفات الطبية الخاصة بك بشكل آمن مع محلل الذكاء الاصطناعي لدينا لتحليلها. يساعد هذا في توفير رؤى صحية مخصصة. هل ترغب في المتابعة؟", "generateAiAnalysisPrescription": "تحليل الذكاء الاصطناعي", "loadingAIAnalysisPrescription": "ٍتحليل الوصفات الطبية بواسطة الذكاء الاصطناعي، يرجى الانتظار...", - "thisAboveInfoPrescription": "تم تحليل الوصفة الطبية هذه بواسطة الذكاء الاصطناعي، وهي لا تُعدّ نصيحة طبية. استشر طبيبك المختص للتشخيص والعلاج." + "thisAboveInfoPrescription": "تم تحليل الوصفة الطبية هذه بواسطة الذكاء الاصطناعي، وهي لا تُعدّ نصيحة طبية. استشر طبيبك المختص للتشخيص والعلاج.", + "weatherIndicators": "طقس" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 06cec111..21a4aa19 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1828,7 +1828,8 @@ "aiDisclaimerPrescription": "Your prescription data will be securely shared with our AI Analyzer for analysis. This helps provide personalized health insights. Do you want to proceed?", "generateAiAnalysisPrescription": "Generate AI analysis", "loadingAIAnalysisPrescription": "Analysing your prescription, let the AI do the magic, This might take some time.", - "thisAboveInfoPrescription": "This prescription was analyzed by AI, and it is not medical advice. Consult your healthcare provider for diagnosis and treatment." + "thisAboveInfoPrescription": "This prescription was analyzed by AI, and it is not medical advice. Consult your healthcare provider for diagnosis and treatment.", + "weatherIndicators": "Weather" } diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 20e60f5a..e3f180c1 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -234,6 +234,8 @@ class AppAssets { static const String add_new_family_icon = '$svgBasePath/add_new_family_icon.svg'; static const String h_calc = '$svgBasePath/h_calc.svg'; static const String h_calc_selected = '$svgBasePath/h_calc_selected.svg'; + static const String weatherBottom = '$svgBasePath/weather_bottom.svg'; + static const String weatherBottomFill = '$svgBasePath/weather_bottom_fill.svg'; static const String height = '$svgBasePath/height.svg'; static const String weight = '$svgBasePath/weight.svg'; diff --git a/lib/features/weather/weather_view_model.dart b/lib/features/weather/weather_view_model.dart index 32b97753..a145fcca 100644 --- a/lib/features/weather/weather_view_model.dart +++ b/lib/features/weather/weather_view_model.dart @@ -30,60 +30,97 @@ class WeatherMonitorViewModel extends ChangeNotifier { isLoading = true; notifyListeners(); _cityInfoList.clear(); - locationUtils.isShowConfirmDialog = true; - locationUtils.getLocation( - isShowConfirmDialog: true, - onFailure: (){ + + // Check if location permission is already granted + final isLocationEnabled = await locationUtils.isLocationPermissionEnabled(); + + if (isLocationEnabled) { + // Permission already granted, fetch data directly + isLocationAvailable = true; + final result = await weatherRepo.getCityInfo(); + result.fold( + (failure) async { isLoading = false; notifyListeners(); - showCommonBottomSheetWithoutHeight( - title: LocaleKeys.notice.tr(), - getIt.get().navigatorKey.currentContext!, - child: Utils.getWarningWidget( - loadingText: LocaleKeys.grantLocationPermission.tr(), - isShowActionButtons: true, - onCancelTap: () { - getIt.get().pop(); - }, - onConfirmTap: () async { - getIt.get().pop(); - openAppSettings(); - }), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isLoading = false; + if (apiResponse.messageStatus == 2) { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? "Unknown error"); + } + } else if (apiResponse.messageStatus == 1) { + final cities = apiResponse.data ?? []; + _cityInfoList.addAll(cities); + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } }, - onSuccess: (position) async { - isLocationAvailable = true; - final result = await weatherRepo.getCityInfo(); - result.fold( - (failure) async { - isLoading = false; - notifyListeners(); - await errorHandlerService.handleError(failure: failure); - if (onError != null) { - onError(failure.toString()); - } - }, - (apiResponse) { - isLoading = false; - if (apiResponse.messageStatus == 2) { + ); + } else { + // Permission not granted, request it + locationUtils.isShowConfirmDialog = true; + locationUtils.getLocation( + isShowConfirmDialog: true, + onFailure: (){ + isLoading = false; + notifyListeners(); + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(), + getIt.get().navigatorKey.currentContext!, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.grantLocationPermission.tr(), + isShowActionButtons: true, + onCancelTap: () { + getIt.get().pop(); + }, + onConfirmTap: () async { + getIt.get().pop(); + openAppSettings(); + }), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + onSuccess: (position) async { + isLocationAvailable = true; + final result = await weatherRepo.getCityInfo(); + result.fold( + (failure) async { + isLoading = false; notifyListeners(); + await errorHandlerService.handleError(failure: failure); if (onError != null) { - onError(apiResponse.errorMessage ?? "Unknown error"); + onError(failure.toString()); } - } else if (apiResponse.messageStatus == 1) { - final cities = apiResponse.data ?? []; - _cityInfoList.addAll(cities); - notifyListeners(); - if (onSuccess != null) { - onSuccess(apiResponse); + }, + (apiResponse) { + isLoading = false; + if (apiResponse.messageStatus == 2) { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? "Unknown error"); + } + } else if (apiResponse.messageStatus == 1) { + final cities = apiResponse.data ?? []; + _cityInfoList.addAll(cities); + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } } - } - }, - ); - }); + }, + ); + }); + } } Future checkIfTheLocationIsEnabledOrNot() async { diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 9d7d99fd..6caf1f3a 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1831,5 +1831,6 @@ abstract class LocaleKeys { static const generateAiAnalysisPrescription = 'generateAiAnalysisPrescription'; static const loadingAIAnalysisPrescription = 'loadingAIAnalysisPrescription'; static const thisAboveInfoPrescription = 'thisAboveInfoPrescription'; + static const weatherIndicators = 'weatherIndicators'; } diff --git a/lib/presentation/home/navigation_screen.dart b/lib/presentation/home/navigation_screen.dart index f96c5d21..92d3bea6 100644 --- a/lib/presentation/home/navigation_screen.dart +++ b/lib/presentation/home/navigation_screen.dart @@ -1,19 +1,23 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; -import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/location_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; -import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/hmg_services/services_page.dart'; import 'package:hmg_patient_app_new/presentation/home/landing_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; +import 'package:hmg_patient_app_new/presentation/weather/weather_details_page.dart'; import 'package:hmg_patient_app_new/routes/app_routes.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/widgets/bottom_navigation/bottom_navigation.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:permission_handler/permission_handler.dart' show openAppSettings; -import '../health_calculators_and_converts/health_calculators_page.dart'; class LandingNavigation extends StatefulWidget { const LandingNavigation({super.key}); @@ -35,7 +39,7 @@ class _LandingNavigationState extends State { physics: const NeverScrollableScrollPhysics(), children: [ const LandingPage(), - appState.isAuthenticated ? MedicalFilePage(showBackIcon: false) : /* need add feedback page */ HealthCalculatorsPage(type: HealthCalConEnum.calculator), + appState.isAuthenticated ? MedicalFilePage(showBackIcon: false) : const WeatherDetailsPage(showBackIcon: false,), SizedBox(), // const ToDoPage(), // appState.isAuthenticated ? UserInfoSelectionScreen() : /* need add news page */ SizedBox(), @@ -45,8 +49,47 @@ class _LandingNavigationState extends State { ), bottomNavigationBar: BottomNavigation( currentIndex: _currentIndex, - onTap: (index) { + onTap: (index) async { setState(() => _currentIndex = index); + + if (_currentIndex == 1 && !appState.isAuthenticated) { + final locationUtils = getIt.get(); + final isLocationEnabled = await locationUtils.isLocationPermissionEnabled(); + if (isLocationEnabled) { + _pageController.animateToPage(index, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); + } else { + locationUtils.getLocation( + isShowConfirmDialog: true, + onSuccess: (position) { + _pageController.animateToPage(index, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); + }, + onFailure: () { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.grantLocationPermission.tr(), + isShowActionButtons: true, + onCancelTap: () { + getIt.get().pop(); + setState(() => _currentIndex = 0); + }, + onConfirmTap: () async { + getIt.get().pop(); + openAppSettings(); + setState(() => _currentIndex = 0); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + ); + } + return; + } + if (_currentIndex == 2) { getIt.get().onTabChanged(0); getIt.get().getPatientFavouriteDoctors(); diff --git a/lib/presentation/weather/weather_details_page.dart b/lib/presentation/weather/weather_details_page.dart index 261eab22..8762a55a 100644 --- a/lib/presentation/weather/weather_details_page.dart +++ b/lib/presentation/weather/weather_details_page.dart @@ -10,9 +10,29 @@ import 'package:hmg_patient_app_new/presentation/hmg_services/widgets/weather_wi import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; -class WeatherDetailsPage extends StatelessWidget { - const WeatherDetailsPage({super.key}); +class WeatherDetailsPage extends StatefulWidget { + final bool showBackIcon; + const WeatherDetailsPage({super.key, this.showBackIcon = true}); + + @override + State createState() => _WeatherDetailsPageState(); +} + +class _WeatherDetailsPageState extends State { + @override + void initState() { + super.initState(); + // Fetch weather data when page loads only if data is not already available + WidgetsBinding.instance.addPostFrameCallback((_) { + final weatherVM = Provider.of(context, listen: false); + // Only fetch if cityInfoList is empty + if (weatherVM.cityInfoList.isEmpty && !weatherVM.isLoading) { + weatherVM.initiateFetchWeather(); + } + }); + } Color _getColorFromColorName(ColorName? colorName) { if (colorName == null) return AppColors.successColor; @@ -132,13 +152,9 @@ class WeatherDetailsPage extends StatelessWidget { backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: LocaleKeys.healthWeatherIndicators.tr(), - isLeading: true, + isLeading: widget.showBackIcon, child: weatherVM.isLoading - ? Center( - child: CircularProgressIndicator( - color: AppColors.primaryRedColor, - ), - ) + ? _buildShimmerLoading() : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -251,5 +267,164 @@ class WeatherDetailsPage extends StatelessWidget { ), ).paddingSymmetrical(24.w, 0.w); } + + Widget _buildShimmerLoading() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Weather Widget Shimmer + Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Container( + margin: EdgeInsets.symmetric(horizontal: 24.w), + padding: EdgeInsets.all(16.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 64.w, + height: 64.w, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(12.r), + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + height: 12.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(4.r), + ), + ), + SizedBox(height: 8.h), + Container( + width: 120.w, + height: 16.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(4.r), + ), + ), + SizedBox(height: 8.h), + Container( + width: 160.w, + height: 12.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(4.r), + ), + ), + ], + ), + ), + ], + ), + SizedBox(height: 16.h), + Container( + width: double.infinity, + height: 40.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(10.r), + ), + ), + ], + ), + ), + ), + SizedBox(height: 16.h), + + // Shimmer for category cards + ...List.generate(3, (index) { + return Shimmer.fromColors( + baseColor: AppColors.shimmerBaseColor, + highlightColor: AppColors.shimmerHighlightColor, + child: Container( + margin: EdgeInsets.only(bottom: 16.h, left: 24.w, right: 24.w), + padding: EdgeInsets.all(16.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 40.w, + height: 40.w, + decoration: BoxDecoration( + color: AppColors.whiteColor, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + height: 14.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(4.r), + ), + ), + SizedBox(height: 8.h), + Container( + width: 100.w, + height: 12.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(4.r), + ), + ), + ], + ), + ), + Container( + width: 60.w, + height: 24.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(8.r), + ), + ), + ], + ), + SizedBox(height: 12.h), + Container( + width: double.infinity, + height: 8.h, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(4.r), + ), + ), + ], + ), + ), + ); + }), + ], + ); + } } diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index f22c6805..41cb6331 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -33,6 +33,7 @@ import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_scr import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_page.dart'; import 'package:hmg_patient_app_new/presentation/water_monitor/water_consumption_page.dart'; import 'package:hmg_patient_app_new/presentation/water_monitor/water_monitor_settings_page.dart'; +import 'package:hmg_patient_app_new/presentation/weather/weather_details_page.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; import 'package:provider/provider.dart'; @@ -95,6 +96,9 @@ class AppRoutes { // Services Price List static const String servicesPriceListPage = '/servicesPriceListPage'; + // Weather + static const String weatherDetailsPage = '/weatherDetailsPage'; + static Map get routes => { initialRoute: (context) => SplashPage(), loginScreen: (context) => LoginScreen(), @@ -131,6 +135,7 @@ class AppRoutes { ), emergencyServicesPage: (context) => EmergencyServicesPage(), servicesPriceListPage: (context) => ServicesPriceListPage(), + weatherDetailsPage: (context) => WeatherDetailsPage(), addHealthTrackerEntryPage: (context) { final args = ModalRoute.of(context)?.settings.arguments as HealthTrackerTypeEnum?; return AddHealthTrackerEntryPage( diff --git a/lib/widgets/bottom_navigation/bottom_navigation.dart b/lib/widgets/bottom_navigation/bottom_navigation.dart index 2401def4..e999842f 100644 --- a/lib/widgets/bottom_navigation/bottom_navigation.dart +++ b/lib/widgets/bottom_navigation/bottom_navigation.dart @@ -23,7 +23,7 @@ class BottomNavigation extends StatelessWidget { BottomNavItem(icon: AppAssets.homeBottom, fillIcon: AppAssets.homeBottomFill, label: LocaleKeys.home.tr(context: context)), appState.isAuthenticated ? BottomNavItem(icon: AppAssets.myFilesBottom, fillIcon: AppAssets.myFilesBottomFill, label: LocaleKeys.medicalFile.tr(context: context)) - : BottomNavItem(icon: AppAssets.h_calc, fillIcon: AppAssets.h_calc_selected, label: LocaleKeys.healthTools.tr()), + : BottomNavItem(icon: AppAssets.weatherBottom, fillIcon: AppAssets.weatherBottomFill, label: LocaleKeys.weatherIndicators.tr(context: context)), BottomNavItem( icon: AppAssets.bookAppoBottom, fillIcon: AppAssets.bookAppoBottom,