diff --git a/assets/images/png/bmi_image_1.png b/assets/images/png/bmi_image_1.png new file mode 100644 index 0000000..db3a613 Binary files /dev/null and b/assets/images/png/bmi_image_1.png differ diff --git a/assets/images/svg/blood_pressure.svg b/assets/images/svg/blood_pressure.svg new file mode 100644 index 0000000..67badbe --- /dev/null +++ b/assets/images/svg/blood_pressure.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/bmi_2.svg b/assets/images/svg/bmi_2.svg new file mode 100644 index 0000000..38468d7 --- /dev/null +++ b/assets/images/svg/bmi_2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/heart_rate.svg b/assets/images/svg/heart_rate.svg new file mode 100644 index 0000000..15c754f --- /dev/null +++ b/assets/images/svg/heart_rate.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/height_2.svg b/assets/images/svg/height_2.svg new file mode 100644 index 0000000..a1c361a --- /dev/null +++ b/assets/images/svg/height_2.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/resp_rate.svg b/assets/images/svg/resp_rate.svg new file mode 100644 index 0000000..7038793 --- /dev/null +++ b/assets/images/svg/resp_rate.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/temperature.svg b/assets/images/svg/temperature.svg new file mode 100644 index 0000000..14c7da4 --- /dev/null +++ b/assets/images/svg/temperature.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/weight_2.svg b/assets/images/svg/weight_2.svg new file mode 100644 index 0000000..c22441f --- /dev/null +++ b/assets/images/svg/weight_2.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 4d5535f..6ba2980 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -284,6 +284,19 @@ class AppAssets { static const String covid19icon = '$svgBasePath/covid_19.svg'; + + //vital sign + + static const String heartRate = '$svgBasePath/heart_rate.svg'; + static const String respRate = '$svgBasePath/resp_rate.svg'; + static const String weightVital = '$svgBasePath/weight_2.svg'; + static const String bmiVital = '$svgBasePath/bmi_2.svg'; + static const String heightVital = '$svgBasePath/height_2.svg'; + static const String bloodPressure = '$svgBasePath/blood_pressure.svg'; + static const String temperature = '$svgBasePath/temperature.svg'; + + + // PNGS // static const String hmgLogo = '$pngBasePath/hmg_logo.png'; static const String liveCareService = '$pngBasePath/livecare_service.png'; @@ -309,7 +322,7 @@ class AppAssets { static const String fullBodyFront = '$pngBasePath/full_body_front.png'; static const String fullBodyBack = '$pngBasePath/full_body_back.png'; - + static const String bmiFullBody = '$pngBasePath/bmi_image_1.png'; } diff --git a/lib/features/hmg_services/hmg_services_repo.dart b/lib/features/hmg_services/hmg_services_repo.dart index 37a813d..85e6018 100644 --- a/lib/features/hmg_services/hmg_services_repo.dart +++ b/lib/features/hmg_services/hmg_services_repo.dart @@ -935,8 +935,8 @@ class HmgServicesRepoImp implements HmgServicesRepo { try { List vitalSignList = []; - if (response['PatientVitalSignList'] != null && response['PatientVitalSignList'] is List) { - final vitalSignsList = response['PatientVitalSignList'] as List; + if (response['List_DoctorPatientVitalSign'] != null && response['List_DoctorPatientVitalSign'] is List) { + final vitalSignsList = response['List_DoctorPatientVitalSign'] as List; for (var vitalSignJson in vitalSignsList) { if (vitalSignJson is Map) { diff --git a/lib/features/hmg_services/hmg_services_view_model.dart b/lib/features/hmg_services/hmg_services_view_model.dart index b620c6d..c55a11c 100644 --- a/lib/features/hmg_services/hmg_services_view_model.dart +++ b/lib/features/hmg_services/hmg_services_view_model.dart @@ -51,6 +51,19 @@ class HmgServicesViewModel extends ChangeNotifier { HospitalsModel? selectedHospital; List vitalSignList = []; + // Vital Sign PageView Controller + PageController _vitalSignPageController = PageController(); + PageController get vitalSignPageController => _vitalSignPageController; + + int _vitalSignCurrentPage = 0; + int get vitalSignCurrentPage => _vitalSignCurrentPage; + + void setVitalSignCurrentPage(int page) { + _vitalSignCurrentPage = page; + notifyListeners(); + } + + // HHC specific lists List hhcOrdersList = []; List hhcServicesList = []; @@ -896,4 +909,10 @@ class HmgServicesViewModel extends ChangeNotifier { }, ); } + + @override + void dispose() { + _vitalSignPageController.dispose(); + super.dispose(); + } } diff --git a/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart b/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart new file mode 100644 index 0000000..45b0ab6 --- /dev/null +++ b/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +/// UI-only helper model for Vital Sign cards. +/// +/// Keeps presentation logic (chip colors, icon colors, simple status rules) +/// in one place so it can be reused across multiple pages. +class VitalSignUiModel { + final Color iconBg; + final Color iconFg; + final Color chipBg; + final Color chipFg; + + const VitalSignUiModel({ + required this.iconBg, + required this.iconFg, + required this.chipBg, + required this.chipFg, + }); + + /// Returns a color scheme for a card based on its [status] and [label]. + /// + /// Rules (mirrors existing behavior in Medical File page): + /// - Height is always blue. + /// - High => red scheme. + /// - Low => yellow scheme. + /// - Otherwise => green scheme (Normal). + static VitalSignUiModel scheme({required String? status, required String label}) { + final s = (status ?? '').toLowerCase(); + final l = label.toLowerCase(); + + // Height should always be blue. + if (l.contains('height')) { + return VitalSignUiModel( + iconBg: AppColors.infoColor.withValues(alpha: 0.12), + iconFg: AppColors.infoColor, + chipBg: AppColors.infoColor.withValues(alpha: 0.12), + chipFg: AppColors.infoColor, + ); + } + + if (s.contains('high')) { + return const VitalSignUiModel( + iconBg: AppColors.chipSecondaryLightRedColor, + iconFg: AppColors.primaryRedColor, + chipBg: AppColors.chipSecondaryLightRedColor, + chipFg: AppColors.primaryRedColor, + ); + } + + if (s.contains('low')) { + final Color yellowBg = AppColors.warningColor.withValues(alpha: 0.12); + return VitalSignUiModel( + iconBg: yellowBg, + iconFg: AppColors.warningColor, + chipBg: yellowBg, + chipFg: AppColors.warningColor, + ); + } + + // Normal (green) + final Color greenBg = AppColors.lightGreenColor; + return VitalSignUiModel( + iconBg: greenBg, + iconFg: AppColors.bgGreenColor, + chipBg: greenBg, + chipFg: AppColors.bgGreenColor, + ); + } + + /// Simple, user-friendly classification: + /// - Low: systolic < 90 OR diastolic < 60 + /// - High: systolic >= 140 OR diastolic >= 90 + /// - Normal: otherwise + /// Returns null if values are missing/unparseable. + static String? bloodPressureStatus({dynamic systolic, dynamic diastolic}) { + final int? s = toIntOrNull(systolic); + final int? d = toIntOrNull(diastolic); + if (s == null || d == null) return null; + + if (s < 90 || d < 60) return 'Low'; + if (s >= 140 || d >= 90) return 'High'; + return 'Normal'; + } + + static int? toIntOrNull(dynamic v) { + if (v == null) return null; + if (v is int) return v; + if (v is double) return v.round(); + return int.tryParse(v.toString()); + } + + static String bmiStatus(dynamic bmi) { + if (bmi == null) return 'N/A'; + final double bmiValue = double.tryParse(bmi.toString()) ?? 0; + if (bmiValue < 18.5) return 'Underweight'; + if (bmiValue < 25) return 'Normal'; + if (bmiValue < 30) return 'Overweight'; + return 'High'; + } +} + diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index b4a25ee..9bd48ec 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -127,6 +127,8 @@ class MyAppointmentsViewModel extends ChangeNotifier { initAppointmentsViewModel() { if (isAppointmentDataToBeLoaded) { + // Default view is grouped by clinic on first open. + isAppointmentsSortByClinic = true; patientAppointmentsHistoryList.clear(); patientUpcomingAppointmentsHistoryList.clear(); patientArrivedAppointmentsHistoryList.clear(); @@ -269,6 +271,9 @@ class MyAppointmentsViewModel extends ChangeNotifier { patientAppointmentsHistoryList.addAll(patientArrivedAppointmentsHistoryList); filteredAppointmentList.addAll(patientAppointmentsHistoryList); + // Build grouped list immediately so the UI has data for the default (By Clinic) view. + _groupAppointmentsByClinicAndHospital(); + if (patientArrivedAppointmentsHistoryList.isNotEmpty) { if (Utils.isDateToday(DateUtil.convertStringToDate(patientArrivedAppointmentsHistoryList.first.appointmentDate))) { // getPatientAppointmentQueueDetails(appointmentNo: patientArrivedAppointmentsHistoryList.first.appointmentNo, patientID: patientArrivedAppointmentsHistoryList.first.patientID); @@ -280,6 +285,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { print('Arrived Appointments: ${patientArrivedAppointmentsHistoryList.length}'); print('All Appointments: ${patientAppointmentsHistoryList.length}'); getFiltersForSelectedAppointmentList(filteredAppointmentList); + notifyListeners(); } void getFiltersForSelectedAppointmentList(List filteredAppointmentList) { diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index eb72bc6..b4c3630 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -4,9 +4,6 @@ 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_state.dart'; -import 'package:hmg_patient_app_new/core/dependencies.dart'; -import 'package:hmg_patient_app_new/core/utils/date_util.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'; diff --git a/lib/presentation/appointments/my_doctors_page.dart b/lib/presentation/appointments/my_doctors_page.dart index 3740a4a..2c5d1b0 100644 --- a/lib/presentation/appointments/my_doctors_page.dart +++ b/lib/presentation/appointments/my_doctors_page.dart @@ -1,8 +1,6 @@ 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_state.dart'; -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/string_extensions.dart'; @@ -43,8 +41,8 @@ class _MyDoctorsPageState extends State { bookAppointmentsViewModel = Provider.of(context, listen: false); return CollapsingListView( - title: LocaleKeys.myDoctor.tr(context: context), - child: Consumer(builder: (context, myAppointmentsVM, child) { + title: LocaleKeys.myDoctor.tr(context: context), + child: Consumer(builder: (context, myAppointmentsVM, child) { // build grouped lists from the flat list final clinicMap = >{}; final hospitalMap = >{}; @@ -171,129 +169,121 @@ class _MyDoctorsPageState extends State { final displayName = isSortByClinic ? (group.first.clinicName ?? 'Unknown') : (group.first.projectName ?? 'Unknown'); final isExpanded = expandedIndex == index; - return Container( - key: _groupKeys.putIfAbsent(index, () => GlobalKey()), - margin: EdgeInsets.only(bottom: 12.h), - padding: EdgeInsets.all(16.h), + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: EdgeInsets.symmetric(vertical: 8.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () { - setState(() { - expandedIndex = isExpanded ? null : index; - }); - WidgetsBinding.instance.addPostFrameCallback((_) { - final key = _groupKeys[index]; - if (key != null && key.currentContext != null && expandedIndex == index) { + child: InkWell( + onTap: () { + setState(() { + expandedIndex = isExpanded ? null : index; + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + final key = _groupKeys.putIfAbsent(index, () => GlobalKey()); + if (key.currentContext != null && expandedIndex == index) { + Future.delayed(const Duration(milliseconds: 450), () { + if (key.currentContext != null) { Scrollable.ensureVisible( key.currentContext!, - duration: Duration(milliseconds: 350), + duration: const Duration(milliseconds: 350), curve: Curves.easeInOut, - alignment: 0.1, + alignment: 0.0, ); } }); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CustomButton( - text: "${group.length} ${'doctors'.needTranslation}", - onPressed: () {}, - backgroundColor: AppColors.greyColor, - borderColor: AppColors.greyColor, - textColor: AppColors.blackColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 30.h, - ), - Icon(isExpanded ? Icons.expand_less : Icons.chevron_right, color: AppColors.greyColor), - ], - ), - SizedBox(height: 8.h), - Text( - displayName, - style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), - overflow: TextOverflow.ellipsis, - ), - ], + } + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + key: _groupKeys.putIfAbsent(index, () => GlobalKey()), + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget(labelText: "${group.length} ${'doctors'.needTranslation}"), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + ], + ), + SizedBox(height: 8.h), + Text( + displayName, + style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), + overflow: TextOverflow.ellipsis, + ), + ], + ), ), - ), - AnimatedSwitcher( - duration: Duration(milliseconds: 400), - child: isExpanded - ? Container( - key: ValueKey(index), - padding: EdgeInsets.only(top: 12.h), - child: Column( - children: group.map((doctor) { - return Container( - margin: EdgeInsets.only(bottom: 12.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.h, - hasShadow: true, - ), - child: Padding( - padding: EdgeInsets.all(14.h), - child: Column( + AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + switchInCurve: Curves.easeIn, + switchOutCurve: Curves.easeOut, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: 0.0, + child: child, + ), + ); + }, + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...group.map((doctor) { + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ Image.network( (doctor?.doctorImageURL ?? doctor?.doctorImage ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png"), - width: 24.h, + width: 24.w, height: 24.h, fit: BoxFit.cover, ).circle(100), SizedBox(width: 8.h), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - (doctor?.doctorName ?? "").toString().toText14(weight: FontWeight.w500), - SizedBox(height: 6.h), - ], - ), + child: (doctor?.doctorName ?? "").toString().toText14(weight: FontWeight.w500), ), ], ), SizedBox(height: 8.h), - Row( + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, children: [ - CustomButton( - text: isSortByClinic ? (doctor?.clinicName ?? "") : (doctor?.projectName ?? ""), - onPressed: () {}, - backgroundColor: AppColors.greyColor, - borderColor: AppColors.greyColor, - textColor: AppColors.blackColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 24.h, + AppCustomChipWidget( + labelText: isSortByClinic ? (doctor?.clinicName ?? "") : (doctor?.projectName ?? ""), ), ], ), - SizedBox(height: 8.h), + SizedBox(height: 12.h), Row( children: [ Expanded( - flex: 6, + flex: 2, child: CustomButton( + icon: AppAssets.view_report_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, text: "View Profile".needTranslation.tr(context: context), onPressed: () async { bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( @@ -320,75 +310,31 @@ class _MyDoctorsPageState extends State { ); }); }, - backgroundColor: AppColors.bgRedLightColor, - borderColor: AppColors.primaryRedColor, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, fontSize: 14, fontWeight: FontWeight.w500, borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), ), - SizedBox(width: 8.h), - Expanded( - flex: 1, - child: Container( - height: 40.h, - width: 40.w, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.textColor, - borderRadius: 12, - ), - child: Padding( - padding: EdgeInsets.all(12.h), - child: Transform.flip( - flipX: getIt().isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon_small, - iconColor: AppColors.whiteColor, - fit: BoxFit.contain, - ), - ), - ), - ).onPress(() async { - bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( - clinicID: doctor?.clinicID ?? 0, - projectID: doctor?.projectID ?? 0, - doctorID: doctor?.doctorID ?? 0, - )); - 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, - ); - }); - }), - ), ], ), + SizedBox(height: 12.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + SizedBox(height: 12.h), ], - ), - ), - ); - }).toList(), - ), - ) - : SizedBox.shrink(), - ), - ], + ); + }).toList(), + ], + ), + ) + : const SizedBox.shrink(), + ), + ], + ), ), ); }, diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 8ff71e3..34d9e17 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -15,6 +15,9 @@ 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/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/vital_sign_respo_model.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/vital_sign_ui_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; @@ -50,6 +53,7 @@ import 'package:hmg_patient_app_new/presentation/my_family/my_family.dart'; import 'package:hmg_patient_app_new/presentation/my_invoices/my_invoices_list.dart'; import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart'; import 'package:hmg_patient_app_new/presentation/radiology/radiology_orders_page.dart'; +import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_page.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -82,9 +86,14 @@ class _MedicalFilePageState extends State { late BookAppointmentsViewModel bookAppointmentsViewModel; late LabViewModel labViewModel; late MyInvoicesViewModel myInvoicesViewModel; + late HmgServicesViewModel hmgServicesViewModel; int currentIndex = 0; + // Used to make the PageView height follow the card's intrinsic height + final GlobalKey _vitalSignMeasureKey = GlobalKey(); + double? _vitalSignMeasuredHeight; + @override void initState() { appState = getIt.get(); @@ -95,11 +104,29 @@ class _MedicalFilePageState extends State { medicalFileViewModel.setIsPatientSickLeaveListLoading(true); medicalFileViewModel.getPatientSickLeaveList(); medicalFileViewModel.onTabChanged(0); + // Load vital signs + hmgServicesViewModel.getPatientVitalSign(); } }); super.initState(); } + void _scheduleVitalSignMeasure() { + WidgetsBinding.instance.addPostFrameCallback((_) { + final ctx = _vitalSignMeasureKey.currentContext; + if (ctx == null) return; + final box = ctx.findRenderObject(); + if (box is RenderBox) { + final h = box.size.height; + if (h > 0 && h != _vitalSignMeasuredHeight) { + setState(() { + _vitalSignMeasuredHeight = h; + }); + } + } + }); + } + @override Widget build(BuildContext context) { labViewModel = Provider.of(context, listen: false); @@ -108,6 +135,7 @@ class _MedicalFilePageState extends State { medicalFileViewModel = Provider.of(context, listen: false); bookAppointmentsViewModel = Provider.of(context, listen: false); myInvoicesViewModel = Provider.of(context, listen: false); + hmgServicesViewModel = Provider.of(context, listen: false); NavigationService navigationService = getIt.get(); return CollapsingListView( // title: "Medical File".needTranslation, @@ -258,6 +286,111 @@ class _MedicalFilePageState extends State { ), ).paddingSymmetrical(24.w, 0.0), SizedBox(height: 16.h), + + // Vital Signs Section + Consumer(builder: (context, hmgServicesVM, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Vital Signs".needTranslation.toText16(weight: FontWeight.w500, letterSpacing: -0.2), + Row( + children: [ + LocaleKeys.viewAll.tr().toText12(color: AppColors.primaryRedColor, fontWeight: FontWeight.w500), + SizedBox(width: 2.h), + Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 10.h), + ], + ), + ], + ).paddingSymmetrical(0.w, 0.h).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: VitalSignPage(), + ), + ); + }), + SizedBox(height: 16.h), + // Make this section dynamic-height (no fixed 160.h) + LayoutBuilder( + builder: (context, constraints) { + if (hmgServicesVM.isVitalSignLoading) { + return _buildVitalSignShimmer(); + } + if (hmgServicesVM.vitalSignList.isEmpty) { + return Container( + padding: EdgeInsets.all(16.w), + width: MediaQuery.of(context).size.width, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + hasShadow: false, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.call_for_vitals, width: 32.h, height: 32.h), + SizedBox(height: 12.h), + "No vital signs recorded yet".needTranslation.toText12(isCenter: true), + ], + ), + ); + } + + // The cards define their own height; measure the first rendered page once + _scheduleVitalSignMeasure(); + final double hostHeight = _vitalSignMeasuredHeight ?? (160.h); + + return SizedBox( + height: hostHeight, + child: PageView( + controller: hmgServicesVM.vitalSignPageController, + onPageChanged: (index) { + hmgServicesVM.setVitalSignCurrentPage(index); + _scheduleVitalSignMeasure(); + }, + children: _buildVitalSignPages( + vitalSign: hmgServicesVM.vitalSignList.first, + onTap: () { + Navigator.of(context).push( + CustomPageRoute( + page: VitalSignPage(), + ), + ); + }, + measureKey: _vitalSignMeasureKey, + currentPageIndex: hmgServicesVM.vitalSignCurrentPage, + ), + ), + ); + }, + ), + if (!hmgServicesVM.isVitalSignLoading && hmgServicesVM.vitalSignList.isNotEmpty) ...[ + SizedBox(height: 12.h), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + 2, // 2 pages (BMI+Height on page 1, Weight+BP on page 2) + (index) => Container( + margin: EdgeInsets.symmetric(horizontal: 3.w), + width: hmgServicesVM.vitalSignCurrentPage == index ? 24.w : 8.w, + height: 8.h, + decoration: BoxDecoration( + color: hmgServicesVM.vitalSignCurrentPage == index + ? AppColors.primaryRedColor + : AppColors.dividerColor, + borderRadius: BorderRadius.circular(4.r), + ), + ), + ), + ), + ], + ], + ).paddingSymmetrical(24.w, 0.0); + }), + SizedBox(height: 16.h), + TextInputWidget( labelText: LocaleKeys.search.tr(context: context), hintText: "Type any record".needTranslation, @@ -276,7 +409,7 @@ class _MedicalFilePageState extends State { // Using CustomExpandableList CustomExpandableList( expansionMode: ExpansionMode.exactlyOne, - dividerColor: Color(0xFF2B353E1A), + dividerColor: Color(0xff2b353e1a), itemPadding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 14.h), items: [ ExpandableListItem( @@ -493,47 +626,51 @@ class _MedicalFilePageState extends State { horizontalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - child: MedicalFileAppointmentCard( - patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], - myAppointmentsViewModel: myAppointmentsViewModel, - onRescheduleTap: () { - openDoctorScheduleCalendar(myAppointmentsVM.patientAppointmentsHistoryList[index]); - }, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: MedicalFileAppointmentCard( + patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], + myAppointmentsViewModel: myAppointmentsViewModel, + onRescheduleTap: () { + openDoctorScheduleCalendar(myAppointmentsVM.patientAppointmentsHistoryList[index]); + }, onAskDoctorTap: () async { LoaderBottomSheet.showLoader(loadingText: "Checking doctor availability...".needTranslation); await myAppointmentsViewModel.isDoctorAvailable( - projectID: myAppointmentsVM.patientAppointmentsHistoryList[index].projectID, - doctorId: myAppointmentsVM.patientAppointmentsHistoryList[index].doctorID, - clinicId: myAppointmentsVM.patientAppointmentsHistoryList[index].clinicID, - onSuccess: (value) async { - if (value) { - await myAppointmentsViewModel.getAskDoctorRequestTypes(onSuccess: (val) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - title: LocaleKeys.askDoctor.tr(context: context), - child: AskDoctorRequestTypeSelect( - askDoctorRequestTypeList: myAppointmentsViewModel.askDoctorRequestTypeList, - myAppointmentsViewModel: myAppointmentsViewModel, - patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], - ), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); - } else { - print("Doctor is not available"); - } - }); + projectID: myAppointmentsVM.patientAppointmentsHistoryList[index].projectID, + doctorId: myAppointmentsVM.patientAppointmentsHistoryList[index].doctorID, + clinicId: myAppointmentsVM.patientAppointmentsHistoryList[index].clinicID, + onSuccess: (value) async { + if (value) { + await myAppointmentsViewModel.getAskDoctorRequestTypes(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.askDoctor.tr(context: context), + child: AskDoctorRequestTypeSelect( + askDoctorRequestTypeList: myAppointmentsViewModel.askDoctorRequestTypeList, + myAppointmentsViewModel: myAppointmentsViewModel, + patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList[index], + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } else { + LoaderBottomSheet.hideLoader(); + print("Doctor is not available"); + } + }, + onError: (_) { + LoaderBottomSheet.hideLoader(); + }, + ); }, ), ), ), - ), - ); + )); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 12.h), ), @@ -651,58 +788,57 @@ class _MedicalFilePageState extends State { ); }), ), + )); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), + ), + SizedBox(height: 16.h), + const Divider(color: AppColors.dividerColor), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: "All Prescriptions".needTranslation, + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: PrescriptionsListPage(), + ), + ); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 12.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + height: 40.h, + icon: AppAssets.requests, + iconColor: AppColors.primaryRedColor, + iconSize: 16.w, + ), ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), - ), - SizedBox(height: 16.h), - const Divider(color: AppColors.dividerColor), - SizedBox(height: 16.h), - Row( - children: [ - Expanded( - child: CustomButton( - text: "All Prescriptions".needTranslation, - onPressed: () { - Navigator.of(context).push( - CustomPageRoute( - page: PrescriptionsListPage(), - ), - ); - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 12.f, - fontWeight: FontWeight.w500, - borderRadius: 12.r, - height: 40.h, - icon: AppAssets.requests, - iconColor: AppColors.primaryRedColor, - iconSize: 16.w, - ), - ), - SizedBox(width: 6.w), - Expanded( - child: CustomButton( - text: "All Medications".needTranslation, - onPressed: () {}, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 12.f, - fontWeight: FontWeight.w500, - borderRadius: 12.h, - height: 40.h, - icon: AppAssets.all_medications_icon, - iconColor: AppColors.primaryRedColor, - iconSize: 16.h, + SizedBox(width: 6.w), + Expanded( + child: CustomButton( + text: "All Medications".needTranslation, + onPressed: () {}, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 12.f, + fontWeight: FontWeight.w500, + borderRadius: 12.h, + height: 40.h, + icon: AppAssets.all_medications_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + ), ), - ), - ], - ), - ], + ], + ), + ], ), ), ).paddingSymmetrical(0.w, 0.h) @@ -834,8 +970,7 @@ class _MedicalFilePageState extends State { }); }), ), - ), - ); + )); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h), ), @@ -1079,7 +1214,7 @@ class _MedicalFilePageState extends State { backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.medical_reports_icon, isLargeText: true, - iconSize: 36.h, + iconSize: 36.w, ).onPress(() { medicalFileViewModel.setIsPatientMedicalReportsLoading(true); medicalFileViewModel.getPatientMedicalReportList(); @@ -1199,4 +1334,265 @@ class _MedicalFilePageState extends State { return Container(); } } + + // Build shimmer for vital signs + Widget _buildVitalSignShimmer() { + return Row( + children: [ + Expanded(child: _buildSingleShimmerCard()), + SizedBox(width: 12.w), + Expanded(child: _buildSingleShimmerCard()), + ], + ); + } + + Widget _buildSingleShimmerCard() { + return Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(16.r), + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 20.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Icon shimmer at top + Container( + width: 44.w, + height: 44.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + ), + ).toShimmer(), + SizedBox(height: 16.h), + // Label shimmer + Container( + width: 70.w, + height: 12.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.r), + ), + ).toShimmer(), + SizedBox(height: 8.h), + // Value shimmer (larger) + Container( + width: 60.w, + height: 32.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.r), + ), + ).toShimmer(), + SizedBox(height: 12.h), + // Bottom row with chip and arrow + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: 60.w, + height: 20.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + ), + ).toShimmer(), + Container( + width: 16.w, + height: 16.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.r), + ), + ).toShimmer(), + ], + ), + ], + ), + ), + ); + } + + // Build pages with 2 cards each + List _buildVitalSignPages({ + required VitalSignResModel vitalSign, + required VoidCallback onTap, + required GlobalKey measureKey, + required int currentPageIndex, + }) { + return [ + // Page 1: BMI + Height + Row( + children: [ + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.bmiVital, + label: "BMI", + value: vitalSign.bodyMassIndex?.toString() ?? '--', + unit: '', + status: vitalSign.bodyMassIndex != null ? _getBMIStatus(vitalSign.bodyMassIndex) : null, + onTap: onTap, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.heightVital, + label: "Height", + value: vitalSign.heightCm?.toString() ?? '--', + unit: 'cm', + status: null, + onTap: onTap, + ), + ), + ], + ), + // Page 2: Weight + Blood Pressure + Row( + children: [ + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.weightVital, + label: "Weight", + value: vitalSign.weightKg?.toString() ?? '--', + unit: 'kg', + status: vitalSign.weightKg != null ? "Normal" : null, + onTap: onTap, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: _buildVitalSignCard( + icon: AppAssets.bloodPressure, + label: "Blood Pressure", + value: vitalSign.bloodPressureLower != null && vitalSign.bloodPressureHigher != null + ? "${vitalSign.bloodPressureHigher}/${vitalSign.bloodPressureLower}" + : '--', + unit: '', + status: _getBloodPressureStatus( + systolic: vitalSign.bloodPressureHigher, + diastolic: vitalSign.bloodPressureLower, + ), + onTap: onTap, + ), + ), + ], + ), + ]; + } + + String _getBMIStatus(dynamic bmi) { + return VitalSignUiModel.bmiStatus(bmi); + } + + String? _getBloodPressureStatus({dynamic systolic, dynamic diastolic}) { + return VitalSignUiModel.bloodPressureStatus(systolic: systolic, diastolic: diastolic); + } + + Widget _buildVitalSignCard({ + required String icon, + required String label, + required String value, + required String unit, + required String? status, + required VoidCallback onTap, + }) { + final VitalSignUiModel scheme = VitalSignUiModel.scheme(status: status, label: label); + + return GestureDetector( + onTap: onTap, + child: Container( + // Same styling used originally for vitals in MedicalFilePage + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(10.h), + decoration: BoxDecoration( + color: scheme.iconBg, + borderRadius: BorderRadius.circular(12.r), + ), + child: Utils.buildSvgWithAssets( + icon: icon, + width: 20.w, + height: 20.h, + iconColor: scheme.iconFg, + fit: BoxFit.contain, + ), + ), + SizedBox(width: 10.w), + Expanded( + child: label.toText14( + color: AppColors.textColor, + weight: FontWeight.w600, + ), + ), + ], + ), + SizedBox(height: 14.h), + + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h), + decoration: BoxDecoration( + color: AppColors.bgScaffoldColor, + borderRadius: BorderRadius.circular(10.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + value.toText17( + isBold: true, + color: AppColors.textColor, + ), + if (unit.isNotEmpty) ...[ + SizedBox(width: 3.w), + unit.toText12( + color: AppColors.textColor, + fontWeight: FontWeight.w500, + ), + ], + ], + ), + if (status != null) + AppCustomChipWidget( + labelText: status, + backgroundColor: scheme.chipBg, + textColor: scheme.chipFg, + ) + else + const SizedBox.shrink(), + ], + ), + ), + SizedBox(height: 8.h), + + Align( + alignment: AlignmentDirectional.centerEnd, + child: Utils.buildSvgWithAssets( + icon: AppAssets.arrow_forward, + width: 18.w, + height: 18.h, + iconColor: AppColors.textColorLight, + fit: BoxFit.contain, + ), + ), + ], + ), + ), + ), + ); + } } + + + diff --git a/lib/presentation/radiology/radiology_orders_page.dart b/lib/presentation/radiology/radiology_orders_page.dart index c0eb782..fb153ea 100644 --- a/lib/presentation/radiology/radiology_orders_page.dart +++ b/lib/presentation/radiology/radiology_orders_page.dart @@ -190,231 +190,173 @@ class _RadiologyOrdersPageState extends State { itemBuilder: (context, index) { final group = model.patientRadiologyOrdersViewList[index]; final displayName = model.isSortByClinic ? (group.first.clinicDescription ?? 'Unknown') : (group.first.projectName ?? 'Unknown'); + final isExpanded = expandedIndex == index; return AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 400), child: SlideAnimation( verticalOffset: 50.0, child: FadeInAnimation( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Group header container with key so we can scroll to it - GestureDetector( - onTap: () { - setState(() { - expandedIndex = expandedIndex == index ? null : index; - }); - WidgetsBinding.instance.addPostFrameCallback((_) { - final key = _groupKeys.putIfAbsent(index, () => GlobalKey()); - if (key.currentContext != null && expandedIndex == index) { - // Delay scrolling to wait for expansion animation - Future.delayed(Duration(milliseconds: 450), () { - if (key.currentContext != null) { - Scrollable.ensureVisible( - key.currentContext!, - duration: Duration(milliseconds: 350), - curve: Curves.easeInOut, - alignment: 0.0, - ); - } - }); - } - }); - }, - child: Container( - key: _groupKeys.putIfAbsent(index, () => GlobalKey()), - margin: EdgeInsets.only(bottom: 8.h), - padding: EdgeInsets.all(12.h), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8.h), - boxShadow: [ - BoxShadow( - color: AppColors.blackColor.withValues(alpha: 0.03), - blurRadius: 6, - offset: Offset(0, 2), - ) - ], - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: InkWell( + onTap: () { + setState(() { + expandedIndex = isExpanded ? null : index; + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + final key = _groupKeys.putIfAbsent(index, () => GlobalKey()); + if (key.currentContext != null && expandedIndex == index) { + Future.delayed(const Duration(milliseconds: 450), () { + if (key.currentContext != null) { + Scrollable.ensureVisible( + key.currentContext!, + duration: const Duration(milliseconds: 350), + curve: Curves.easeInOut, + alignment: 0.0, + ); + } + }); + } + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + key: _groupKeys.putIfAbsent(index, () => GlobalKey()), + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CustomButton( - text: "${group.length} ${'results'.needTranslation}", - onPressed: () {}, - backgroundColor: AppColors.greyColor, - borderColor: AppColors.greyColor, - textColor: AppColors.blackColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 30.h, - ), - Icon(expandedIndex == index ? Icons.expand_less : Icons.expand_more), - ], - ), - SizedBox(height: 8.h), - Text( - displayName, - style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), - overflow: TextOverflow.ellipsis, - ), + AppCustomChipWidget(labelText: "${group.length} ${'results'.needTranslation}"), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), ], ), - ), - ], + SizedBox(height: 8.h), + Text( + displayName, + style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), + overflow: TextOverflow.ellipsis, + ), + ], + ), ), - ), - ), - AnimatedSwitcher( - duration: Duration(milliseconds: 400), - child: expandedIndex == index - ? Container( - key: ValueKey(index), - padding: EdgeInsets.only(top: 12.h), - child: Column( - children: group.map((order) { - return Container( - margin: EdgeInsets.only(bottom: 12.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 12.h, - hasShadow: true, - ), - child: Padding( - padding: EdgeInsets.all(14.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.network( - order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", - width: 24.h, - height: 24.h, - fit: BoxFit.cover, - ).circle(100), - SizedBox(width: 8.h), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - - children: [ - (order.doctorName ?? "").toString().toText14(weight: FontWeight.w500), - SizedBox(height: 6.h), - - - ], + AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + switchInCurve: Curves.easeIn, + switchOutCurve: Curves.easeOut, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: 0.0, + child: child, + ), + ); + }, + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...group.map((order) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.network( + order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", + width: 24.w, + height: 24.h, + fit: BoxFit.cover, + ).circle(100), + SizedBox(width: 8.h), + Expanded( + child: (order.doctorName ?? '').toString().toText14(weight: FontWeight.w500), ), - ), - ], - ), - SizedBox(height: 8.h), - Row(children: [ - CustomButton( - text: order.description!, - onPressed: () {}, - backgroundColor: AppColors.greyColor, - borderColor: AppColors.greyColor, - textColor: AppColors.blackColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 24.h, - ) - - ] - ), - SizedBox(height: 6.h), - - Row( - children: [ - CustomButton( - text: DateUtil.formatDateToDate(order.orderDate ?? order.appointmentDate ?? "", false), - onPressed: () {}, - backgroundColor: AppColors.greyColor, - borderColor: AppColors.greyColor, - textColor: AppColors.blackColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 24.h, - ), - SizedBox(width: 8.h), - CustomButton( - text: model.isSortByClinic ? (order.clinicDescription ?? "") : (order.projectName ?? ""), - onPressed: () {}, - backgroundColor: AppColors.greyColor, - borderColor: AppColors.greyColor, - textColor: AppColors.blackColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 24.h, - ), - ], - ), - SizedBox(height: 8.h), - Row( - children: [ - Expanded( - flex: 6, - child: SizedBox(), - ), - SizedBox(width: 8.h), - Expanded( - flex: 1, - child: Container( - height: 40.h, - width: 40.w, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.textColor, - borderRadius: 12, + ], + ), + SizedBox(height: 8.h), + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, + children: [ + if ((order.description ?? '').isNotEmpty) + AppCustomChipWidget( + labelText: (order.description ?? '').toString(), ), - child: Padding( - padding: EdgeInsets.all(12.h), - child: Transform.flip( - flipX: false, - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon_small, - iconColor: AppColors.whiteColor, - fit: BoxFit.contain, - ), - ), + AppCustomChipWidget( + labelText: DateUtil.formatDateToDate( + (order.orderDate ?? order.appointmentDate), + false, ), - ).onPress(() { - model.navigationService.push( - CustomPageRoute( - page: RadiologyResultPage(patientRadiologyResponseModel: order), - ), - ); - }), - ), - ], - ), - ], - ), - ), - ); - }).toList(), - ), - ) - : SizedBox.shrink(), + ), + AppCustomChipWidget( + labelText: model.isSortByClinic ? (order.clinicDescription ?? '') : (order.projectName ?? ''), + ), + ], + ), + SizedBox(height: 12.h), + Row( + children: [ + Expanded(flex: 2, child: const SizedBox()), + Expanded( + flex: 2, + child: CustomButton( + icon: AppAssets.view_report_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + text: "View Results".needTranslation, + onPressed: () { + model.navigationService.push( + CustomPageRoute( + page: RadiologyResultPage(patientRadiologyResponseModel: order), + ), + ); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + ), + ], + ), + SizedBox(height: 12.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + SizedBox(height: 12.h), + ], + ); + }).toList(), + ], + ), + ) + : const SizedBox.shrink(), + ), + ], ), - ], + ), ), ), ), diff --git a/lib/presentation/vital_sign/vital_sign_page.dart b/lib/presentation/vital_sign/vital_sign_page.dart index 858e801..bd25641 100644 --- a/lib/presentation/vital_sign/vital_sign_page.dart +++ b/lib/presentation/vital_sign/vital_sign_page.dart @@ -1,4 +1,4 @@ -import 'dart:async'; +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -11,7 +11,7 @@ import 'package:hmg_patient_app_new/features/hmg_services/models/resq_models/vit 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/chip/app_custom_chip_widget.dart'; -import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/vital_sign_ui_model.dart'; import 'package:provider/provider.dart'; class VitalSignPage extends StatefulWidget { @@ -22,22 +22,10 @@ class VitalSignPage extends StatefulWidget { } class _VitalSignPageState extends State { + @override void initState() { super.initState(); - final HmgServicesViewModel hmgServicesViewModel = context.read(); - - scheduleMicrotask(() async { - LoaderBottomSheet.showLoader(loadingText: 'Loading Vital Signs...'); - await hmgServicesViewModel.getPatientVitalSign( - onSuccess: (_) { - LoaderBottomSheet.hideLoader(); - }, - onError: (_) { - LoaderBottomSheet.hideLoader(); - }, - ); - }); } @override @@ -72,48 +60,40 @@ class _VitalSignPageState extends State { children: [ // BMI Card _buildVitalSignCard( - icon: AppAssets.activity, - iconColor: AppColors.successColor, - iconBgColor: AppColors.successColor.withValues(alpha: 0.1), + icon: AppAssets.bmiVital, label: 'BMI', value: latestVitalSign?.bodyMassIndex?.toString() ?? '--', unit: '', - chipText: _getBMIStatus(latestVitalSign?.bodyMassIndex), - chipBgColor: AppColors.successColor.withValues(alpha: 0.1), - chipTextColor: AppColors.successColor, + status: VitalSignUiModel.bmiStatus(latestVitalSign?.bodyMassIndex), + onTap: () {}, ), SizedBox(height: 16.h), // Height Card _buildVitalSignCard( - icon: AppAssets.height, - iconColor: AppColors.infoColor, - iconBgColor: AppColors.infoColor.withValues(alpha: 0.1), + icon: AppAssets.heightVital, label: 'Height', value: latestVitalSign?.heightCm?.toString() ?? '--', unit: 'cm', + status: null, + onTap: () {}, ), SizedBox(height: 16.h), // Weight Card _buildVitalSignCard( - icon: AppAssets.weight, - iconColor: AppColors.successColor, - iconBgColor: AppColors.successColor.withValues(alpha: 0.1), + icon: AppAssets.weightVital, label: 'Weight', value: latestVitalSign?.weightKg?.toString() ?? '--', unit: 'kg', - chipText: 'Normal', - chipBgColor: AppColors.successColor.withValues(alpha: 0.1), - chipTextColor: AppColors.successColor, + status: (latestVitalSign?.weightKg != null) ? 'Normal' : null, + onTap: () {}, ), SizedBox(height: 16.h), // Blood Pressure Card _buildVitalSignCard( - icon: AppAssets.activity, - iconColor: AppColors.warningColor, - iconBgColor: AppColors.warningColor.withValues(alpha: 0.1), + icon: AppAssets.bloodPressure, label: 'Blood Pressure', value: latestVitalSign != null && latestVitalSign.bloodPressureHigher != null && @@ -121,17 +101,22 @@ class _VitalSignPageState extends State { ? '${latestVitalSign.bloodPressureHigher}/${latestVitalSign.bloodPressureLower}' : '--', unit: '', + status: VitalSignUiModel.bloodPressureStatus( + systolic: latestVitalSign?.bloodPressureHigher, + diastolic: latestVitalSign?.bloodPressureLower, + ), + onTap: () {}, ), SizedBox(height: 16.h), // Temperature Card _buildVitalSignCard( - icon: AppAssets.activity, - iconColor: AppColors.errorColor, - iconBgColor: AppColors.errorColor.withValues(alpha: 0.1), + icon: AppAssets.temperature, label: 'Temperature', value: latestVitalSign?.temperatureCelcius?.toString() ?? '--', unit: '°C', + status: null, + onTap: () {}, ), ], ), @@ -143,54 +128,80 @@ class _VitalSignPageState extends State { Expanded( child: Column( children: [ - // Body anatomy image - Container( - height: 280.h, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: true, - ), - child: Center( - child: Image.asset( - AppAssets.fullBodyFront, - height: 260.h, - fit: BoxFit.contain, - ), - ), - ), - SizedBox(height: 16.h), + // Body anatomy image with Heart Rate card overlaid at bottom + SizedBox( + height: 480.h, + width: double.infinity, + child: Stack( + clipBehavior: Clip.none, + children: [ + // Image + Positioned.fill( + child: Stack( + fit: StackFit.expand, + children: [ + Image.asset( + AppAssets.bmiFullBody, + fit: BoxFit.cover, + alignment: Alignment.topCenter, + ), + Align( + alignment: Alignment.bottomCenter, + child: SizedBox( + height: 420.h, + child: ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 6, sigmaY: 6), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppColors.whiteColor.withValues(alpha: 0.0), + AppColors.whiteColor.withValues(alpha: 0.97), + AppColors.whiteColor, + ], + ), + ), + ), + ), + ), + ), + ], + ), + ), - // Heart Rate Card - _buildVitalSignCard( - icon: AppAssets.heart, - iconColor: AppColors.errorColor, - iconBgColor: AppColors.errorColor.withValues(alpha: 0.1), - label: 'Heart Rate', - value: latestVitalSign?.heartRate?.toString() ?? - latestVitalSign?.pulseBeatPerMinute?.toString() ?? '--', - unit: 'bpm', - chipText: 'Normal', - chipBgColor: AppColors.successColor.withValues(alpha: 0.1), - chipTextColor: AppColors.successColor, + // Overlay Heart Rate card + Positioned( + left: 0, + right: 0, + bottom: 12.h, + child: _buildVitalSignCard( + icon: AppAssets.heart, + label: 'Heart Rate', + value: latestVitalSign?.heartRate?.toString() ?? latestVitalSign?.pulseBeatPerMinute?.toString() ?? '--', + unit: 'bpm', + status: 'Normal', + onTap: () {}, + ), + ), + ], + ), ), - SizedBox(height: 16.h), + SizedBox(height: 12.h), - // Respiratory rate Card - _buildVitalSignCard( - icon: AppAssets.activity, - iconColor: AppColors.successColor, - iconBgColor: AppColors.successColor.withValues(alpha: 0.1), - label: 'Respiratory rate', - value: latestVitalSign?.respirationBeatPerMinute?.toString() ?? '--', - unit: 'bpm', - chipText: 'Normal', - chipBgColor: AppColors.successColor.withValues(alpha: 0.1), - chipTextColor: AppColors.successColor, - ), - ], - ), - ), + // Respiratory rate Card + _buildVitalSignCard( + icon: AppAssets.respRate, + label: 'Respiratory rate', + value: latestVitalSign?.respirationBeatPerMinute?.toString() ?? '--', + unit: 'bpm', + status: 'Normal', + onTap: () {}, + ), + ], + ), + ), ], ), ), @@ -205,104 +216,106 @@ class _VitalSignPageState extends State { ); } - String? _getBMIStatus(dynamic bmi) { - if (bmi == null) return null; - double bmiValue = double.tryParse(bmi.toString()) ?? 0; - if (bmiValue < 18.5) return 'Underweight'; - if (bmiValue < 25) return 'Normal'; - if (bmiValue < 30) return 'Overweight'; - return 'Obese'; - } - Widget _buildVitalSignCard({ required String icon, - required Color iconColor, - required Color iconBgColor, required String label, required String value, required String unit, - String? chipText, - Color? chipBgColor, - Color? chipTextColor, + required String? status, + required VoidCallback onTap, }) { - return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: true, - ), - child: Padding( - padding: EdgeInsets.all(12.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - // Icon with background - Container( - padding: EdgeInsets.all(8.h), - decoration: BoxDecoration( - color: iconBgColor, - borderRadius: BorderRadius.circular(12.r), + final VitalSignUiModel scheme = VitalSignUiModel.scheme(status: status, label: label); + + return GestureDetector( + onTap: onTap, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(10.h), + decoration: BoxDecoration( + color: scheme.iconBg, + borderRadius: BorderRadius.circular(12.r), + ), + child: Utils.buildSvgWithAssets( + icon: icon, + width: 20.w, + height: 20.h, + iconColor: scheme.iconFg, + fit: BoxFit.contain, + ), ), - child: Utils.buildSvgWithAssets( - icon: icon, - width: 16.w, - height: 16.h, - iconColor: iconColor, - fit: BoxFit.contain, + SizedBox(width: 10.w), + Expanded( + child: label.toText14( + color: AppColors.textColor, + weight: FontWeight.w600, + ), ), + ], + ), + SizedBox(height: 14.h), + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h), + decoration: BoxDecoration( + color: AppColors.bgScaffoldColor, + borderRadius: BorderRadius.circular(10.r), ), - SizedBox(width: 8.w), - Expanded( - child: label.toText10( - color: AppColors.textColorLight, - weight: FontWeight.w500, - ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + value.toText17( + isBold: true, + color: AppColors.textColor, + ), + if (unit.isNotEmpty) ...[ + SizedBox(width: 3.w), + unit.toText12( + color: AppColors.textColor, + fontWeight: FontWeight.w500, + ), + ], + ], + ), + if (status != null) + AppCustomChipWidget( + labelText: status, + backgroundColor: scheme.chipBg, + textColor: scheme.chipFg, + ) + else + const SizedBox.shrink(), + ], ), - // Forward arrow - Utils.buildSvgWithAssets( + ), + SizedBox(height: 8.h), + Align( + alignment: AlignmentDirectional.centerEnd, + child: Utils.buildSvgWithAssets( icon: AppAssets.arrow_forward, - width: 16.w, - height: 16.h, + width: 18.w, + height: 18.h, iconColor: AppColors.textColorLight, fit: BoxFit.contain, ), - ], - ), - SizedBox(height: 12.h), - - // Value - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - value.toText18( - isBold: true, - color: AppColors.textColor, - ), - if (unit.isNotEmpty) ...[ - SizedBox(width: 4.w), - unit.toText12( - color: AppColors.textColorLight, - ), - ], - ], - ), - - // Chip if available - if (chipText != null) ...[ - SizedBox(height: 8.h), - AppCustomChipWidget( - labelText: chipText, - backgroundColor: chipBgColor, - textColor: chipTextColor, - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), ), ], - ], + ), ), ), ); } } - diff --git a/lib/widgets/shimmer/vital_sign_shimmer_widget.dart b/lib/widgets/shimmer/vital_sign_shimmer_widget.dart new file mode 100644 index 0000000..2ab4741 --- /dev/null +++ b/lib/widgets/shimmer/vital_sign_shimmer_widget.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +class VitalSignShimmerWidget extends StatelessWidget { + const VitalSignShimmerWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + // BMI Card Shimmer + Expanded(child: _buildShimmerCard()), + SizedBox(width: 8.w), + // Height Card Shimmer + Expanded(child: _buildShimmerCard()), + SizedBox(width: 8.w), + // Weight Card Shimmer + Expanded(child: _buildShimmerCard()), + SizedBox(width: 8.w), + // Blood Pressure Card Shimmer + Expanded(child: _buildShimmerCard()), + ], + ); + } + + Widget _buildShimmerCard() { + return Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(12.r), + ), + child: Padding( + padding: EdgeInsets.all(12.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Icon shimmer + Container( + width: 32.w, + height: 32.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.r), + ), + ).toShimmer(), + SizedBox(height: 8.h), + // Label shimmer + Container( + width: 50.w, + height: 10.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.r), + ), + ).toShimmer(), + SizedBox(height: 4.h), + // Value shimmer + Container( + width: 40.w, + height: 16.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.r), + ), + ).toShimmer(), + SizedBox(height: 4.h), + // Chip shimmer + Container( + width: 45.w, + height: 18.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + ), + ).toShimmer(), + SizedBox(height: 4.h), + // Arrow shimmer + Align( + alignment: AlignmentDirectional.centerEnd, + child: Container( + width: 10.w, + height: 10.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.r), + ), + ).toShimmer(), + ), + ], + ), + ), + ); + } +} +