From 8ee0e3b4ea26f4f7efa31ced4f537ea39c9a3a6b Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Mon, 12 Jan 2026 12:10:08 +0300 Subject: [PATCH 1/3] no message --- lib/core/dependencies.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 582b795..012bd34 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -265,7 +265,8 @@ class AppDependencies { getIt.registerLazySingleton(() => WaterMonitorViewModel(waterMonitorRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton(() => MyInvoicesViewModel(myInvoicesRepo: getIt(), errorHandlerService: getIt(), navServices: getIt())); + //commenting this because its already define there was on run time error because of this. + // getIt.registerLazySingleton(() => MyInvoicesViewModel(myInvoicesRepo: getIt(), errorHandlerService: getIt(), navServices: getIt())); getIt.registerLazySingleton(() => MonthlyReportViewModel(errorHandlerService: getIt(), monthlyReportRepo: getIt())); getIt.registerLazySingleton(() => MyInvoicesViewModel( From 6725ab028d5ec69dd9d7b006f90508cec054b87b Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Mon, 12 Jan 2026 15:55:41 +0300 Subject: [PATCH 2/3] doctor search filter design update & switch user bug fix. --- .../authentication_view_model.dart | 11 +- .../book_appointments_view_model.dart | 37 ++++- .../my_appointments_view_model.dart | 1 + .../search_doctor_by_name.dart | 144 ++++++++++++++---- .../book_appointment/select_doctor_page.dart | 80 +++++----- .../book_appointment/widgets/doctor_card.dart | 2 +- 6 files changed, 203 insertions(+), 72 deletions(-) diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index b393772..6379456 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -246,7 +246,6 @@ class AuthenticationViewModel extends ChangeNotifier { } Future _handleNewImeiRegistration() async { - await selectDeviceImei(onSuccess: (dynamic respData) async { try { if (respData != null) { @@ -461,6 +460,12 @@ class AuthenticationViewModel extends ChangeNotifier { bool isForRegister = (_appState.getUserRegistrationPayload.healthId != null || _appState.getUserRegistrationPayload.patientOutSa == true || _appState.getUserRegistrationPayload.patientOutSa == 1); MyAppointmentsViewModel myAppointmentsVM = getIt(); + if (isSwitchUser && _appState.getSuperUserID == null) { + nationalIdController.text = responseID.toString(); + }else if( isSwitchUser && _appState.getSuperUserID != null){ + nationalIdController.text = _appState.getSuperUserID.toString(); + } + final request = RequestUtils.getCommonRequestWelcome( phoneNumber: phoneNumberController.text, otpTypeEnum: otpTypeEnum, @@ -761,14 +766,14 @@ class AuthenticationViewModel extends ChangeNotifier { phoneNumberController.text = (_appState.getAuthenticatedUser()!.mobileNumber!.startsWith("0") ? _appState.getAuthenticatedUser()!.mobileNumber!.replaceFirst("0", "") : _appState.getAuthenticatedUser()!.mobileNumber)!; - nationalIdController.text = _appState.getAuthenticatedUser()!.nationalityId!; + nationalIdController.text = _appState.getAuthenticatedUser()!.patientIdentificationNo!; onSuccess(); } else if ((loginTypeEnum == LoginTypeEnum.sms || loginTypeEnum == LoginTypeEnum.whatsapp && _appState.getSelectDeviceByImeiRespModelElement == null) && _appState.getAuthenticatedUser() != null) { phoneNumberController.text = (_appState.getAuthenticatedUser()!.mobileNumber!.startsWith("0") ? _appState.getAuthenticatedUser()!.mobileNumber!.replaceFirst("0", "") : _appState.getAuthenticatedUser()!.mobileNumber)!; - nationalIdController.text = _appState.getAuthenticatedUser()!.nationalityId!; + nationalIdController.text = _appState.getAuthenticatedUser()!.patientIdentificationNo!; onSuccess(); } } diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index aa1467b..a225c1d 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -53,6 +53,9 @@ class BookAppointmentsViewModel extends ChangeNotifier { int? calculationID = 0; bool isSortByClinic = true; + // Accordion expansion state + int? expandedGroupIndex; + int initialSlotDuration = 0; bool isNearestAppointmentSelected = false; @@ -158,13 +161,28 @@ class BookAppointmentsViewModel extends ChangeNotifier { bool isBodyPartsLoading = false; int duration = 0; - setIsSortByClinic(bool value) { isSortByClinic = value; doctorsListGrouped = isSortByClinic ? doctorsListByClinic : doctorsListByHospital; notifyListeners(); } + // Toggle accordion expansion + void toggleGroupExpansion(int index) { + if (expandedGroupIndex == index) { + expandedGroupIndex = null; + } else { + expandedGroupIndex = index; + } + notifyListeners(); + } + + // Reset accordion expansion + void resetGroupExpansion() { + expandedGroupIndex = null; + notifyListeners(); + } + // Sort filtered doctor list by clinic or hospital void sortFilteredDoctorList(bool sortByClinic) { isSortByClinic = sortByClinic; @@ -186,6 +204,22 @@ class BookAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } + // Group filtered doctors list for accordion display + List> getGroupedFilteredDoctorsList() { + final clinicMap = >{}; + final hospitalMap = >{}; + + for (var doctor in filteredDoctorList) { + final clinicKey = (doctor.clinicName ?? 'Unknown').trim(); + clinicMap.putIfAbsent(clinicKey, () => []).add(doctor); + + final hospitalKey = (doctor.projectName ?? 'Unknown').trim(); + hospitalMap.putIfAbsent(hospitalKey, () => []).add(doctor); + } + + return isSortByClinic ? clinicMap.values.toList() : hospitalMap.values.toList(); + } + // Group doctors by clinic and hospital void _groupDoctorsList() { final clinicMap = >{}; @@ -237,6 +271,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { isDoctorProfileLoading = true; isLiveCareSchedule = false; currentlySelectedHospitalFromRegionFlow = null; + expandedGroupIndex = null; // Reset accordion state clinicsList.clear(); doctorsList.clear(); liveCareClinicsList.clear(); diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index 9bd48ec..3934bf7 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -726,6 +726,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { } Future getPatientAppointmentQueueDetails({Function(dynamic)? onSuccess, Function(String)? onError}) async { + //TODO: Discuss With Haroon, Is the User Has no data it return No Element Bad State; isAppointmentQueueDetailsLoading = true; notifyListeners(); final result = await myAppointmentsRepo.getPatientAppointmentQueueDetails( diff --git a/lib/presentation/book_appointment/search_doctor_by_name.dart b/lib/presentation/book_appointment/search_doctor_by_name.dart index 559e1e5..eb6d06f 100644 --- a/lib/presentation/book_appointment/search_doctor_by_name.dart +++ b/lib/presentation/book_appointment/search_doctor_by_name.dart @@ -34,6 +34,14 @@ class _SearchDoctorByNameState extends State { TextEditingController searchEditingController = TextEditingController(); FocusNode textFocusNode = FocusNode(); late BookAppointmentsViewModel bookAppointmentsViewModel; + late ScrollController _scrollController; + final Map _itemKeys = {}; + + @override + void initState() { + _scrollController = ScrollController(); + super.initState(); + } @override Widget build(BuildContext context) { @@ -173,8 +181,11 @@ class _SearchDoctorByNameState extends State { padding: EdgeInsets.only(top: 20.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 : bookAppointmentsVM.filteredDoctorList.length, + itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 : bookAppointmentsVM.getGroupedFilteredDoctorsList().length, itemBuilder: (context, index) { + final isExpanded = bookAppointmentsVM.expandedGroupIndex == index; + final groupedDoctors = bookAppointmentsVM.getGroupedFilteredDoctorsList(); + return bookAppointmentsVM.isDoctorsListLoading ? DoctorCard( doctorsListResponseModel: DoctorsListResponseModel(), @@ -188,35 +199,113 @@ class _SearchDoctorByNameState extends State { verticalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( + key: _itemKeys.putIfAbsent(index, () => GlobalKey()), duration: Duration(milliseconds: 300), curve: Curves.easeInOut, decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: DoctorCard( - doctorsListResponseModel: bookAppointmentsVM.filteredDoctorList[index], - isLoading: false, - bookAppointmentsViewModel: bookAppointmentsViewModel, - ).onPress(() async { - bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.filteredDoctorList[index]); - // bookAppointmentsVM.setSelectedDoctor(DoctorsListResponseModel()); - LoaderBottomSheet.showLoader(); - await bookAppointmentsVM.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, - ); - }); - }), + child: InkWell( + onTap: () { + bookAppointmentsVM.toggleGroupExpansion(index); + // After rebuild, ensure the expanded item is visible + WidgetsBinding.instance.addPostFrameCallback((_) { + final key = _itemKeys[index]; + if (key != null && key.currentContext != null && bookAppointmentsVM.expandedGroupIndex == index) { + Scrollable.ensureVisible( + key.currentContext!, + duration: Duration(milliseconds: 350), + curve: Curves.easeInOut, + alignment: 0.1, + ); + } + }); + }, + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomButton( + text: "${groupedDoctors[index].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.expand_more), + ], + ), + SizedBox(height: 8.h), + // Clinic/Hospital name as group title + Text( + bookAppointmentsVM.isSortByClinic + ? (groupedDoctors[index].first.clinicName ?? 'Unknown') + : (groupedDoctors[index].first.projectName ?? 'Unknown'), + style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), + overflow: TextOverflow.ellipsis, + ), + // Expanded content - list of doctors in this group + AnimatedSwitcher( + duration: Duration(milliseconds: 400), + child: isExpanded + ? Container( + key: ValueKey(index), + padding: EdgeInsets.only(top: 12.h), + child: Column( + children: groupedDoctors[index].asMap().entries.map((entry) { + final doctorIndex = entry.key; + final doctor = entry.value; + final isLastDoctor = doctorIndex == groupedDoctors[index].length - 1; + + return Column( + children: [ + DoctorCard( + doctorsListResponseModel: doctor, + isLoading: false, + bookAppointmentsViewModel: bookAppointmentsViewModel, + ).onPress(() async { + bookAppointmentsVM.setSelectedDoctor(doctor); + LoaderBottomSheet.showLoader(); + await bookAppointmentsVM.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, + ); + }, + ); + }), + if (!isLastDoctor) + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + ], + ); + }).toList(), + ), + ) + : SizedBox.shrink(), + ), + ], + ), + ), + ), ), ), ), @@ -296,6 +385,7 @@ class _SearchDoctorByNameState extends State { @override void dispose() { + _scrollController.dispose(); bookAppointmentsViewModel.doctorsList.clear(); super.dispose(); } diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart index 569e8ac..28b0f61 100644 --- a/lib/presentation/book_appointment/select_doctor_page.dart +++ b/lib/presentation/book_appointment/select_doctor_page.dart @@ -33,7 +33,6 @@ class SelectDoctorPage extends StatefulWidget { class _SelectDoctorPageState extends State { TextEditingController searchEditingController = TextEditingController(); - int? expandedIndex; FocusNode textFocusNode = FocusNode(); @@ -170,8 +169,7 @@ class _SelectDoctorPageState extends State { ), ], ).paddingSymmetrical(0.h, 0.h), - if (bookAppointmentsViewModel.isGetDocForHealthCal && bookAppointmentsVM.showSortFilterButtons) - SizedBox(height: 16.h), + if (bookAppointmentsViewModel.isGetDocForHealthCal && bookAppointmentsVM.showSortFilterButtons) SizedBox(height: 16.h), Row( mainAxisSize: MainAxisSize.max, children: [ @@ -190,7 +188,6 @@ class _SelectDoctorPageState extends State { value: bookAppointmentsVM.isNearestAppointmentSelected, onChanged: (newValue) async { bookAppointmentsVM.setIsNearestAppointmentSelected(newValue); - }, ), ], @@ -199,11 +196,9 @@ class _SelectDoctorPageState extends State { padding: EdgeInsets.only(top: 16.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - itemCount: bookAppointmentsVM.isDoctorsListLoading - ? 5 - : (bookAppointmentsVM.doctorsListGrouped.isNotEmpty ? bookAppointmentsVM.doctorsListGrouped.length : 1), + itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 : (bookAppointmentsVM.doctorsListGrouped.isNotEmpty ? bookAppointmentsVM.doctorsListGrouped.length : 1), itemBuilder: (context, index) { - final isExpanded = expandedIndex == index; + final isExpanded = bookAppointmentsVM.expandedGroupIndex == index; return bookAppointmentsVM.isDoctorsListLoading ? DoctorCard( doctorsListResponseModel: DoctorsListResponseModel(), @@ -225,13 +220,11 @@ class _SelectDoctorPageState extends State { decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), child: InkWell( onTap: () { - setState(() { - expandedIndex = isExpanded ? null : index; - }); + bookAppointmentsVM.toggleGroupExpansion(index); // After rebuild, ensure the expanded item is visible WidgetsBinding.instance.addPostFrameCallback((_) { final key = _itemKeys[index]; - if (key != null && key.currentContext != null && expandedIndex == index) { + if (key != null && key.currentContext != null && bookAppointmentsVM.expandedGroupIndex == index) { Scrollable.ensureVisible( key.currentContext!, duration: Duration(milliseconds: 350), @@ -282,34 +275,41 @@ class _SelectDoctorPageState extends State { key: ValueKey(index), padding: EdgeInsets.only(top: 12.h), child: Column( - children: bookAppointmentsVM.doctorsListGrouped[index].map((doctor) { - return Container( - margin: EdgeInsets.only(bottom: 12.h), - child: DoctorCard( - doctorsListResponseModel: doctor, - isLoading: false, - bookAppointmentsViewModel: bookAppointmentsViewModel, - ).onPress(() async { - bookAppointmentsVM.setSelectedDoctor(doctor); - LoaderBottomSheet.showLoader(); - await bookAppointmentsVM.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, - ); - }); - }), + children: bookAppointmentsVM.doctorsListGrouped[index].asMap().entries.map((entry) { + final doctorIndex = entry.key; + final doctor = entry.value; + final isLastDoctor = doctorIndex == bookAppointmentsVM.doctorsListGrouped[index].length - 1; + + return Column( + children: [ + DoctorCard( + doctorsListResponseModel: doctor, + isLoading: false, + bookAppointmentsViewModel: bookAppointmentsViewModel, + ).onPress(() async { + bookAppointmentsVM.setSelectedDoctor(doctor); + LoaderBottomSheet.showLoader(); + await bookAppointmentsVM.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, + ); + }); + }), + if (!isLastDoctor) + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + ], ); }).toList(), ), diff --git a/lib/presentation/book_appointment/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart index ffe26ff..0f30c5f 100644 --- a/lib/presentation/book_appointment/widgets/doctor_card.dart +++ b/lib/presentation/book_appointment/widgets/doctor_card.dart @@ -38,7 +38,7 @@ class DoctorCard extends StatelessWidget { hasShadow: false, ), child: Padding( - padding: EdgeInsets.all(14.h), + padding: EdgeInsets.only(top: 14.h,bottom: 20.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ From 42222804fdacf7bc0bbaa06a7af2d18a1ae8e3cc Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Mon, 12 Jan 2026 17:25:34 +0300 Subject: [PATCH 3/3] fix graph dot and bp graph. --- .../vital_sign/vital_sign_details_page.dart | 124 ++++++++++++++---- lib/widgets/graph/custom_graph.dart | 37 ++++-- 2 files changed, 126 insertions(+), 35 deletions(-) diff --git a/lib/presentation/vital_sign/vital_sign_details_page.dart b/lib/presentation/vital_sign/vital_sign_details_page.dart index f75a71b..f632502 100644 --- a/lib/presentation/vital_sign/vital_sign_details_page.dart +++ b/lib/presentation/vital_sign/vital_sign_details_page.dart @@ -216,6 +216,14 @@ class _VitalSignDetailsPageState extends State { } Widget _historyCard(BuildContext context, {required List history}) { + // For blood pressure, we need both systolic and diastolic series + List? secondaryHistory; + if (args.metric == VitalSignMetric.bloodPressure) { + secondaryHistory = _buildBloodPressureDiastolicSeries( + context.read().vitalSignList, + ); + } + return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, @@ -282,7 +290,7 @@ class _VitalSignDetailsPageState extends State { if (history.isEmpty) Utils.getNoDataWidget(context, noDataText: 'No history available'.needTranslation, isSmallWidget: true) else if (_isGraphVisible) - _buildHistoryGraph(history) + _buildHistoryGraph(history, secondaryHistory: secondaryHistory) else _buildHistoryList(context, history), ], @@ -290,31 +298,25 @@ class _VitalSignDetailsPageState extends State { ); } - Widget _buildHistoryGraph(List history) { - final minY = _minY(history); - final maxY = _maxY(history); + Widget _buildHistoryGraph(List history, {List? secondaryHistory}) { + final minY = _minY(history, secondaryHistory: secondaryHistory); + final maxY = _maxY(history, secondaryHistory: secondaryHistory); final scheme = VitalSignUiModel.scheme(status: _statusForLatest(null), label: args.title); return CustomGraph( dataPoints: history, + secondaryDataPoints: secondaryHistory, makeGraphBasedOnActualValue: true, leftLabelReservedSize: 40, showGridLines: true, showShadow: true, - leftLabelInterval: _leftInterval(history), + leftLabelInterval: _leftInterval(history, secondaryHistory: secondaryHistory), maxY: maxY, minY: minY, maxX: history.length.toDouble() - .75, - horizontalInterval: _leftInterval(history), + horizontalInterval: _leftInterval(history, secondaryHistory: secondaryHistory), leftLabelFormatter: (value) { - // Show labels at interval points - if (args.high != null && (value - args.high!).abs() < 0.1) { - return _axisLabel('High'); - } - if (args.low != null && (value - args.low!).abs() < 0.1) { - return _axisLabel('Low'); - } - // Show numeric labels at regular intervals + // Show only numeric labels at regular intervals return _axisLabel(value.toStringAsFixed(0)); }, getDrawingHorizontalLine: (value) { @@ -341,6 +343,7 @@ class _VitalSignDetailsPageState extends State { ); }, graphColor: AppColors.bgGreenColor, + secondaryGraphColor: AppColors.blueColor, graphShadowColor: AppColors.lightGreenColor.withOpacity(.4), graphGridColor: scheme.iconFg, bottomLabelFormatter: (value, data) { @@ -350,7 +353,7 @@ class _VitalSignDetailsPageState extends State { if (value == ((data.length - 1) / 2)) return _bottomLabel(data[value.toInt()].label); return const SizedBox.shrink(); }, - rangeAnnotations: _rangeAnnotations(history), + rangeAnnotations: _rangeAnnotations(history, secondaryHistory: secondaryHistory), minX: (history.length == 1) ? null : -.2, scrollDirection: Axis.horizontal, height: 180.h, @@ -361,6 +364,15 @@ class _VitalSignDetailsPageState extends State { Widget _buildHistoryList(BuildContext context, List history) { final items = history.reversed.toList(); final height = items.length < 3 ? items.length * 64.0 : 180.h; + + // Get diastolic values if this is blood pressure + List? secondaryItems; + if (args.metric == VitalSignMetric.bloodPressure) { + final viewModel = context.read(); + final secondaryHistory = _buildBloodPressureDiastolicSeries(viewModel.vitalSignList); + secondaryItems = secondaryHistory.reversed.toList(); + } + return SizedBox( height: height, child: ListView.separated( @@ -372,13 +384,25 @@ class _VitalSignDetailsPageState extends State { ), itemBuilder: (context, index) { final dp = items[index]; + + // Build the value text based on metric type + String valueText; + if (args.metric == VitalSignMetric.bloodPressure && secondaryItems != null && index < secondaryItems.length) { + // Show systolic/diastolic for blood pressure + final diastolic = secondaryItems[index]; + valueText = '${dp.actualValue}/${diastolic.actualValue} ${dp.unitOfMeasurement ?? ''}'; + } else { + // Show single value for other metrics + valueText = '${dp.actualValue} ${dp.unitOfMeasurement ?? ''}'; + } + return Padding( padding: EdgeInsets.symmetric(vertical: 12.h), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ dp.displayTime.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), - ('${dp.actualValue} ${dp.unitOfMeasurement ?? ''}').toText12( + valueText.toText12( color: AppColors.textColor, fontWeight: FontWeight.w600, ), @@ -390,34 +414,46 @@ class _VitalSignDetailsPageState extends State { ); } - double _minY(List points) { + double _minY(List points, {List? secondaryHistory}) { // IMPORTANT: y-axis uses actual numeric values (from actualValue). final values = points.map((e) => double.tryParse(e.actualValue) ?? 0).toList(); + + // Include secondary data points if provided (for blood pressure) + if (secondaryHistory != null && secondaryHistory.isNotEmpty) { + values.addAll(secondaryHistory.map((e) => double.tryParse(e.actualValue) ?? 0)); + } + final min = values.reduce((a, b) => a < b ? a : b); final double boundLow = args.low ?? min; return (min < boundLow ? min : boundLow) - 1; } - double _maxY(List points) { + double _maxY(List points, {List? secondaryHistory}) { // IMPORTANT: y-axis uses actual numeric values (from actualValue). final values = points.map((e) => double.tryParse(e.actualValue) ?? 0).toList(); + + // Include secondary data points if provided (for blood pressure) + if (secondaryHistory != null && secondaryHistory.isNotEmpty) { + values.addAll(secondaryHistory.map((e) => double.tryParse(e.actualValue) ?? 0)); + } + final max = values.reduce((a, b) => a > b ? a : b); final double boundHigh = args.high ?? max; return (max > boundHigh ? max : boundHigh) + 1; } - double _leftInterval(List points) { + double _leftInterval(List points, {List? secondaryHistory}) { // Keep it stable; graph will mostly show just two labels. - final range = (_maxY(points) - _minY(points)).abs(); + final range = (_maxY(points, secondaryHistory: secondaryHistory) - _minY(points, secondaryHistory: secondaryHistory)).abs(); if (range <= 0) return 1; return (range / 4).clamp(1, 20); } - RangeAnnotations? _rangeAnnotations(List points) { + RangeAnnotations? _rangeAnnotations(List points, {List? secondaryHistory}) { if (args.low == null && args.high == null) return null; - final minY = _minY(points); - final maxY = _maxY(points); + final minY = _minY(points, secondaryHistory: secondaryHistory); + final maxY = _maxY(points, secondaryHistory: secondaryHistory); final List ranges = []; @@ -480,7 +516,7 @@ class _VitalSignDetailsPageState extends State { case VitalSignMetric.respiratoryRate: return _toDouble(v.respirationBeatPerMinute); case VitalSignMetric.bloodPressure: - // Graph only systolic for now (simple single-series). + // Graph systolic for primary series return _toDouble(v.bloodPressureHigher); } } @@ -513,6 +549,46 @@ class _VitalSignDetailsPageState extends State { return points; } + /// Build diastolic blood pressure series for dual-line graph + List _buildBloodPressureDiastolicSeries(List vitals) { + final List points = []; + + // Build a chronological series (oldest -> newest), skipping null/zero values. + final sorted = List.from(vitals); + sorted.sort((a, b) { + final ad = a.vitalSignDate ?? DateTime.fromMillisecondsSinceEpoch(0); + final bd = b.vitalSignDate ?? DateTime.fromMillisecondsSinceEpoch(0); + return ad.compareTo(bd); + }); + + const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + double index = 0; + for (final v in sorted) { + final diastolic = _toDouble(v.bloodPressureLower); + if (diastolic == null) continue; + if (diastolic == 0) continue; + + final dt = v.vitalSignDate ?? DateTime.now(); + final label = '${monthNames[dt.month - 1]}, ${dt.year}'; + + points.add( + DataPoint( + value: index, + label: label, + actualValue: diastolic.toStringAsFixed(0), + time: dt, + displayTime: '${dt.day}/${dt.month}/${dt.year}', + unitOfMeasurement: args.unit, + ), + ); + index += 1; + } + + return points; + } + double? _toDouble(dynamic v) { if (v == null) return null; if (v is num) return v.toDouble(); diff --git a/lib/widgets/graph/custom_graph.dart b/lib/widgets/graph/custom_graph.dart index ad47cd2..f114521 100644 --- a/lib/widgets/graph/custom_graph.dart +++ b/lib/widgets/graph/custom_graph.dart @@ -130,9 +130,23 @@ class CustomGraph extends StatelessWidget { minX: minX, lineTouchData: LineTouchData( getTouchLineEnd: (_, __) => 0, + handleBuiltInTouches: true, + touchCallback: (FlTouchEvent event, LineTouchResponse? touchResponse) { + // Let fl_chart handle the touch + }, getTouchedSpotIndicator: (barData, indicators) { - // Only show custom marker for touched spot + // Show custom marker for touched spot with correct color per line return indicators.map((int index) { + // Determine which line is being touched based on barData + Color dotColor = spotColor; + if (secondaryDataPoints != null && barData.spots.length > 0) { + // Check if this is the secondary line by comparing the first spot's color + final gradient = barData.gradient; + if (gradient != null && gradient.colors.isNotEmpty) { + dotColor = gradient.colors.first; + } + } + return TouchedSpotIndicatorData( FlLine(color: Colors.transparent), FlDotData( @@ -140,7 +154,7 @@ class CustomGraph extends StatelessWidget { getDotPainter: (spot, percent, barData, idx) { return FlDotCirclePainter( radius: 8, - color: spotColor, + color: dotColor, strokeWidth: 2, strokeColor: Colors.white, ); @@ -154,17 +168,18 @@ class CustomGraph extends StatelessWidget { getTooltipColor: (_) => Colors.white, getTooltipItems: (touchedSpots) { if (touchedSpots.isEmpty) return []; - // Only show tooltip for the first touched spot, hide others + // Show tooltip for each touched line return touchedSpots.map((spot) { - if (spot == touchedSpots.first) { - final dataPoint = dataPoints[spot.x.toInt()]; + // Determine which dataset this spot belongs to + final isSecondary = secondaryDataPoints != null && spot.barIndex == 1; + final dataPoint = isSecondary + ? secondaryDataPoints![spot.x.toInt()] + : dataPoints[spot.x.toInt()]; - return LineTooltipItem( - '${dataPoint.actualValue} ${dataPoint.unitOfMeasurement ?? ""} - ${dataPoint.displayTime}', - TextStyle(color: Colors.black, fontSize: 12.f, fontWeight: FontWeight.w500), - ); - } - return null; // hides the rest + return LineTooltipItem( + '${dataPoint.actualValue} ${dataPoint.unitOfMeasurement ?? ""} - ${dataPoint.displayTime}', + TextStyle(color: Colors.black, fontSize: 12.f, fontWeight: FontWeight.w500), + ); }).toList(); }, ),