diff --git a/assets/images/svg/home_bordered.svg b/assets/images/svg/home_bordered.svg new file mode 100644 index 0000000..b9f6291 --- /dev/null +++ b/assets/images/svg/home_bordered.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/refresh.svg b/assets/images/svg/refresh.svg new file mode 100644 index 0000000..6a84970 --- /dev/null +++ b/assets/images/svg/refresh.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index eb6f74c..ab2bfb2 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -192,6 +192,8 @@ class AppAssets { // Symptoms Checker static const String bodyIcon = '$svgBasePath/body_icon.svg'; static const String rotateIcon = '$svgBasePath/rotate_icon.svg'; + static const String refreshIcon = '$svgBasePath/refresh.svg'; + static const String homeBorderedIcon = '$svgBasePath/home_bordered.svg'; // PNGS // static const String hmgLogo = '$pngBasePath/hmg_logo.png'; diff --git a/lib/core/enums.dart b/lib/core/enums.dart index e318401..6754e8c 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -241,3 +241,7 @@ extension ServiceTypeEnumExt on ServiceTypeEnum { } } } + +// SymptomsChecker + +enum PossibleConditionsSeverityEnum { seekMedicalAdvice, monitorOnly, emergency } diff --git a/lib/features/emergency_services/emergency_services_view_model.dart b/lib/features/emergency_services/emergency_services_view_model.dart index ad72ef4..0f264c5 100644 --- a/lib/features/emergency_services/emergency_services_view_model.dart +++ b/lib/features/emergency_services/emergency_services_view_model.dart @@ -3,12 +3,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart' as GMSMapServices; 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'; import 'package:hmg_patient_app_new/core/location_util.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; -import 'package:hmg_patient_app_new/core/utils/doctor_response_mapper.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/string_extensions.dart'; @@ -16,29 +14,24 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/authenticated_user_resp_model.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_repo.dart'; -import 'package:hmg_patient_app_new/features/emergency_services/models/OrderDisplay.dart'; -import 'package:hmg_patient_app_new/features/emergency_services/models/request_model/RRTRequestModel.dart'; -import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/EROnlineCheckInPaymentDetailsResponse.dart'; -import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/RRTServiceData.dart'; -import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/AmbulanceCallingPlace.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/OrderDisplay.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/request_model/PatientER_RC.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/request_model/RRTRequestModel.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/AmbulanceRequestOrdersModel.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/EROnlineCheckInPaymentDetailsResponse.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/PatientERTransportationMethod.dart' show PatientERTransportationMethod; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/ProjectAvgERWaitingTime.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/RRTServiceData.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/resp_model/rrt_procedures_response_model.dart'; import 'package:hmg_patient_app_new/features/location/PlaceDetails.dart' show PlaceDetails; import 'package:hmg_patient_app_new/features/location/PlacePrediction.dart'; +import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_repo.dart'; -import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; -import 'package:hmg_patient_app_new/presentation/emergency_services/RRT/rrt_map_screen.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/RRT/rrt_request_type_select.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/RRT/terms_and_condition.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/call_ambulance_page.dart'; @@ -85,7 +78,6 @@ class EmergencyServicesViewModel extends ChangeNotifier { List nearestERList = []; List nearestERFilteredList = []; - List? hospitalList; List? hmgHospitalList; List? hmcHospitalList; @@ -104,7 +96,6 @@ class EmergencyServicesViewModel extends ChangeNotifier { AmbulanceCallingPlace callingPlace = AmbulanceCallingPlace.FROM_HOSPITAL; AmbulanceDirection ambulanceDirection = AmbulanceDirection.ONE_WAY; - bool patientHasAdvanceERBalance = false; bool isERBookAppointment = false; late EROnlineCheckInPaymentDetailsResponse erOnlineCheckInPaymentDetailsResponse; @@ -118,14 +109,11 @@ class EmergencyServicesViewModel extends ChangeNotifier { RRTServiceData? ordersRRT; TextEditingController rrtNotes = TextEditingController(); - List allOrders = []; List orderDisplayList = []; - bool historyLoading= false; + bool historyLoading = false; OrderDislpay currentlyDisplayedOrder = OrderDislpay.ALL; - - setSelectedRRTProcedure(RRTProceduresResponseModel procedure) { selectedRRTProcedure = procedure; notifyListeners(); @@ -161,7 +149,6 @@ class EmergencyServicesViewModel extends ChangeNotifier { String? termsAndConditions; Future getRRTProcedures({Function(dynamic)? onSuccess, Function(String)? onError}) async { - print("the app state is ${appState.isAuthenticated}"); if (!appState.isAuthenticated) { dialogService.showErrorBottomSheet( @@ -797,8 +784,8 @@ class EmergencyServicesViewModel extends ChangeNotifier { historyLoading = false; ambulanceOrders = apiResponse.data; allOrders.clear(); - allOrders.addAll(ambulanceOrders??[]); - allOrders.addAll(ordersRRT?.completedOrders??[]); + allOrders.addAll(ambulanceOrders ?? []); + allOrders.addAll(ordersRRT?.completedOrders ?? []); changeOrderDisplayItems(OrderDislpay.ALL); notifyListeners(); }, @@ -889,8 +876,7 @@ class EmergencyServicesViewModel extends ChangeNotifier { }); } - - RRTRequestModel createRRTRequest(GeocodeResult? result, PlaceDetails? place, PlacePrediction? placePrediction){ + RRTRequestModel createRRTRequest(GeocodeResult? result, PlaceDetails? place, PlacePrediction? placePrediction) { AuthenticatedUser? user = appState.getAuthenticatedUser(); if (user == null) throw Exception("Authentication Required to Continue"); @@ -898,7 +884,6 @@ class EmergencyServicesViewModel extends ChangeNotifier { Procedures procedures = new Procedures(); rrtRequestModel.procedures = []; - procedures.serviceID = selectedRRTProcedure?.procedureID; rrtRequestModel.latitude = ((result?.geometry.location.lat) ?? place?.lat); @@ -920,7 +905,7 @@ class EmergencyServicesViewModel extends ChangeNotifier { notifyListeners(); } - FutureOr submitRRTRequest(GeocodeResult? result, PlaceDetails? place, PlacePrediction? placePrediction) async { + FutureOr submitRRTRequest(GeocodeResult? result, PlaceDetails? place, PlacePrediction? placePrediction) async { RRTRequestModel request = createRRTRequest(result, place, placePrediction); navServices.push(CustomPageRoute(page: RequestingServicesPage())); @@ -929,9 +914,9 @@ class EmergencyServicesViewModel extends ChangeNotifier { navServices.pushAndRemoveUntil( CustomPageRoute( page: TrackingScreen( - isRRTOrder: true, - state: OrderTrackingState.failed, - )), + isRRTOrder: true, + state: OrderTrackingState.failed, + )), ModalRoute.withName("/EmergencyServicesPage")); }, (success) { getRRTOrders(shouldNavigateToTrackingScreen: true); @@ -939,7 +924,7 @@ class EmergencyServicesViewModel extends ChangeNotifier { } Future getRRTOrders({bool shouldNavigateToTrackingScreen = false, bool showLoader = false}) async { - if(shouldNavigateToTrackingScreen == false && showLoader ) { + if (shouldNavigateToTrackingScreen == false && showLoader) { LoaderBottomSheet.showLoader(loadingText: "Fetching Orders"); } historyLoading = true; @@ -947,39 +932,40 @@ class EmergencyServicesViewModel extends ChangeNotifier { int? id = appState.getAuthenticatedUser()?.patientId; var response = await emergencyServicesRepo.getRRTOrders(id: id); - if(shouldNavigateToTrackingScreen == false && showLoader ) { - LoaderBottomSheet.hideLoader();} + if (shouldNavigateToTrackingScreen == false && showLoader) { + LoaderBottomSheet.hideLoader(); + } response.fold( - (failure) async { - historyLoading = false; - notifyListeners(); + (failure) async { + historyLoading = false; + notifyListeners(); if (shouldNavigateToTrackingScreen) { - navServices.pushAndRemoveUntil(CustomPageRoute(page: TrackingScreen(isRRTOrder: true,state: OrderTrackingState.waitingForCall)), ModalRoute.withName("/EmergencyServicesPage")); + navServices.pushAndRemoveUntil(CustomPageRoute(page: TrackingScreen(isRRTOrder: true, state: OrderTrackingState.waitingForCall)), + ModalRoute.withName("/EmergencyServicesPage")); } }, - (apiResponse) { + (apiResponse) { if (shouldNavigateToTrackingScreen) { navServices.pushAndRemoveUntil( CustomPageRoute( page: TrackingScreen( - state: OrderTrackingState.waitingForCall, - isRRTOrder: true, - rrtOrder: apiResponse.data?.pendingOrders.first, - )), + state: OrderTrackingState.waitingForCall, + isRRTOrder: true, + rrtOrder: apiResponse.data?.pendingOrders.first, + )), ModalRoute.withName("/EmergencyServicesPage")); } historyLoading = false; ordersRRT = apiResponse.data; allOrders.clear(); - allOrders.addAll(ambulanceOrders??[]); - allOrders.addAll(ordersRRT?.completedOrders??[]); + allOrders.addAll(ambulanceOrders ?? []); + allOrders.addAll(ordersRRT?.completedOrders ?? []); changeOrderDisplayItems(OrderDislpay.ALL); notifyListeners(); }, ); } - FutureOr cancelRRTOrder(int? orderID, {bool shouldPop = false}) async { dialogService.showCommonBottomSheetWithoutH( message: "Do you want to cancel the request".needTranslation, @@ -998,52 +984,54 @@ class EmergencyServicesViewModel extends ChangeNotifier { }); } - void changeOrderDisplayItems(OrderDislpay currentlyDisplayedOrder){ + void changeOrderDisplayItems(OrderDislpay currentlyDisplayedOrder) { this.currentlyDisplayedOrder = currentlyDisplayedOrder; - switch(currentlyDisplayedOrder){ + switch (currentlyDisplayedOrder) { case OrderDislpay.ALL: - orderDisplayList = allOrders; - break; + orderDisplayList = allOrders; + break; case OrderDislpay.RRT: orderDisplayList = ordersRRT?.completedOrders ?? []; break; case OrderDislpay.AMBULANCE: - orderDisplayList = ambulanceOrders??[]; + orderDisplayList = ambulanceOrders ?? []; break; } notifyListeners(); } - void openRRT(){ + void openRRT() { print("the app state is ${appState.isAuthenticated}"); if (appState.isAuthenticated) { - if(agreedToTermsAndCondition == false){ - dialogService.showErrorBottomSheet(message: "You Need To Agree To Terms And Conditions".needTranslation, onOkPressed: (){ - if(navServices.context == null ) return; - showCommonBottomSheetWithoutHeight( - navServices.context!, - padding: EdgeInsets.only(top: 24.h), - titleWidget: Transform.flip( - flipX: isArabic, - child: Utils.buildSvgWithAssets( - icon: AppAssets.arrow_back, - iconColor: Color(0xff2B353E), - fit: BoxFit.contain, - ), - ).onPress(() { - navServices.pop(); - }), - // title: "Rapid Response Team (RRT)".needTranslation, - child: RrtRequestTypeSelect(), - isFullScreen: false, - isCloseButtonVisible: true, - hasBottomPadding: false, - backgroundColor: AppColors.bottomSheetBgColor, - callBackFunc: () { - navServices.pop(); - }, - ); - }); + if (agreedToTermsAndCondition == false) { + dialogService.showErrorBottomSheet( + message: "You Need To Agree To Terms And Conditions".needTranslation, + onOkPressed: () { + if (navServices.context == null) return; + showCommonBottomSheetWithoutHeight( + navServices.context!, + padding: EdgeInsets.only(top: 24.h), + titleWidget: Transform.flip( + flipX: isArabic, + child: Utils.buildSvgWithAssets( + icon: AppAssets.arrow_back, + iconColor: Color(0xff2B353E), + fit: BoxFit.contain, + ), + ).onPress(() { + navServices.pop(); + }), + // title: "Rapid Response Team (RRT)".needTranslation, + child: RrtRequestTypeSelect(), + isFullScreen: false, + isCloseButtonVisible: true, + hasBottomPadding: false, + backgroundColor: AppColors.bottomSheetBgColor, + callBackFunc: () { + navServices.pop(); + }, + ); + }); return; } placeValueInController(); @@ -1051,7 +1039,7 @@ class EmergencyServicesViewModel extends ChangeNotifier { isShowConfirmDialog: true, onSuccess: (position) async { updateBottomSheetState(BottomSheetType.FIXED); - bool result = await navServices.push( + bool result = await navServices.push( CustomPageRoute( page: MapUtilityScreen( confirmButtonString: "Submit Request ".needTranslation, @@ -1061,16 +1049,15 @@ class EmergencyServicesViewModel extends ChangeNotifier { ), direction: AxisDirection.down), ); - if(result){ - LocationViewModel locationViewModel = getIt.get(); - GeocodeResponse? response = locationViewModel.geocodeResponse; - PlaceDetails? placeDetails = locationViewModel.placeDetails; - PlacePrediction? placePrediction = locationViewModel.selectedPrediction; - submitRRTRequest(response?.results.first, placeDetails, placePrediction); - } - + if (result) { + LocationViewModel locationViewModel = getIt.get(); + GeocodeResponse? response = locationViewModel.geocodeResponse; + PlaceDetails? placeDetails = locationViewModel.placeDetails; + PlacePrediction? placePrediction = locationViewModel.selectedPrediction; + submitRRTRequest(response?.results.first, placeDetails, placePrediction); + } }); - } else{ + } else { dialogService.showErrorBottomSheet( message: "You Need To Login First To Continue".needTranslation, onOkPressed: () { @@ -1079,24 +1066,22 @@ class EmergencyServicesViewModel extends ChangeNotifier { }); } } - clearRRTData(){ + + clearRRTData() { selectedRRTProcedure = null; } - FutureOr getTermsAndConditions() async { LoaderBottomSheet.showLoader(loadingText: "Fetching Terms And Conditions".needTranslation); var response = await emergencyServicesRepo.getTermsAndCondition(); LoaderBottomSheet.hideLoader(); - response.fold((failure)=>errorHandlerService.handleError(failure: failure),(success){ + response.fold((failure) => errorHandlerService.handleError(failure: failure), (success) { termsAndConditions = success.data; print("the response terms are $termsAndConditions"); notifyListeners(); navServices.push( - CustomPageRoute( - page: TermsAndCondition(termsAndCondition:success.data??""), direction: AxisDirection.down), + CustomPageRoute(page: TermsAndCondition(termsAndCondition: success.data ?? ""), direction: AxisDirection.down), ); }); } - } diff --git a/lib/features/symptoms_checker/models/conditions_model.dart b/lib/features/symptoms_checker/models/conditions_model.dart new file mode 100644 index 0000000..774d8c3 --- /dev/null +++ b/lib/features/symptoms_checker/models/conditions_model.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; + +class ConditionsModel { + final IconData icon; + final String title; + final int percentage; + final String tagText; + final String clinic; + final List symptoms; + final String description; + final String? monitorNote; + final String? appointmentLabel; + final PossibleConditionsSeverityEnum possibleConditionsSeverityEnum; + + ConditionsModel({ + required this.icon, + required this.title, + required this.percentage, + required this.tagText, + required this.clinic, + required this.symptoms, + required this.description, + required this.possibleConditionsSeverityEnum, + this.monitorNote, + this.appointmentLabel, + }); +} + +List dummyConditions = [ + ConditionsModel( + icon: Icons.psychology_alt, + possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.seekMedicalAdvice, + title: "Migraine", + percentage: 87, + tagText: "Seek Medical Advice", + clinic: "Internal Medicine Clinic", + symptoms: ["Headache", "Nausea", "Sensitivity to light"], + description: "A migraine is a type of headache that can cause severe throbbing pain, usually on one side of the head.", + appointmentLabel: "Book Appointment", + ), + ConditionsModel( + icon: Icons.deblur, + title: "Tension Headache", + percentage: 37, + tagText: "Monitor", + monitorNote: "No need to visit doctor", + clinic: "GP Clinic", + symptoms: ["Mild head pressure", "Scalp tenderness"], + description: "A tension-type headache is generally a mild to moderate pain that feels like a tight band around your head.", + possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.monitorOnly, + ), + ConditionsModel( + icon: Icons.medication_liquid, + title: "Meningitis", + percentage: 28, + tagText: "Emergency", + clinic: "Neurology Clinic", + symptoms: ["Mild head pressure", "Scalp tenderness"], + description: "A tension-type headache is generally a mild to moderate pain that feels like a tight band around your head.", + appointmentLabel: "Book Appointment", + possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.emergency, + ), + ConditionsModel( + icon: Icons.psychology_alt, + title: "Migraine", + percentage: 87, + tagText: "Seek Medical Advice", + clinic: "Internal Medicine Clinic", + symptoms: ["Headache", "Nausea", "Sensitivity to light"], + description: "A migraine is a type of headache that can cause severe throbbing pain, usually on one side of the head.", + appointmentLabel: "Book Appointment", + possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.seekMedicalAdvice, + ), + ConditionsModel( + icon: Icons.deblur, + title: "Tension Headache", + percentage: 37, + tagText: "Monitor", + monitorNote: "No need to visit doctor", + clinic: "GP Clinic", + symptoms: ["Mild head pressure", "Scalp tenderness"], + description: "A tension-type headache is generally a mild to moderate pain that feels like a tight band around your head.", + possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.monitorOnly, + ), + ConditionsModel( + icon: Icons.medication_liquid, + title: "Meningitis", + percentage: 28, + tagText: "Emergency", + clinic: "Neurology Clinic", + symptoms: ["Mild head pressure", "Scalp tenderness"], + description: "A tension-type headache is generally a mild to moderate pain that feels like a tight band around your head.", + appointmentLabel: "Book Appointment", + possibleConditionsSeverityEnum: PossibleConditionsSeverityEnum.emergency, + ), +]; diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index 4ffd979..0efa5da 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -1 +1 @@ -import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.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/utils/size_utils.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/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_order_by_test.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/transitions/fade_page.dart'; import 'package:provider/provider.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import '../../widgets/appbar/collapsing_list_view.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: SingleChildScrollView( padding: EdgeInsets.all(24.h), physics: NeverScrollableScrollPhysics(), child: Consumer( builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.patientLabOrders.isNotEmpty ? model.patientLabOrders.length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.patientLabOrders.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabResultItemView( onTap: () { model.currentlySelectedPatientOrder = model.patientLabOrders[ index]; labProvider.getPatientLabResultByHospital(model.patientLabOrders[ index]); labProvider .getPatientSpecialResult( model.patientLabOrders[ index]); Navigator.push( context, CustomPageRoute( page: LabResultByClinic(labOrder: model.patientLabOrders[index]), )); }, labOrder: model.patientLabOrders[index], index: index, isExpanded: isExpanded), ), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) : ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.uniqueTests.toList().isNotEmpty ? model.uniqueTests.toList().length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.uniqueTests.toList().isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabOrderByTest( appState: _appState, onTap: () { if (model.uniqueTests.toList()[index].model != null) { rangeViewModel.flush(); model.getPatientLabResult(model.uniqueTests.toList()[index].model!, model.uniqueTests.toList()[index].description!, (_appState.isArabic() ? model.uniqueTests.toList()[index].testDescriptionAr! : model.uniqueTests.toList()[index].testDescriptionEn!)); } }, tests: model.uniqueTests.toList()[index], index: index, isExpanded: isExpanded)), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) ], ); }, ), ), )); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file +import 'dart:async'; 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_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/utils/size_utils.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/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_order_by_test.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import '../../widgets/appbar/collapsing_list_view.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: LocaleKeys.labResults.tr(), search: () async { final lavVM = Provider.of(context, listen: false); if (lavVM.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: lavVM.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; lavVM.filterLabReports(value); } } }, child: SingleChildScrollView( padding: EdgeInsets.all(24.h), physics: NeverScrollableScrollPhysics(), child: Consumer( builder: (context, model, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, "By Visit".needTranslation), CustomTabBarModel(null, "By Test".needTranslation), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? CustomChipWidget( chipText: selectedFilterText!, chipType: ChipTypeEnum.alert, isSelected: true, ) : SizedBox(), activeIndex == 0 ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.patientLabOrders.isNotEmpty ? model.patientLabOrders.length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.patientLabOrders.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabResultItemView( onTap: () { model.currentlySelectedPatientOrder = model.patientLabOrders[index]; labProvider.getPatientLabResultByHospital(model.patientLabOrders[index]); labProvider.getPatientSpecialResult(model.patientLabOrders[index]); Navigator.push( context, CustomPageRoute( page: LabResultByClinic(labOrder: model.patientLabOrders[index]), )); }, labOrder: model.patientLabOrders[index], index: index, isExpanded: isExpanded), ), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) : ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: model.isLabOrdersLoading ? 5 : model.uniqueTests.toList().isNotEmpty ? model.uniqueTests.toList().length : 1, itemBuilder: (context, index) { final isExpanded = expandedIndex == index; return model.isLabOrdersLoading ? LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ) : model.uniqueTests.toList().isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: LabOrderByTest( appState: _appState, onTap: () { if (model.uniqueTests.toList()[index].model != null) { rangeViewModel.flush(); model.getPatientLabResult( model.uniqueTests.toList()[index].model!, model.uniqueTests.toList()[index].description!, (_appState.isArabic() ? model.uniqueTests.toList()[index].testDescriptionAr! : model.uniqueTests.toList()[index].testDescriptionEn!)); } }, tests: model.uniqueTests.toList()[index], index: index, isExpanded: isExpanded)), ), ) : Utils.getNoDataWidget(context, noDataText: "You don't have any lab results yet.".needTranslation); }, ) ], ); }, ), ), )); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file diff --git a/lib/presentation/symptoms_checker/organ_selector_screen.dart b/lib/presentation/symptoms_checker/organ_selector_screen.dart index 6b9582c..d945b5b 100644 --- a/lib/presentation/symptoms_checker/organ_selector_screen.dart +++ b/lib/presentation/symptoms_checker/organ_selector_screen.dart @@ -5,6 +5,7 @@ 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/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/presentation/symptoms_checker/symptoms_checker_view_model.dart'; @@ -47,6 +48,7 @@ class _OrganSelectorPageState extends State { backgroundColor: AppColors.successColor, ), ); + context.navigateWithName(AppRoutes.possibleConditionsScreen); } @override @@ -116,7 +118,7 @@ class _OrganSelectorPageState extends State { Widget _buildBodyViewer(SymptomsCheckerViewModel viewModel) { return Padding( - padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 45.h), + padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 60.h), child: AnimatedSwitcher( duration: const Duration(milliseconds: 600), transitionBuilder: (child, animation) => _build3DFlipTransition(child, animation), @@ -129,6 +131,7 @@ class _OrganSelectorPageState extends State { selectedOrganIds: viewModel.selectedOrganIds, onOrganTap: viewModel.toggleOrganSelection, isBodyHidden: viewModel.isBodyHidden, + tooltipOrganId: viewModel.tooltipOrganId, ), ), ); diff --git a/lib/presentation/symptoms_checker/possible_conditions_screen.dart b/lib/presentation/symptoms_checker/possible_conditions_screen.dart new file mode 100644 index 0000000..0587c32 --- /dev/null +++ b/lib/presentation/symptoms_checker/possible_conditions_screen.dart @@ -0,0 +1,177 @@ +import 'package:easy_localization/easy_localization.dart'; +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/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/features/symptoms_checker/models/conditions_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/symptoms_checker_view_model.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/condition_card.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.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/common_bottom_sheet.dart'; +import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; + +class PossibleConditionsScreen extends StatelessWidget { + const PossibleConditionsScreen({super.key}); + + Widget _buildLoadingShimmer() { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.all(16.w), + itemCount: 10, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + height: 80.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10.r), + ), + ), + ); + }, + ); + } + + Widget _buildPredictionsList(List conditions) { + if (conditions.isEmpty) { + return Center( + child: Padding( + padding: EdgeInsets.all(24.h), + child: Text( + 'No Predictions available'.needTranslation, + style: TextStyle( + fontSize: 16.h, + color: AppColors.greyTextColor, + ), + ), + ), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + ListView.separated( + padding: EdgeInsets.zero, + itemCount: conditions.length, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => SizedBox(height: 16.h), + itemBuilder: (context, index) { + final conditionModel = conditions[index]; + return ConditionCard( + icon: conditionModel.icon, + title: conditionModel.title, + percentage: conditionModel.percentage, + tagText: conditionModel.tagText, + clinic: conditionModel.clinic, + symptoms: conditionModel.symptoms, + description: conditionModel.description, + possibleConditionsSeverityEnum: conditionModel.possibleConditionsSeverityEnum, + onActionPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('We are not available for a week. May you Rest In Peace :('), + backgroundColor: AppColors.primaryRedColor, + ), + ); + }, + ); + }, + ).paddingSymmetrical(24.w, 0), + SizedBox(height: 24.h), + ], + ); + } + + _buildConfirmationBottomSheet({required BuildContext context, required VoidCallback onConfirm}) { + return showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: "Are you sure you want to restart the organ selection?".needTranslation, + isShowActionButtons: true, + onCancelTap: () => Navigator.pop(context), + onConfirmTap: () => onConfirm(), + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + _restartOrganSelection(BuildContext context) async { + final symptomsCheckerVm = context.read(); + symptomsCheckerVm.reset(); + context.pop(); + await Future.delayed(Duration(seconds: 1)).whenComplete(() => context.pop()); + } + + _navigateToLandingPage() { + NavigationService navigationService = getIt.get(); + navigationService.replaceAllRoutesAndNavigateToLanding(); + } + + _buildTrailingSection(BuildContext context) { + return Row( + children: [ + Container( + height: 40.h, + padding: EdgeInsets.all(8.w), + child: Center( + child: Utils.buildSvgWithAssets( + icon: AppAssets.refreshIcon, + height: 20.h, + width: 20.w, + iconColor: AppColors.textColor, + ), + ), + ).onPress(() => _buildConfirmationBottomSheet(context: context, onConfirm: () => _restartOrganSelection(context))), + Container( + height: 40.h, + padding: EdgeInsets.all(8.w), + child: Center( + child: Utils.buildSvgWithAssets( + icon: AppAssets.homeBorderedIcon, + height: 20.h, + width: 20.w, + iconColor: AppColors.textColor, + ), + ), + ).onPress(() => _buildConfirmationBottomSheet(context: context, onConfirm: () => _navigateToLandingPage())), + ], + ).paddingSymmetrical(20.w, 0); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: "Possible Conditions".needTranslation, + trailing: _buildTrailingSection(context), + child: Consumer( + builder: (context, symptomsCheckerViewModel, child) { + if (symptomsCheckerViewModel.isPossibleConditionsLoading || symptomsCheckerViewModel.isPossibleConditionsLoading) { + return _buildLoadingShimmer(); + } + return _buildPredictionsList(dummyConditions); + }, + ), + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/symptoms_checker_view_model.dart b/lib/presentation/symptoms_checker/symptoms_checker_view_model.dart index 35a23f1..e162b67 100644 --- a/lib/presentation/symptoms_checker/symptoms_checker_view_model.dart +++ b/lib/presentation/symptoms_checker/symptoms_checker_view_model.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/cupertino.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart'; @@ -10,14 +12,22 @@ class SymptomsCheckerViewModel extends ChangeNotifier { final Set _selectedOrganIds = {}; bool _isBottomSheetExpanded = false; + // Tooltip state + String? _tooltipOrganId; + Timer? _tooltipTimer; + // Getters + bool isPossibleConditionsLoading = false; + BodyView get currentView => _currentView; Set get selectedOrganIds => _selectedOrganIds; bool get isBottomSheetExpanded => _isBottomSheetExpanded; + String? get tooltipOrganId => _tooltipOrganId; + /// Get organs for current view List get currentOrgans => OrganData.getOrgansForView(_currentView); @@ -56,6 +66,33 @@ class SymptomsCheckerViewModel extends ChangeNotifier { } else { _selectedOrganIds.add(organId); } + + // Show tooltip + _showTooltip(organId); + + notifyListeners(); + } + + /// Show tooltip for an organ + void _showTooltip(String organId) { + // Cancel any existing timer + _tooltipTimer?.cancel(); + + // Set the tooltip organ + _tooltipOrganId = organId; + notifyListeners(); + + // Hide tooltip after 2 seconds + _tooltipTimer = Timer(const Duration(seconds: 1), () { + _tooltipOrganId = null; + notifyListeners(); + }); + } + + /// Hide tooltip immediately + void hideTooltip() { + _tooltipTimer?.cancel(); + _tooltipOrganId = null; notifyListeners(); } @@ -103,6 +140,15 @@ class SymptomsCheckerViewModel extends ChangeNotifier { _currentView = BodyView.front; _selectedOrganIds.clear(); _isBottomSheetExpanded = false; + _tooltipTimer?.cancel(); + _tooltipOrganId = null; notifyListeners(); } + + @override + void dispose() { + _tooltipTimer?.cancel(); + super.dispose(); + } + } diff --git a/lib/presentation/symptoms_checker/widgets/condition_card.dart b/lib/presentation/symptoms_checker/widgets/condition_card.dart new file mode 100644 index 0000000..87a8f3d --- /dev/null +++ b/lib/presentation/symptoms_checker/widgets/condition_card.dart @@ -0,0 +1,199 @@ +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/enums.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/presentation/symptoms_checker/widgets/custom_progress_bar.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; + +class ConditionCard extends StatelessWidget { + final IconData icon; + final String title; + final int percentage; + final String tagText; + final String clinic; + final List symptoms; + final String description; + final String? monitorNote; + final String? appointmentLabel; + final PossibleConditionsSeverityEnum possibleConditionsSeverityEnum; + final VoidCallback? onActionPressed; + + const ConditionCard({ + super.key, + required this.icon, + required this.title, + required this.percentage, + required this.tagText, + required this.clinic, + required this.symptoms, + required this.description, + this.monitorNote, + this.appointmentLabel, + required this.possibleConditionsSeverityEnum, + this.onActionPressed, + }); + + Color getChipColorBySeverityEnum(PossibleConditionsSeverityEnum possibleConditionsSeverityEnum) { + switch (possibleConditionsSeverityEnum) { + case PossibleConditionsSeverityEnum.seekMedicalAdvice: + return AppColors.chipColorSeekMedicalAdvice; + case PossibleConditionsSeverityEnum.monitorOnly: + return AppColors.chipColorMonitor; + case PossibleConditionsSeverityEnum.emergency: + return AppColors.chipColorEmergency; + } + } + + Color getChipTextColorBySeverityEnum(PossibleConditionsSeverityEnum possibleConditionsSeverityEnum) { + switch (possibleConditionsSeverityEnum) { + case PossibleConditionsSeverityEnum.seekMedicalAdvice: + return AppColors.chipTextColorSeekMedicalAdvice; + case PossibleConditionsSeverityEnum.monitorOnly: + return AppColors.chipColorMonitor; + case PossibleConditionsSeverityEnum.emergency: + return AppColors.chipColorEmergency; + } + } + + _buildSymptomsRow() { + return Wrap( + spacing: 6.w, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + for (int i = 0; i < symptoms.length; i++) ...[ + Text(symptoms[i], style: TextStyle(color: AppColors.greyTextColor, fontWeight: FontWeight.w500, fontSize: 12.f)), + if (i != symptoms.length - 1) + Padding( + padding: EdgeInsets.symmetric(horizontal: 2.w), + child: Icon(Icons.circle, size: 7, color: AppColors.textColor), + ), + ], + ], + ); + } + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 48.w, + height: 48.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: getChipColorBySeverityEnum(possibleConditionsSeverityEnum).withValues(alpha: 0.2), + borderRadius: 12.r, + hasShadow: false, + ), + child: Icon(icon, color: getChipTextColorBySeverityEnum(possibleConditionsSeverityEnum), size: 24.f), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + title, + style: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, color: AppColors.textColor), + overflow: TextOverflow.ellipsis, + ), + ), + AppCustomChipWidget( + labelText: tagText, + backgroundColor: getChipColorBySeverityEnum(possibleConditionsSeverityEnum).withValues(alpha: 0.2), + textColor: getChipTextColorBySeverityEnum(possibleConditionsSeverityEnum), + ), + ], + ), + CustomRoundedProgressBar( + percentage: percentage, + height: 6.h, + color: getChipColorBySeverityEnum(possibleConditionsSeverityEnum), + backgroundColor: AppColors.scaffoldBgColor, + titleWidget: Row( + children: [ + Text( + "$percentage%", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12.f, + color: getChipColorBySeverityEnum(possibleConditionsSeverityEnum), + ), + ), + ], + ).paddingSymmetrical(0, 4.h), + ), + ], + ), + ), + ], + ).paddingAll(16.w), + Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + clinic, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14.f, + color: AppColors.textColor, + ), + ), + _buildSymptomsRow(), + SizedBox(height: 16.h), + Text("Description".needTranslation, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14.f, color: AppColors.textColor)), + SizedBox(height: 2.h), + Text(description, style: TextStyle(color: AppColors.greyTextColor, fontWeight: FontWeight.w500, fontSize: 12.f)), + if (possibleConditionsSeverityEnum == PossibleConditionsSeverityEnum.emergency) + CustomButton( + text: appointmentLabel ?? "Book Appointment".needTranslation, + onPressed: () { + if (onActionPressed != null) { + onActionPressed!(); + } + }, + backgroundColor: AppColors.lightRedButtonColor, + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 48.h, + icon: AppAssets.add_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 18.h, + ).paddingOnly(top: 16.w), + if (monitorNote != null) + Container( + margin: EdgeInsets.only(top: 12.h), + child: AppCustomChipWidget( + labelText: monitorNote!, + backgroundColor: AppColors.whiteColor, + textColor: AppColors.textColor, + padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 8.h), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r)), + ), + ), + ], + ).paddingAll(16.w), + ], + ), + ); + } +} diff --git a/lib/presentation/symptoms_checker/widgets/custom_progress_bar.dart b/lib/presentation/symptoms_checker/widgets/custom_progress_bar.dart new file mode 100644 index 0000000..5f55395 --- /dev/null +++ b/lib/presentation/symptoms_checker/widgets/custom_progress_bar.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; + +class CustomRoundedProgressBar extends StatelessWidget { + final int percentage; + final Color color; + final Color backgroundColor; + final double? height; + final Widget? titleWidget; + + const CustomRoundedProgressBar({ + super.key, + this.titleWidget, + required this.percentage, + required this.color, + required this.backgroundColor, + this.height, + }); + + @override + Widget build(BuildContext context) { + final h = height ?? 8.h; + return Column( + children: [ + if (titleWidget != null) ...[ + titleWidget!, + ], + LayoutBuilder( + builder: (context, constraints) { + double radius = h / 2; + return Container( + height: h, + width: constraints.maxWidth, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(radius), + ), + child: Align( + alignment: Alignment.centerLeft, + child: FractionallySizedBox( + widthFactor: (percentage.clamp(0, 100)) / 100, + child: Container( + height: height, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.horizontal( + left: Radius.circular(radius), + right: Radius.circular(radius), + ), + ), + ), + ), + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/presentation/symptoms_checker/widgets/interactive_body_widget.dart b/lib/presentation/symptoms_checker/widgets/interactive_body_widget.dart index 33663b5..4c41ddd 100644 --- a/lib/presentation/symptoms_checker/widgets/interactive_body_widget.dart +++ b/lib/presentation/symptoms_checker/widgets/interactive_body_widget.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/organ_dot_widget.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/organ_tooltip_widget.dart'; class InteractiveBodyWidget extends StatefulWidget { final String bodyImageAsset; @@ -11,6 +12,7 @@ class InteractiveBodyWidget extends StatefulWidget { final Set selectedOrganIds; final bool isBodyHidden; final Function(String organId) onOrganTap; + final String? tooltipOrganId; const InteractiveBodyWidget({ super.key, @@ -19,6 +21,7 @@ class InteractiveBodyWidget extends StatefulWidget { required this.selectedOrganIds, required this.onOrganTap, this.isBodyHidden = false, + this.tooltipOrganId, }); @override @@ -77,13 +80,13 @@ class _InteractiveBodyWidgetState extends State { transformationController: _transformationController, minScale: 0.5, maxScale: 4.0, - boundaryMargin: const EdgeInsets.all(20), clipBehavior: Clip.none, child: AspectRatio( aspectRatio: _imageAspectRatio!, child: LayoutBuilder( builder: (context, imageConstraints) { return Stack( + clipBehavior: Clip.none, children: [ // Body image Positioned.fill( @@ -108,6 +111,18 @@ class _InteractiveBodyWidgetState extends State { child: OrganDot(isSelected: isSelected, onTap: () => widget.onOrganTap(organ.id), size: dotSize), ); }), + + // Tooltip + if (widget.tooltipOrganId != null) + ...widget.organs.where((organ) => organ.id == widget.tooltipOrganId).map((organ) { + final centerX = organ.position.x * imageConstraints.maxWidth; + final centerY = organ.position.y * imageConstraints.maxHeight; + + return OrganTooltipWidget( + organName: organ.name, + position: Offset(centerX, centerY), + ); + }), ], ); }, diff --git a/lib/presentation/symptoms_checker/widgets/organ_tooltip_widget.dart b/lib/presentation/symptoms_checker/widgets/organ_tooltip_widget.dart new file mode 100644 index 0000000..77e8319 --- /dev/null +++ b/lib/presentation/symptoms_checker/widgets/organ_tooltip_widget.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class OrganTooltipWidget extends StatelessWidget { + final String organName; + final Offset position; + + const OrganTooltipWidget({super.key, required this.organName, required this.position}); + + @override + Widget build(BuildContext context) { + return Positioned( + left: position.dx, + top: position.dy, + child: FractionalTranslation( + translation: const Offset(-0.5, -1.2), // Center horizontally, position above the dot + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.scaffoldBgColor, borderRadius: 8.h), + child: Text( + organName, + style: TextStyle( + color: AppColors.primaryRedColor, + fontSize: 12.f, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ); + } +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index a144d19..08686d3 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -8,6 +8,7 @@ import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_procedures_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/organ_selector_screen.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/possible_conditions_screen.dart'; import 'package:hmg_patient_app_new/splashPage.dart'; class AppRoutes { @@ -23,6 +24,7 @@ class AppRoutes { // Symptoms Checker static const String organSelectorPage = '/organSelectorPage'; + static const String possibleConditionsScreen = '/possibleConditionsScreen'; static Map get routes => { initialRoute: (context) => SplashPage(), @@ -34,6 +36,7 @@ class AppRoutes { eReferralPage: (context) => EReferralPage(), comprehensiveCheckupPage: (context) => ComprehensiveCheckupPage(), homeHealthCarePage: (context) => HhcProceduresPage(), - organSelectorPage: (context) => OrganSelectorPage() + organSelectorPage: (context) => OrganSelectorPage(), + possibleConditionsScreen: (context) => PossibleConditionsScreen() }; } diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index ccdf030..03e889b 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -82,4 +82,11 @@ class AppColors { static const Color infoBannerBorderColor = Color(0xFFFFE5B4); static const Color infoBannerIconColor = Color(0xFFCC9B14); static const Color infoBannerTextColor = Color(0xFF856404); + + // SymptomsChecker + + static const Color chipColorSeekMedicalAdvice = Color(0xFFFFAF15); + static const Color chipTextColorSeekMedicalAdvice = Color(0xFFAB7103); + static const Color chipColorMonitor = Color(0xFF18C273); + static const Color chipColorEmergency = Color(0xFFED1C2B); }