diff --git a/assets/images/png/female_icon.png b/assets/images/png/female_icon.png new file mode 100644 index 0000000..b41a542 Binary files /dev/null and b/assets/images/png/female_icon.png differ diff --git a/assets/images/png/male_icon.png b/assets/images/png/male_icon.png new file mode 100644 index 0000000..fa518fb Binary files /dev/null and b/assets/images/png/male_icon.png differ diff --git a/assets/images/svg/calendar-grey.svg b/assets/images/svg/calendar-grey.svg new file mode 100644 index 0000000..2bcc178 --- /dev/null +++ b/assets/images/svg/calendar-grey.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/images/svg/gender.svg b/assets/images/svg/gender.svg new file mode 100644 index 0000000..6819ba6 --- /dev/null +++ b/assets/images/svg/gender.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/ruler.svg b/assets/images/svg/ruler.svg new file mode 100644 index 0000000..a2e8c11 --- /dev/null +++ b/assets/images/svg/ruler.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/tick.svg b/assets/images/svg/tick.svg new file mode 100644 index 0000000..b6210c0 --- /dev/null +++ b/assets/images/svg/tick.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/weight-scale.svg b/assets/images/svg/weight-scale.svg new file mode 100644 index 0000000..c3329ff --- /dev/null +++ b/assets/images/svg/weight-scale.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index b369d62..2f806b6 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -195,8 +195,13 @@ class AppAssets { static const String heart = '$svgBasePath/heart.svg'; static const String alertSquare = '$svgBasePath/alert-square.svg'; static const String arrowRight = '$svgBasePath/arrow-right.svg'; + static const String tickIcon = '$svgBasePath/tick.svg'; // Symptoms Checker + static const String calendarGrey = '$svgBasePath/calendar-grey.svg'; + static const String weightScale = '$svgBasePath/weight-scale.svg'; + static const String rulerIcon = '$svgBasePath/ruler.svg'; + static const String genderIcon = '$svgBasePath/gender.svg'; static const String bodyIcon = '$svgBasePath/body_icon.svg'; static const String rotateIcon = '$svgBasePath/rotate_icon.svg'; static const String refreshIcon = '$svgBasePath/refresh.svg'; @@ -218,6 +223,8 @@ class AppAssets { static const String dummyUser = '$pngBasePath/dummy_user.png'; static const String comprehensiveCheckupEn = '$pngBasePath/cc_en.png'; static const String comprehensiveCheckupAr = '$pngBasePath/cc_er.png'; + static const String maleIcon = '$pngBasePath/male_icon.png'; + static const String femaleIcon = '$pngBasePath/female_icon.png'; static const String fullBodyFront = '$pngBasePath/full_body_front.png'; static const String fullBodyBack = '$pngBasePath/full_body_back.png'; @@ -236,9 +243,9 @@ class AppAnimations { static const String splashLaunching = '$lottieBasePath/splash_launching.json'; static const String noData = '$lottieBasePath/Nodata.json'; static const String ripple = '$lottieBasePath/Ripple.json'; - static const String pending_loading_animation = '$lottieBasePath/pending_loading_animation.json'; + static const String pendingLoadingAnimation = '$lottieBasePath/pending_loading_animation.json'; static const String ambulance = '$lottieBasePath/ambulance.json'; - static const String ambulance_alert = '$lottieBasePath/ambulance_alert.json'; - static const String rrt_ambulance = '$lottieBasePath/rrt_ambulance.json'; + static const String ambulanceAlert = '$lottieBasePath/ambulance_alert.json'; + static const String rrtAmbulance = '$lottieBasePath/rrt_ambulance.json'; } diff --git a/lib/features/symptoms_checker/symptoms_checker_view_model.dart b/lib/features/symptoms_checker/symptoms_checker_view_model.dart index a467d01..34a1754 100644 --- a/lib/features/symptoms_checker/symptoms_checker_view_model.dart +++ b/lib/features/symptoms_checker/symptoms_checker_view_model.dart @@ -36,10 +36,28 @@ class SymptomsCheckerViewModel extends ChangeNotifier { // Selected symptoms tracking (organId -> Set of symptom IDs) final Map> _selectedSymptomsByOrgan = {}; + // User Info Flow State + int _userInfoCurrentPage = 0; + String? _selectedGender; + int? _selectedAge; + double? _selectedHeight; + bool _isHeightCm = true; + double? _selectedWeight; + bool _isWeightKg = true; + // Getters bool isPossibleConditionsLoading = false; + // User Info Getters + int get userInfoCurrentPage => _userInfoCurrentPage; + String? get selectedGender => _selectedGender; + int? get selectedAge => _selectedAge; + double? get selectedHeight => _selectedHeight; + bool get isHeightCm => _isHeightCm; + double? get selectedWeight => _selectedWeight; + bool get isWeightKg => _isWeightKg; + BodyView get currentView => _currentView; Set get selectedOrganIds => _selectedOrganIds; @@ -247,6 +265,91 @@ class SymptomsCheckerViewModel extends ChangeNotifier { _isBottomSheetExpanded = false; _tooltipTimer?.cancel(); _tooltipOrganId = null; + // Reset user info flow + _userInfoCurrentPage = 0; + _selectedGender = null; + _selectedAge = null; + _selectedHeight = null; + _isHeightCm = true; + _selectedWeight = null; + _isWeightKg = true; + notifyListeners(); + } + + // User Info Flow Methods + + /// Set current page in user info flow + void setUserInfoPage(int page) { + _userInfoCurrentPage = page; + notifyListeners(); + } + + /// Navigate to next page in user info flow + void nextUserInfoPage() { + if (_userInfoCurrentPage < 3) { + _userInfoCurrentPage++; + notifyListeners(); + } + } + + /// Navigate to previous page in user info flow + void previousUserInfoPage() { + if (_userInfoCurrentPage > 0) { + _userInfoCurrentPage--; + notifyListeners(); + } + } + + /// Set selected gender + void setGender(String gender) { + _selectedGender = gender; + notifyListeners(); + } + + /// Set selected age + void setAge(int age) { + _selectedAge = age; + notifyListeners(); + } + + /// Set selected height + void setHeight(double height, bool isCm) { + _selectedHeight = height; + _isHeightCm = isCm; + notifyListeners(); + } + + /// Set selected weight + void setWeight(double weight, bool isKg) { + _selectedWeight = weight; + _isWeightKg = isKg; + notifyListeners(); + } + + /// Check if user info page is last + bool get isUserInfoLastPage => _userInfoCurrentPage == 3; + + /// Validate and submit user info + Map getUserInfoData() { + return { + 'gender': _selectedGender, + 'age': _selectedAge, + 'height': _selectedHeight, + 'heightUnit': _isHeightCm ? 'cm' : 'ft', + 'weight': _selectedWeight, + 'weightUnit': _isWeightKg ? 'kg' : 'lbs', + }; + } + + /// Reset user info flow + void resetUserInfo() { + _userInfoCurrentPage = 0; + _selectedGender = null; + _selectedAge = null; + _selectedHeight = null; + _isHeightCm = true; + _selectedWeight = null; + _isWeightKg = true; notifyListeners(); } diff --git a/lib/presentation/authentication/login.dart b/lib/presentation/authentication/login.dart index 28430a8..c14e957 100644 --- a/lib/presentation/authentication/login.dart +++ b/lib/presentation/authentication/login.dart @@ -1,7 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index 0ad58cc..0e60a28 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -4,11 +4,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.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/utils/size_config.dart'; -import 'package:hmg_patient_app_new/core/utils/size_utils.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/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; @@ -25,7 +25,6 @@ import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immed import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/select_immediate_livecare_clinic_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/search_doctor_by_name.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart'; -import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -71,145 +70,137 @@ class _BookAppointmentPageState extends State { regionalViewModel = Provider.of(context, listen: true); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, - body: CollapsingListView( - title: LocaleKeys.bookAppo.tr(context: context), - isLeading: true, - leadingCallback: () { - Navigator.pushAndRemoveUntil( - context, - CustomPageRoute( - page: LandingNavigation(), - ), - (r) => false); - }, - child: SingleChildScrollView( - child: Consumer(builder: (context, bookAppointmentsVM, child) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 16.h), - CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), - tabs: [ - CustomTabBarModel(null, "General".needTranslation), - CustomTabBarModel(null, "LiveCare".needTranslation), - ], - onTabChange: (index) { - bookAppointmentsVM.onTabChanged(index); - }, - ).paddingSymmetrical(24.h, 0.h), - SizedBox(height: 24.h), - getSelectedTabData(bookAppointmentsVM.selectedTabIndex), - SizedBox(height: 24.h), - "Recent Visits".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0.h), - SizedBox(height: 16.h), - Consumer(builder: (context, myAppointmentsVM, child) { - return myAppointmentsVM.isPatientMyDoctorsLoading - ? Column( - crossAxisAlignment: CrossAxisAlignment.center, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: LocaleKeys.bookAppo.tr(context: context), + isLeading: true, + child: SingleChildScrollView( + child: Consumer(builder: (context, bookAppointmentsVM, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Image.network( - "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", - width: 64.w, - height: 64.h, - fit: BoxFit.cover, - ).circle(100).toShimmer2(isShow: true, radius: 50.r), - SizedBox(height: 8.h), - ("Dr. John Smith Smith Smith") - .toString() - .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) - .toShimmer2(isShow: true), - ], - ) - : myAppointmentsVM.patientMyDoctorsList.isEmpty - ? Container( - width: SizeConfig.screenWidth, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.r, - hasShadow: false, - ), - child: Utils.getNoDataWidget( - context, - noDataText: "You don't have any completed visits yet".needTranslation, - isSmallWidget: true, - width: 62.w, - height: 62.h, - ), - ).paddingSymmetrical(24.w, 0.h) - : SizedBox( - height: 110.h, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: myAppointmentsVM.patientMyDoctorsList.length, - shrinkWrap: true, - padding: EdgeInsets.only(left: 24.w, right: 24.w), - itemBuilder: (context, index) { - return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 1000), - child: SlideAnimation( - horizontalOffset: 100.0, - child: FadeInAnimation( - child: SizedBox( - // width: 80.w, - child: Column( + SizedBox(height: 16.h), + CustomTabBar( + activeTextColor: Color(0xffED1C2B), + activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + tabs: [ + CustomTabBarModel(null, "General".needTranslation), + CustomTabBarModel(null, "LiveCare".needTranslation), + ], + onTabChange: (index) { + bookAppointmentsVM.onTabChanged(index); + }, + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 24.h), + getSelectedTabData(bookAppointmentsVM.selectedTabIndex), + SizedBox(height: 24.h), + if (appState.isAuthenticated) ...[ + Consumer(builder: (context, myAppointmentsVM, child) { + return myAppointmentsVM.isPatientMyDoctorsLoading + ? Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Image.network( - myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, + "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", width: 64.w, height: 64.h, fit: BoxFit.cover, - ).circle(100).toShimmer2(isShow: false, radius: 50.r), + ).circle(100).toShimmer2(isShow: true, radius: 50.r), SizedBox(height: 8.h), - SizedBox( - width: 80.w, - child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName) - .toString() - .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) - .toShimmer2(isShow: false), - ), + ("Dr. John Smith Smith Smith") + .toString() + .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) + .toShimmer2(isShow: true), ], - ), - ).onPress(() async { - bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( - clinicID: myAppointmentsVM.patientMyDoctorsList[index].clinicID, - projectID: myAppointmentsVM.patientMyDoctorsList[index].projectID, - doctorID: myAppointmentsVM.patientMyDoctorsList[index].doctorID, - )); - LoaderBottomSheet.showLoader(); - await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) { - LoaderBottomSheet.hideLoader(); - Navigator.of(context).push( - CustomPageRoute( - page: DoctorProfilePage(), - ), - ); - }, onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); - }), - ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h), - ), + ) + : myAppointmentsVM.patientMyDoctorsList.isEmpty + ? SizedBox() + : Column( + children: [ + if (appState.isAuthenticated) ...[], + "Recent Visits".needTranslation.toText18(isBold: true).paddingSymmetrical(24.w, 0.h), + SizedBox(height: 16.h), + SizedBox( + height: 110.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: myAppointmentsVM.patientMyDoctorsList.length, + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.w, right: 24.w), + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: SizedBox( + // width: 80.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.network( + myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, + width: 64.w, + height: 64.h, + fit: BoxFit.cover, + ).circle(100).toShimmer2(isShow: false, radius: 50.r), + SizedBox(height: 8.h), + SizedBox( + width: 80.w, + child: (myAppointmentsVM.patientMyDoctorsList[index].doctorName) + .toString() + .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) + .toShimmer2(isShow: false), + ), + ], + ), + ).onPress(() async { + bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( + clinicID: myAppointmentsVM.patientMyDoctorsList[index].clinicID, + projectID: myAppointmentsVM.patientMyDoctorsList[index].projectID, + doctorID: myAppointmentsVM.patientMyDoctorsList[index].doctorID, + )); + LoaderBottomSheet.showLoader(); + await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage(), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }), + ), + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h), + ), + ), + ], + ); + }), + ], + ], ); }), - ], - ); - }), - ), + ), + ), + ), + _buildSymptomsBottomCard(), + ], ), ); } @@ -460,6 +451,37 @@ class _BookAppointmentPageState extends State { return Container(); } + Widget _buildSymptomsBottomCard() { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Row( + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Not sure? help me choose a clinic!".needTranslation.toText16(weight: FontWeight.w600, color: AppColors.textColor), + SizedBox(height: 4.h), + "Mention your symptoms and find the list of doctors accordingly".needTranslation.toText12( + fontWeight: FontWeight.w500, + color: AppColors.greyTextColor, + ), + ], + ), + ), + SizedBox(width: 16.w), + CustomButton( + height: 40.h, + text: "", + onPressed: () => context.navigateWithName(AppRoutes.userInfoSelection), + icon: AppAssets.arrow_forward, + ) + ], + ).paddingAll(24.w), + ); + } + void openRegionListBottomSheet(BuildContext context, RegionBottomSheetType type) { regionalViewModel.flush(); regionalViewModel.setBottomSheetType(type); diff --git a/lib/presentation/emergency_services/emergency_services_page.dart b/lib/presentation/emergency_services/emergency_services_page.dart index bce7daf..3833d32 100644 --- a/lib/presentation/emergency_services/emergency_services_page.dart +++ b/lib/presentation/emergency_services/emergency_services_page.dart @@ -97,7 +97,7 @@ class EmergencyServicesPage extends StatelessWidget { }), ], ), - Lottie.asset(AppAnimations.ambulance_alert, + Lottie.asset(AppAnimations.ambulanceAlert, repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), SizedBox(height: 8.h), "Confirmation".needTranslation.toText28(color: AppColors.whiteColor, isBold: true), @@ -200,7 +200,7 @@ class EmergencyServicesPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Lottie.asset(AppAnimations.ambulance_alert, repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), + Lottie.asset(AppAnimations.ambulanceAlert, repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), SizedBox(height: 8.h), LocaleKeys.confirm.tr().toText28(color: AppColors.whiteColor, isBold: true), SizedBox(height: 8.h), @@ -313,7 +313,7 @@ class EmergencyServicesPage extends StatelessWidget { }), ], ), - Lottie.asset(AppAnimations.ambulance_alert, + Lottie.asset(AppAnimations.ambulanceAlert, repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), SizedBox(height: 8.h), LocaleKeys.confirm.tr().toText28(color: AppColors.whiteColor, isBold: true), diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 454f1e7..96ec558 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -12,7 +12,6 @@ import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/int_extensions.dart'; -import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; @@ -40,7 +39,6 @@ import 'package:hmg_patient_app_new/presentation/home/widgets/small_service_card import 'package:hmg_patient_app_new/presentation/home/widgets/welcome_widget.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; import 'package:hmg_patient_app_new/presentation/profile_settings/profile_settings.dart'; -import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -494,8 +492,7 @@ class _LandingPageState extends State { mainAxisSize: MainAxisSize.min, spacing: 12.h, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.bell, height: 18.h, width: 18.h) - .onPress(() => context.navigateWithName(AppRoutes.organSelectorPage)), + Utils.buildSvgWithAssets(icon: AppAssets.bell, height: 18.h, width: 18.h), Utils.buildSvgWithAssets(icon: AppAssets.search_icon, height: 18.h, width: 18.h).onPress(() {}), Utils.buildSvgWithAssets(icon: AppAssets.contact_icon, height: 18.h, width: 18.h).onPress(() { showCommonBottomSheetWithoutHeight( @@ -581,7 +578,6 @@ class _LandingPageState extends State { ); }, ), - // height: isDone == false ? ResponsiveExtension.screenHeight * 0.5 : ResponsiveExtension.screenHeight * 0.3, isFullScreen: false, callBackFunc: () { isDone = true; diff --git a/lib/presentation/home/navigation_screen.dart b/lib/presentation/home/navigation_screen.dart index f8447d2..7fbcdb3 100644 --- a/lib/presentation/home/navigation_screen.dart +++ b/lib/presentation/home/navigation_screen.dart @@ -1,12 +1,13 @@ 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/presentation/book_appointment/book_appointment_page.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.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/todo_section/todo_page.dart'; +import 'package:hmg_patient_app_new/routes/app_routes.dart'; import 'package:hmg_patient_app_new/widgets/bottom_navigation/bottom_navigation.dart'; class LandingNavigation extends StatefulWidget { @@ -18,12 +19,11 @@ class LandingNavigation extends StatefulWidget { class _LandingNavigationState extends State { int _currentIndex = 0; - late AppState appState; final PageController _pageController = PageController(); @override Widget build(BuildContext context) { - appState = getIt.get(); + AppState appState = getIt.get(); return Scaffold( body: PageView( controller: _pageController, @@ -31,7 +31,7 @@ class _LandingNavigationState extends State { children: [ const LandingPage(), appState.isAuthenticated ? MedicalFilePage() : /* need add feedback page */ FeedbackPage(), - BookAppointmentPage(), + SizedBox(), const ToDoPage(), ServicesPage(), ], @@ -40,6 +40,10 @@ class _LandingNavigationState extends State { currentIndex: _currentIndex, onTap: (index) { setState(() => _currentIndex = index); + if (_currentIndex == 2) { + context.navigateWithName(AppRoutes.bookAppointmentPage); + return; + } _pageController.animateToPage(index, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); }, ), diff --git a/lib/presentation/symptoms_checker/organ_selector_screen.dart b/lib/presentation/symptoms_checker/organ_selector_screen.dart index 65d9a00..d5dc32c 100644 --- a/lib/presentation/symptoms_checker/organ_selector_screen.dart +++ b/lib/presentation/symptoms_checker/organ_selector_screen.dart @@ -42,7 +42,7 @@ class _OrganSelectorPageState extends State { return; } - context.navigateWithName(AppRoutes.symptomsCheckerScreen); + context.navigateWithName(AppRoutes.symptomsSelectorScreen); } @override diff --git a/lib/presentation/symptoms_checker/risk_factors_screen.dart b/lib/presentation/symptoms_checker/risk_factors_screen.dart index 211aabc..2992593 100644 --- a/lib/presentation/symptoms_checker/risk_factors_screen.dart +++ b/lib/presentation/symptoms_checker/risk_factors_screen.dart @@ -148,7 +148,7 @@ class _RiskFactorsScreenState extends State { Expanded( child: CollapsingListView( title: "Risks".needTranslation, - onLeadingTapped: () => _buildConfirmationBottomSheet( + leadingCallback: () => _buildConfirmationBottomSheet( context: context, onConfirm: () => { context.pop(), diff --git a/lib/presentation/symptoms_checker/suggestions_screen.dart b/lib/presentation/symptoms_checker/suggestions_screen.dart index f9d922a..2832515 100644 --- a/lib/presentation/symptoms_checker/suggestions_screen.dart +++ b/lib/presentation/symptoms_checker/suggestions_screen.dart @@ -148,7 +148,7 @@ class _SuggestionsScreenState extends State { Expanded( child: CollapsingListView( title: "Suggestions".needTranslation, - onLeadingTapped: () => _buildConfirmationBottomSheet( + leadingCallback: () => _buildConfirmationBottomSheet( context: context, onConfirm: () => { context.pop(), diff --git a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart index bed232b..522c5f8 100644 --- a/lib/presentation/symptoms_checker/symptoms_selector_screen.dart +++ b/lib/presentation/symptoms_checker/symptoms_selector_screen.dart @@ -78,7 +78,7 @@ class _SymptomsSelectorScreenState extends State { Expanded( child: CollapsingListView( title: "Symptoms Selector".needTranslation, - onLeadingTapped: () => _buildConfirmationBottomSheet( + leadingCallback: () => _buildConfirmationBottomSheet( context: context, onConfirm: () => { context.pop(), diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index 4dbe0ca..aa0cd72 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -103,7 +103,7 @@ class _TriageScreenState extends State { // context.pop(), // }), - onLeadingTapped: () => context.pop(), + leadingCallback: () => context.pop(), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/presentation/symptoms_checker/user_info_selection.dart b/lib/presentation/symptoms_checker/user_info_selection.dart new file mode 100644 index 0000000..2fb6cc9 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.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/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; + +class UserInfoSelectionScreen extends StatelessWidget { + const UserInfoSelectionScreen({super.key}); + + _buildEditInfoTile({ + required String leadingIcon, + required String title, + required String subTitle, + required VoidCallback onTap, + required String trailingIcon, + required BuildContext context, + Color? iconColor, + }) { + return InkWell( + onTap: onTap, + child: Row( + children: [ + Expanded( + child: Row( + children: [ + Container( + height: 40.h, + width: 40.h, + margin: EdgeInsets.only(right: 10.h), + padding: EdgeInsets.all(8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 12.r, color: AppColors.greyColor), + child: Utils.buildSvgWithAssets(icon: leadingIcon, iconColor: iconColor)), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.toText16(weight: FontWeight.w500), + subTitle.toText14(color: AppColors.primaryRedColor, weight: FontWeight.w500), + ], + ), + ], + ), + ), + Utils.buildSvgWithAssets(icon: trailingIcon, height: 24.h, width: 24.h), + ], + ), + ); + } + + Widget _getDivider() { + return Divider( + color: AppColors.dividerColor, + ).paddingSymmetrical(0, 16.h); + } + + @override + Widget build(BuildContext context) { + AppState appState = getIt.get(); + + String name = ""; + if (appState.isAuthenticated) { + name = "${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!} "; + } else { + name = "Guest"; + } + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: "Symptoms Checker".needTranslation, + isLeading: true, + child: SingleChildScrollView( + child: Column( + children: [ + Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 16.w), + child: Column( + children: [ + "Hi $name, Is your information up to date?".needTranslation.toText18( + weight: FontWeight.w600, + color: AppColors.textColor, + ), + SizedBox(height: 24.h), + _buildEditInfoTile( + context: context, + leadingIcon: AppAssets.genderIcon, + title: "Gender".needTranslation, + subTitle: "Male".needTranslation, + onTap: () {}, + trailingIcon: AppAssets.edit_icon, + ), + _getDivider(), + _buildEditInfoTile( + context: context, + leadingIcon: AppAssets.calendar, + title: "Age".needTranslation, + subTitle: "32 Years", + iconColor: AppColors.greyTextColor, + onTap: () {}, + trailingIcon: AppAssets.edit_icon, + ), + _getDivider(), + _buildEditInfoTile( + context: context, + leadingIcon: AppAssets.rulerIcon, + title: "Height".needTranslation, + subTitle: "17 8cm", + onTap: () {}, + trailingIcon: AppAssets.edit_icon, + ), + _getDivider(), + _buildEditInfoTile( + context: context, + leadingIcon: AppAssets.weightScale, + title: "Weight".needTranslation, + subTitle: "88 kg", + onTap: () {}, + trailingIcon: AppAssets.edit_icon, + ), + ], + ), + ), + ], + ).paddingAll(24.w), + ), + ), + ), + _buildBottomCard(context), + ], + ), + ); + } + + Widget _buildBottomCard(BuildContext context) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 24.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: "No, Edit all".needTranslation, + icon: AppAssets.edit_icon, + iconColor: AppColors.primaryRedColor, + onPressed: () => context.navigateWithName(AppRoutes.userInfoFlowManager), + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 16.f, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: CustomButton( + text: "Yes, It is".needTranslation, + icon: AppAssets.tickIcon, + iconColor: AppColors.whiteColor, + onPressed: () => () {}, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + ), + ), + ], + ), + ], + ).paddingSymmetrical(24.w, 0), + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart b/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart new file mode 100644 index 0000000..6433b93 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:provider/provider.dart'; + +/// Age selection page content +class AgeSelectionPage extends StatelessWidget { + final int? selectedAge; + final Function(int) onAgeSelected; + + const AgeSelectionPage({ + super.key, + required this.selectedAge, + required this.onAgeSelected, + }); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: EdgeInsets.all(24.w), + child: Consumer( + builder: (BuildContext context, symptomsViewModel, Widget? child) { + return Column( + children: [ + "What is your gender?".needTranslation.toText18(weight: FontWeight.w600, color: AppColors.textColor), + SizedBox(height: 70.h), + "< Age Widget Here >".needTranslation.toText18(weight: FontWeight.w600, color: AppColors.greyTextColor), + ], + ); + }, + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/pages/gender_selection_page.dart b/lib/presentation/symptoms_checker/user_info_selection/pages/gender_selection_page.dart new file mode 100644 index 0000000..85cb6e2 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/pages/gender_selection_page.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:provider/provider.dart'; + +/// Gender selection page content +class GenderSelectionPage extends StatelessWidget { + final String? selectedGender; + final Function(String) onGenderSelected; + + GenderSelectionPage({ + super.key, + required this.selectedGender, + required this.onGenderSelected, + }); + + _buildGenderOption(String iconPng, String label, bool isSelected) { + return Container( + height: 160.h, + width: 160.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, borderRadius: 24.r, side: isSelected ? BorderSide(color: AppColors.primaryRedColor, width: 2.5) : null), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Utils.buildImgWithAssets(icon: iconPng, height: 80.h, width: 80.h, fit: BoxFit.contain), + SizedBox(height: 8.h), + label.toText16( + weight: FontWeight.w500, + ) + ], + ), + ); + } + + final genders = ["Male", "Female"]; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: EdgeInsets.all(24.w), + child: Consumer( + builder: (BuildContext context, symptomsViewModel, Widget? child) { + return Column( + children: [ + "What is your gender?".needTranslation.toText18(weight: FontWeight.w600, color: AppColors.textColor), + SizedBox(height: 70.h), + Row( + children: [ + Expanded( + child: InkWell( + onTap: () => onGenderSelected(genders[0]), + child: _buildGenderOption(AppAssets.maleIcon, "Male".needTranslation, symptomsViewModel.selectedGender == genders[0]), + ), + ), + SizedBox(width: 16.w), + Expanded( + child: InkWell( + onTap: () => onGenderSelected(genders[1]), + child: _buildGenderOption(AppAssets.femaleIcon, "Female".needTranslation, symptomsViewModel.selectedGender == genders[1]), + )) + ], + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/pages/height_selection_page.dart b/lib/presentation/symptoms_checker/user_info_selection/pages/height_selection_page.dart new file mode 100644 index 0000000..e10aca3 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/pages/height_selection_page.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +/// Height selection page content +class HeightSelectionPage extends StatefulWidget { + final double height; + final bool isCm; + final Function(double, bool) onHeightChanged; + + const HeightSelectionPage({ + super.key, + required this.height, + required this.isCm, + required this.onHeightChanged, + }); + + @override + State createState() => _HeightSelectionPageState(); +} + +class _HeightSelectionPageState extends State { + late double heightValue; + + @override + void initState() { + super.initState(); + heightValue = widget.height; + } + + Widget _unitSelector() { + return Container( + height: 54.h, + padding: EdgeInsets.all(4.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(10.r), + ), + child: LayoutBuilder( + builder: (context, constraints) { + final tabWidth = (constraints.maxWidth - 8.w) / 2; + return Stack( + children: [ + // Animated sliding indicator + AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + width: tabWidth, + height: constraints.maxHeight, + margin: EdgeInsets.only(left: widget.isCm ? 0 : tabWidth + 8.w), + decoration: BoxDecoration( + color: AppColors.bottomNAVBorder, + borderRadius: BorderRadius.circular(7.r), + ), + ), + // Tab buttons + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + if (!widget.isCm) { + // Convert from FT to CM (1 ft = 30.48 cm) + final convertedHeight = heightValue * 30.48; + widget.onHeightChanged(convertedHeight, true); + } + }, + child: Container( + alignment: Alignment.center, + color: Colors.transparent, + child: Text( + 'CM', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14.f, + color: widget.isCm ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6), + ), + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () { + if (widget.isCm) { + // Convert from CM to FT (1 cm = 0.0328084 ft) + final convertedHeight = heightValue / 30.48; + widget.onHeightChanged(convertedHeight, false); + } + }, + child: Container( + alignment: Alignment.center, + color: Colors.transparent, + child: Text( + 'FT', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14.f, + color: !widget.isCm ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6), + ), + ), + ), + ), + ), + ], + ), + ], + ); + }, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + Center( + child: Text( + 'How tall are you?'.needTranslation, + style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor), + ), + ), + SizedBox(height: 24.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: _unitSelector(), + ), + SizedBox(height: 90.h), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + heightValue.round().toString(), + style: TextStyle(fontSize: 100.f, color: AppColors.textColor, height: 1), + ), + SizedBox(width: 8.w), + Text(widget.isCm ? 'cm' : 'ft', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 24.f)).paddingOnly(bottom: 10.h, left: 8.w), + ], + ), + ], + ); + } +} + +class _RulerPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paintTick = Paint()..color = const Color(0xFF222222); + final paintSmall = Paint()..color = const Color(0xFF222222).withValues(alpha: 0.6); + + final width = size.width; + final start = 120; + final end = 210; + final steps = end - start; + for (int i = 0; i <= steps; i++) { + final x = (i / steps) * width; + if (i % 10 == 0) { + canvas.drawLine(Offset(x, size.height * 0.1), Offset(x, size.height * 0.6), paintTick); + } else if (i % 5 == 0) { + canvas.drawLine(Offset(x, size.height * 0.2), Offset(x, size.height * 0.5), paintSmall); + } else { + canvas.drawLine(Offset(x, size.height * 0.35), Offset(x, size.height * 0.5), paintSmall); + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/pages/weight_selection_page.dart b/lib/presentation/symptoms_checker/user_info_selection/pages/weight_selection_page.dart new file mode 100644 index 0000000..0319f4b --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/pages/weight_selection_page.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +/// Weight selection page content +class WeightSelectionPage extends StatefulWidget { + final double weight; + final bool isKg; + final Function(double, bool) onWeightChanged; + + const WeightSelectionPage({ + super.key, + required this.weight, + required this.isKg, + required this.onWeightChanged, + }); + + @override + State createState() => _WeightSelectionPageState(); +} + +class _WeightSelectionPageState extends State { + late double weightValue; + + @override + void initState() { + super.initState(); + weightValue = widget.weight; + } + + Widget _unitSelector() { + return Container( + height: 54.h, + padding: EdgeInsets.all(4.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(10.r), + ), + child: LayoutBuilder( + builder: (context, constraints) { + final tabWidth = (constraints.maxWidth - 8.w) / 2; + return Stack( + children: [ + // Animated sliding indicator + AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + width: tabWidth, + height: constraints.maxHeight, + margin: EdgeInsets.only(left: widget.isKg ? 0 : tabWidth + 8.w), + decoration: BoxDecoration( + color: AppColors.bottomNAVBorder, + borderRadius: BorderRadius.circular(7.r), + ), + ), + // Tab buttons + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + if (!widget.isKg) { + // Convert from LBS to KG (1 lb = 0.453592 kg) + final convertedWeight = weightValue / 2.20462; + widget.onWeightChanged(convertedWeight, true); + } + }, + child: Container( + alignment: Alignment.center, + color: Colors.transparent, + child: Text( + 'KG', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14.f, + color: widget.isKg ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6), + ), + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () { + if (widget.isKg) { + // Convert from KG to LBS (1 kg = 2.20462 lbs) + final convertedWeight = weightValue * 2.20462; + widget.onWeightChanged(convertedWeight, false); + } + }, + child: Container( + alignment: Alignment.center, + color: Colors.transparent, + child: Text( + 'LBS', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14.f, + color: !widget.isKg ? AppColors.textColor : AppColors.textColor.withValues(alpha: 0.6), + ), + ), + ), + ), + ), + ], + ), + ], + ); + }, + ), + ); + } + + Widget _weightSlider() { + return Column( + children: [ + SizedBox(height: 18.h), + SizedBox(height: 12.h), + SizedBox( + height: 80.h, + child: Row( + children: [ + Expanded(child: Container()), + Expanded( + flex: 6, + child: Stack( + alignment: Alignment.center, + children: [ + Positioned.fill( + child: CustomPaint( + painter: _WeightRulerPainter(), + ), + ), + SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: 6.h, + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 0), + overlayShape: const RoundSliderOverlayShape(overlayRadius: 0), + activeTrackColor: AppColors.primaryRedColor, + inactiveTrackColor: AppColors.lightRedButtonColor, + ), + child: Slider( + min: 30, + max: 150, + value: weightValue, + onChanged: (v) { + setState(() => weightValue = v); + widget.onWeightChanged(v, widget.isKg); + }, + ), + ), + Positioned( + right: 0, + child: Container( + width: 18.w, + height: 18.h, + decoration: BoxDecoration( + color: AppColors.primaryRedColor, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(4.r), + ), + ), + ), + ], + ), + ), + Expanded(child: Container()), + ], + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + Center( + child: Text( + 'What is your weight?'.needTranslation, + style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor), + ), + ), + SizedBox(height: 20.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: _unitSelector(), + ), + SizedBox(height: 90.h), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + weightValue.round().toString(), + style: TextStyle(fontSize: 100.f, color: AppColors.textColor, height: 1), + ), + SizedBox(width: 8.w), + Text(widget.isKg ? 'kg' : 'lbs', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 24.f)).paddingOnly(bottom: 10.h, left: 8.w), + ], + ), + ], + ); + } +} + +class _WeightRulerPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paintTick = Paint()..color = const Color(0xFF222222); + final paintSmall = Paint()..color = const Color(0xFF222222).withValues(alpha: 0.6); + + final width = size.width; + final start = 30; + final end = 150; + final steps = end - start; + for (int i = 0; i <= steps; i++) { + final x = (i / steps) * width; + if (i % 10 == 0) { + canvas.drawLine(Offset(x, size.height * 0.1), Offset(x, size.height * 0.6), paintTick); + } else if (i % 5 == 0) { + canvas.drawLine(Offset(x, size.height * 0.2), Offset(x, size.height * 0.5), paintSmall); + } else { + canvas.drawLine(Offset(x, size.height * 0.35), Offset(x, size.height * 0.5), paintSmall); + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart b/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart new file mode 100644 index 0000000..43d485a --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart @@ -0,0 +1,209 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/pages/age_selection_page.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/pages/gender_selection_page.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/pages/height_selection_page.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/pages/weight_selection_page.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:provider/provider.dart'; + +/// Manages the user info selection flow with PageView +/// Only the page content changes, header and footer remain fixed +class UserInfoFlowManager extends StatefulWidget { + const UserInfoFlowManager({super.key}); + + @override + State createState() => _UserInfoFlowManagerState(); +} + +class _UserInfoFlowManagerState extends State { + final PageController _pageController = PageController(); + late SymptomsCheckerViewModel _viewModel; + + // Page titles + final List _pageTitles = [ + "Your Gender", + "Your Birth Date", + "Your Height", + "Your Weight", + ]; + + @override + void initState() { + super.initState(); + _viewModel = context.read(); + // _viewModel.resetUserInfo(); + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + void _onNext() { + if (_viewModel.userInfoCurrentPage < 3) { + _viewModel.nextUserInfoPage(); + _pageController.animateToPage( + _viewModel.userInfoCurrentPage, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } else { + // Submit and navigate to next screen + _submitUserInfo(); + } + } + + void _onPrevious() { + if (_viewModel.userInfoCurrentPage > 0) { + _viewModel.previousUserInfoPage(); + _pageController.animateToPage( + _viewModel.userInfoCurrentPage, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } else { + context.pop(); + } + } + + void _submitUserInfo() { + final userInfo = _viewModel.getUserInfoData(); + + // Log user info + log('User Info Submitted:'); + log('Gender: ${userInfo['gender']}'); + log('Age: ${userInfo['age']}'); + log('Height: ${userInfo['height']} ${userInfo['heightUnit']}'); + log('Weight: ${userInfo['weight']} ${userInfo['weightUnit']}'); + + // TODO: Save user info to backend/storage + + // Navigate to symptoms checker or next screen + context.pop(); + } + + Widget _buildProgressBar(int currentPage) { + return Row( + children: List.generate(4, (index) { + final isActive = index <= currentPage; + return Expanded( + child: Container( + height: 4.h, + margin: EdgeInsets.symmetric(horizontal: 6.w), + decoration: BoxDecoration( + color: isActive ? AppColors.primaryRedColor : AppColors.greyLightColor, + borderRadius: BorderRadius.circular(8.r), + ), + ), + ); + }), + ); + } + + Widget _buildStickyBottomCard(bool isLastPage) { + return Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.vertical(top: Radius.circular(24.r)), + ), + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 16.h), + child: SafeArea( + top: false, + child: Row( + children: [ + Expanded( + child: CustomButton( + text: "Previous".needTranslation, + onPressed: _onPrevious, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 16.f, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: CustomButton( + text: isLastPage ? "Submit".needTranslation : "Next".needTranslation, + onPressed: _onNext, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + ), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, viewModel, child) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: _pageTitles[viewModel.userInfoCurrentPage].needTranslation, + isLeading: true, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + _buildProgressBar(viewModel.userInfoCurrentPage), + SizedBox(height: 24.h), + SizedBox( + height: 600.h, + child: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), // Disable swipe + onPageChanged: (index) { + viewModel.setUserInfoPage(index); + }, + children: [ + GenderSelectionPage( + selectedGender: viewModel.selectedGender, + onGenderSelected: viewModel.setGender, + ), + AgeSelectionPage( + selectedAge: viewModel.selectedAge, + onAgeSelected: viewModel.setAge, + ), + HeightSelectionPage( + height: viewModel.selectedHeight ?? 178, + isCm: viewModel.isHeightCm, + onHeightChanged: viewModel.setHeight, + ), + WeightSelectionPage( + weight: viewModel.selectedWeight ?? 70, + isKg: viewModel.isWeightKg, + onWeightChanged: viewModel.setWeight, + ), + ], + ), + ), + ], + ), + ), + ), + _buildStickyBottomCard(viewModel.isUserInfoLastPage), + ], + ), + ); + }, + ); + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_progress_bar.dart b/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_progress_bar.dart new file mode 100644 index 0000000..6e6d762 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_progress_bar.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +/// Progress bar widget showing the current step in user info selection flow +/// Total steps: 4 (Gender -> Age -> Height -> Weight) +class UserInfoProgressBar extends StatelessWidget { + final int currentStep; + final int totalSteps; + + const UserInfoProgressBar({ + super.key, + required this.currentStep, + this.totalSteps = 4, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: List.generate(totalSteps, (index) { + final isActive = index < currentStep; + return Expanded( + child: Container( + height: 4.h, + margin: EdgeInsets.symmetric(horizontal: 6.w), + decoration: BoxDecoration( + color: isActive ? AppColors.primaryRedColor : AppColors.greyLightColor, + borderRadius: BorderRadius.circular(8.r), + ), + ), + ); + }), + ); + } +} + diff --git a/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_selection_scaffold.dart b/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_selection_scaffold.dart new file mode 100644 index 0000000..3390c31 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_selection_scaffold.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/user_info_progress_bar.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/widgets/user_info_sticky_bottom_card.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; + +/// Base scaffold for user info selection flow pages +/// Provides consistent layout with progress bar and sticky bottom card +class UserInfoSelectionScaffold extends StatelessWidget { + final String title; + final int currentStep; + final Widget child; + final VoidCallback? onPrevious; + final VoidCallback? onNext; + final bool showPrevious; + final String? nextButtonText; + final bool isScrollable; + + const UserInfoSelectionScaffold({ + super.key, + required this.title, + required this.currentStep, + required this.child, + this.onPrevious, + this.onNext, + this.showPrevious = true, + this.nextButtonText, + this.isScrollable = true, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: title, + isLeading: true, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + UserInfoProgressBar(currentStep: currentStep), + SizedBox(height: 24.h), + isScrollable + ? SingleChildScrollView( + child: child, + ) + : child, + ], + ), + ), + ), + UserInfoStickyBottomCard( + onPrevious: onPrevious, + onNext: onNext, + showPrevious: showPrevious, + nextButtonText: nextButtonText, + ), + ], + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_sticky_bottom_card.dart b/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_sticky_bottom_card.dart new file mode 100644 index 0000000..10fd934 --- /dev/null +++ b/lib/presentation/symptoms_checker/user_info_selection/widgets/user_info_sticky_bottom_card.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; + +/// Sticky bottom card with Previous/Next navigation buttons +class UserInfoStickyBottomCard extends StatelessWidget { + final VoidCallback? onPrevious; + final VoidCallback? onNext; + final bool showPrevious; + final String? nextButtonText; + + const UserInfoStickyBottomCard({ + super.key, + this.onPrevious, + this.onNext, + this.showPrevious = true, + this.nextButtonText, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 16.h), + Row( + children: [ + if (showPrevious) ...[ + Expanded( + child: CustomButton( + text: "Previous".needTranslation, + onPressed: onPrevious ?? () => context.pop(), + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.11), + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 16.f, + ), + ), + SizedBox(width: 12.w), + ], + Expanded( + child: CustomButton( + text: nextButtonText ?? "Next".needTranslation, + onPressed: onNext ?? () {}, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + ), + ), + ], + ), + SizedBox(height: 24.h), + ], + ).paddingSymmetrical(24.w, 0), + ); + } +} + diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index f73eba3..183c2a0 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -3,6 +3,7 @@ import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; import 'package:hmg_patient_app_new/presentation/authentication/register.dart'; import 'package:hmg_patient_app_new/presentation/authentication/register_step2.dart'; import 'package:hmg_patient_app_new/presentation/blood_donation/blood_donation_page.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/comprehensive_checkup_page.dart'; import 'package:hmg_patient_app_new/presentation/e_referral/new_e_referral.dart'; import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; @@ -14,6 +15,8 @@ import 'package:hmg_patient_app_new/presentation/symptoms_checker/risk_factors_s import 'package:hmg_patient_app_new/presentation/symptoms_checker/suggestions_screen.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/symptoms_selector_screen.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/triage_screen.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/user_info_selection/user_info_flow_manager.dart'; import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; @@ -30,14 +33,21 @@ class AppRoutes { static const String zoomCallPage = '/zoomCallPage'; static const String bloodDonationPage = '/bloodDonationPage'; + //appointments + static const String bookAppointmentPage = '/bookAppointmentPage'; + // Symptoms Checker static const String organSelectorPage = '/organSelectorPage'; - static const String symptomsCheckerScreen = '/symptomsCheckerScreen'; + static const String symptomsSelectorScreen = '/symptomsCheckerScreen'; static const String suggestionsScreen = '/suggestionsScreen'; static const String riskFactorsScreen = '/riskFactorsScreen'; static const String possibleConditionsScreen = '/possibleConditionsScreen'; static const String triageScreen = '/triageProgressScreen'; + //UserInfoSelection + static const String userInfoSelection = '/userInfoSelection'; + static const String userInfoFlowManager = '/userInfoFlowManager'; + static Map get routes => { initialRoute: (context) => SplashPage(), loginScreen: (context) => LoginScreen(), @@ -50,12 +60,16 @@ class AppRoutes { comprehensiveCheckupPage: (context) => ComprehensiveCheckupPage(), homeHealthCarePage: (context) => HhcProceduresPage(), organSelectorPage: (context) => OrganSelectorPage(), - symptomsCheckerScreen: (context) => SymptomsSelectorScreen(), + symptomsSelectorScreen: (context) => SymptomsSelectorScreen(), riskFactorsScreen: (context) => RiskFactorsScreen(), suggestionsScreen: (context) => SuggestionsScreen(), possibleConditionsScreen: (context) => PossibleConditionsScreen(), - triageScreen: (context) => TriageScreen() - zoomCallPage: (context) => CallScreen(), - bloodDonationPage: (context) => BloodDonationPage() + triageScreen: (context) => TriageScreen(), + bloodDonationPage: (context) => BloodDonationPage(), + bookAppointmentPage: (context) => BookAppointmentPage(), + userInfoSelection: (context) => UserInfoSelectionScreen(), + userInfoFlowManager: (context) => UserInfoFlowManager(), + + // }; } diff --git a/lib/widgets/appbar/collapsing_list_view.dart b/lib/widgets/appbar/collapsing_list_view.dart index 3e20689..ad8e743 100644 --- a/lib/widgets/appbar/collapsing_list_view.dart +++ b/lib/widgets/appbar/collapsing_list_view.dart @@ -26,7 +26,7 @@ class CollapsingListView extends StatelessWidget { Widget? trailing; bool isClose; bool isLeading; - VoidCallback? onLeadingTapped; + VoidCallback? leadingCallback; CollapsingListView({ super.key, @@ -42,7 +42,7 @@ class CollapsingListView extends StatelessWidget { this.requests, this.isLeading = true, this.trailing, - this.onLeadingTapped, + this.leadingCallback, }); @override @@ -69,8 +69,8 @@ class CollapsingListView extends StatelessWidget { icon: Utils.buildSvgWithAssets(icon: isClose ? AppAssets.closeBottomNav : AppAssets.arrow_back, width: 32.h, height: 32.h), padding: EdgeInsets.only(left: 12), onPressed: () { - if (onLeadingTapped != null) { - onLeadingTapped!(); + if (leadingCallback != null) { + leadingCallback!(); } else { context.pop(); } diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index c1a38ab..7992ece 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -215,8 +215,11 @@ class TextInputWidget extends StatelessWidget { language: appState.getLanguageCode()!, initialDate: DateTime.now(), fontFamily: appState.getLanguageCode() == "ar" ? "GESSTwo" : "Poppins", - okWidget: Padding(padding: EdgeInsets.only(right: 8.h), child: Utils.buildSvgWithAssets(icon: AppAssets.confirm, width: 24.h, height: 24.h)), - cancelWidget: Padding(padding: EdgeInsets.only(right: 8.h), child: Utils.buildSvgWithAssets(icon: AppAssets.cancel, iconColor: Colors.white, width: 24.h, height: 24.h)), + okWidget: + Padding(padding: EdgeInsets.only(right: 8.h), child: Utils.buildSvgWithAssets(icon: AppAssets.confirm, width: 24.h, height: 24.h)), + cancelWidget: Padding( + padding: EdgeInsets.only(right: 8.h), + child: Utils.buildSvgWithAssets(icon: AppAssets.cancel, iconColor: Colors.white, width: 24.h, height: 24.h)), onCalendarTypeChanged: (bool value) { isGregorian = value; }); @@ -276,7 +279,12 @@ class TextInputWidget extends StatelessWidget { decoration: InputDecoration( isDense: true, hintText: hintText, - hintStyle: TextStyle(fontSize: 14.f, height: 21 / 16, fontWeight: FontWeight.w500, color: hintColor != null ? AppColors.textColor : Color(0xff898A8D), letterSpacing: -0.75), + hintStyle: TextStyle( + fontSize: 14.f, + height: 21 / 16, + fontWeight: FontWeight.w500, + color: hintColor != null ? AppColors.textColor : Color(0xff898A8D), + letterSpacing: -0.75), prefixIconConstraints: BoxConstraints(minWidth: 30.h), prefixIcon: prefix == null ? null : "+${prefix!}".toText14(letterSpacing: -1, color: AppColors.textColor, weight: FontWeight.w500), contentPadding: EdgeInsets.zero,