Merge branch 'master' into haroon_dev

pull/156/head
haroon amjad 2 months ago
commit 475a9b9acb

@ -291,7 +291,8 @@ class AppDependencies {
getIt.registerLazySingleton<WaterMonitorViewModel>(() => WaterMonitorViewModel(waterMonitorRepo: getIt(), errorHandlerService: getIt())); getIt.registerLazySingleton<WaterMonitorViewModel>(() => WaterMonitorViewModel(waterMonitorRepo: getIt(), errorHandlerService: getIt()));
getIt.registerLazySingleton<MyInvoicesViewModel>(() => 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>(() => MyInvoicesViewModel(myInvoicesRepo: getIt(), errorHandlerService: getIt(), navServices: getIt()));
getIt.registerLazySingleton<MonthlyReportViewModel>(() => MonthlyReportViewModel(errorHandlerService: getIt(), monthlyReportRepo: getIt())); getIt.registerLazySingleton<MonthlyReportViewModel>(() => MonthlyReportViewModel(errorHandlerService: getIt(), monthlyReportRepo: getIt()));

@ -246,7 +246,6 @@ class AuthenticationViewModel extends ChangeNotifier {
} }
Future<void> _handleNewImeiRegistration() async { Future<void> _handleNewImeiRegistration() async {
await selectDeviceImei(onSuccess: (dynamic respData) async { await selectDeviceImei(onSuccess: (dynamic respData) async {
try { try {
if (respData != null) { 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); bool isForRegister = (_appState.getUserRegistrationPayload.healthId != null || _appState.getUserRegistrationPayload.patientOutSa == true || _appState.getUserRegistrationPayload.patientOutSa == 1);
MyAppointmentsViewModel myAppointmentsVM = getIt<MyAppointmentsViewModel>(); MyAppointmentsViewModel myAppointmentsVM = getIt<MyAppointmentsViewModel>();
if (isSwitchUser && _appState.getSuperUserID == null) {
nationalIdController.text = responseID.toString();
}else if( isSwitchUser && _appState.getSuperUserID != null){
nationalIdController.text = _appState.getSuperUserID.toString();
}
final request = RequestUtils.getCommonRequestWelcome( final request = RequestUtils.getCommonRequestWelcome(
phoneNumber: phoneNumberController.text, phoneNumber: phoneNumberController.text,
otpTypeEnum: otpTypeEnum, otpTypeEnum: otpTypeEnum,
@ -761,14 +766,14 @@ class AuthenticationViewModel extends ChangeNotifier {
phoneNumberController.text = (_appState.getAuthenticatedUser()!.mobileNumber!.startsWith("0") phoneNumberController.text = (_appState.getAuthenticatedUser()!.mobileNumber!.startsWith("0")
? _appState.getAuthenticatedUser()!.mobileNumber!.replaceFirst("0", "") ? _appState.getAuthenticatedUser()!.mobileNumber!.replaceFirst("0", "")
: _appState.getAuthenticatedUser()!.mobileNumber)!; : _appState.getAuthenticatedUser()!.mobileNumber)!;
nationalIdController.text = _appState.getAuthenticatedUser()!.nationalityId!; nationalIdController.text = _appState.getAuthenticatedUser()!.patientIdentificationNo!;
onSuccess(); onSuccess();
} else if ((loginTypeEnum == LoginTypeEnum.sms || loginTypeEnum == LoginTypeEnum.whatsapp && _appState.getSelectDeviceByImeiRespModelElement == null) && } else if ((loginTypeEnum == LoginTypeEnum.sms || loginTypeEnum == LoginTypeEnum.whatsapp && _appState.getSelectDeviceByImeiRespModelElement == null) &&
_appState.getAuthenticatedUser() != null) { _appState.getAuthenticatedUser() != null) {
phoneNumberController.text = (_appState.getAuthenticatedUser()!.mobileNumber!.startsWith("0") phoneNumberController.text = (_appState.getAuthenticatedUser()!.mobileNumber!.startsWith("0")
? _appState.getAuthenticatedUser()!.mobileNumber!.replaceFirst("0", "") ? _appState.getAuthenticatedUser()!.mobileNumber!.replaceFirst("0", "")
: _appState.getAuthenticatedUser()!.mobileNumber)!; : _appState.getAuthenticatedUser()!.mobileNumber)!;
nationalIdController.text = _appState.getAuthenticatedUser()!.nationalityId!; nationalIdController.text = _appState.getAuthenticatedUser()!.patientIdentificationNo!;
onSuccess(); onSuccess();
} }
} }

@ -53,6 +53,9 @@ class BookAppointmentsViewModel extends ChangeNotifier {
int? calculationID = 0; int? calculationID = 0;
bool isSortByClinic = true; bool isSortByClinic = true;
// Accordion expansion state
int? expandedGroupIndex;
int initialSlotDuration = 0; int initialSlotDuration = 0;
bool isNearestAppointmentSelected = false; bool isNearestAppointmentSelected = false;
@ -158,13 +161,28 @@ class BookAppointmentsViewModel extends ChangeNotifier {
bool isBodyPartsLoading = false; bool isBodyPartsLoading = false;
int duration = 0; int duration = 0;
setIsSortByClinic(bool value) { setIsSortByClinic(bool value) {
isSortByClinic = value; isSortByClinic = value;
doctorsListGrouped = isSortByClinic ? doctorsListByClinic : doctorsListByHospital; doctorsListGrouped = isSortByClinic ? doctorsListByClinic : doctorsListByHospital;
notifyListeners(); 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 // Sort filtered doctor list by clinic or hospital
void sortFilteredDoctorList(bool sortByClinic) { void sortFilteredDoctorList(bool sortByClinic) {
isSortByClinic = sortByClinic; isSortByClinic = sortByClinic;
@ -186,6 +204,22 @@ class BookAppointmentsViewModel extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
// Group filtered doctors list for accordion display
List<List<DoctorsListResponseModel>> getGroupedFilteredDoctorsList() {
final clinicMap = <String, List<DoctorsListResponseModel>>{};
final hospitalMap = <String, List<DoctorsListResponseModel>>{};
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 // Group doctors by clinic and hospital
void _groupDoctorsList() { void _groupDoctorsList() {
final clinicMap = <String, List<DoctorsListResponseModel>>{}; final clinicMap = <String, List<DoctorsListResponseModel>>{};
@ -237,6 +271,7 @@ class BookAppointmentsViewModel extends ChangeNotifier {
isDoctorProfileLoading = true; isDoctorProfileLoading = true;
isLiveCareSchedule = false; isLiveCareSchedule = false;
currentlySelectedHospitalFromRegionFlow = null; currentlySelectedHospitalFromRegionFlow = null;
expandedGroupIndex = null; // Reset accordion state
clinicsList.clear(); clinicsList.clear();
doctorsList.clear(); doctorsList.clear();
liveCareClinicsList.clear(); liveCareClinicsList.clear();

@ -726,6 +726,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
} }
Future<void> getPatientAppointmentQueueDetails({Function(dynamic)? onSuccess, Function(String)? onError}) async { Future<void> 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; isAppointmentQueueDetailsLoading = true;
notifyListeners(); notifyListeners();
final result = await myAppointmentsRepo.getPatientAppointmentQueueDetails( final result = await myAppointmentsRepo.getPatientAppointmentQueueDetails(

@ -34,6 +34,14 @@ class _SearchDoctorByNameState extends State<SearchDoctorByName> {
TextEditingController searchEditingController = TextEditingController(); TextEditingController searchEditingController = TextEditingController();
FocusNode textFocusNode = FocusNode(); FocusNode textFocusNode = FocusNode();
late BookAppointmentsViewModel bookAppointmentsViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel;
late ScrollController _scrollController;
final Map<int, GlobalKey> _itemKeys = {};
@override
void initState() {
_scrollController = ScrollController();
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -173,8 +181,11 @@ class _SearchDoctorByNameState extends State<SearchDoctorByName> {
padding: EdgeInsets.only(top: 20.h), padding: EdgeInsets.only(top: 20.h),
shrinkWrap: true, shrinkWrap: true,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 : bookAppointmentsVM.filteredDoctorList.length, itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 : bookAppointmentsVM.getGroupedFilteredDoctorsList().length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final isExpanded = bookAppointmentsVM.expandedGroupIndex == index;
final groupedDoctors = bookAppointmentsVM.getGroupedFilteredDoctorsList();
return bookAppointmentsVM.isDoctorsListLoading return bookAppointmentsVM.isDoctorsListLoading
? DoctorCard( ? DoctorCard(
doctorsListResponseModel: DoctorsListResponseModel(), doctorsListResponseModel: DoctorsListResponseModel(),
@ -188,35 +199,113 @@ class _SearchDoctorByNameState extends State<SearchDoctorByName> {
verticalOffset: 100.0, verticalOffset: 100.0,
child: FadeInAnimation( child: FadeInAnimation(
child: AnimatedContainer( child: AnimatedContainer(
key: _itemKeys.putIfAbsent(index, () => GlobalKey()),
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true),
child: DoctorCard( child: InkWell(
doctorsListResponseModel: bookAppointmentsVM.filteredDoctorList[index], onTap: () {
isLoading: false, bookAppointmentsVM.toggleGroupExpansion(index);
bookAppointmentsViewModel: bookAppointmentsViewModel, // After rebuild, ensure the expanded item is visible
).onPress(() async { WidgetsBinding.instance.addPostFrameCallback((_) {
bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.filteredDoctorList[index]); final key = _itemKeys[index];
// bookAppointmentsVM.setSelectedDoctor(DoctorsListResponseModel()); if (key != null && key.currentContext != null && bookAppointmentsVM.expandedGroupIndex == index) {
LoaderBottomSheet.showLoader(); Scrollable.ensureVisible(
await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) { key.currentContext!,
LoaderBottomSheet.hideLoader(); duration: Duration(milliseconds: 350),
Navigator.of(context).push( curve: Curves.easeInOut,
CustomPageRoute( alignment: 0.1,
page: DoctorProfilePage(), );
), }
); });
}, onError: (err) { },
LoaderBottomSheet.hideLoader(); child: Padding(
showCommonBottomSheetWithoutHeight( padding: EdgeInsets.all(16.h),
context, child: Column(
child: Utils.getErrorWidget(loadingText: err), crossAxisAlignment: CrossAxisAlignment.start,
callBackFunc: () {}, children: [
isFullScreen: false, Row(
isCloseButtonVisible: true, 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<int>(index),
padding: EdgeInsets.only(top: 12.h),
child: Column(
children: groupedDoctors[index].asMap().entries.map<Widget>((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<SearchDoctorByName> {
@override @override
void dispose() { void dispose() {
_scrollController.dispose();
bookAppointmentsViewModel.doctorsList.clear(); bookAppointmentsViewModel.doctorsList.clear();
super.dispose(); super.dispose();
} }

@ -33,7 +33,6 @@ class SelectDoctorPage extends StatefulWidget {
class _SelectDoctorPageState extends State<SelectDoctorPage> { class _SelectDoctorPageState extends State<SelectDoctorPage> {
TextEditingController searchEditingController = TextEditingController(); TextEditingController searchEditingController = TextEditingController();
int? expandedIndex;
FocusNode textFocusNode = FocusNode(); FocusNode textFocusNode = FocusNode();
@ -170,8 +169,7 @@ class _SelectDoctorPageState extends State<SelectDoctorPage> {
), ),
], ],
).paddingSymmetrical(0.h, 0.h), ).paddingSymmetrical(0.h, 0.h),
if (bookAppointmentsViewModel.isGetDocForHealthCal && bookAppointmentsVM.showSortFilterButtons) if (bookAppointmentsViewModel.isGetDocForHealthCal && bookAppointmentsVM.showSortFilterButtons) SizedBox(height: 16.h),
SizedBox(height: 16.h),
Row( Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: [ children: [
@ -190,7 +188,6 @@ class _SelectDoctorPageState extends State<SelectDoctorPage> {
value: bookAppointmentsVM.isNearestAppointmentSelected, value: bookAppointmentsVM.isNearestAppointmentSelected,
onChanged: (newValue) async { onChanged: (newValue) async {
bookAppointmentsVM.setIsNearestAppointmentSelected(newValue); bookAppointmentsVM.setIsNearestAppointmentSelected(newValue);
}, },
), ),
], ],
@ -199,11 +196,9 @@ class _SelectDoctorPageState extends State<SelectDoctorPage> {
padding: EdgeInsets.only(top: 16.h), padding: EdgeInsets.only(top: 16.h),
shrinkWrap: true, shrinkWrap: true,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
itemCount: bookAppointmentsVM.isDoctorsListLoading itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 : (bookAppointmentsVM.doctorsListGrouped.isNotEmpty ? bookAppointmentsVM.doctorsListGrouped.length : 1),
? 5
: (bookAppointmentsVM.doctorsListGrouped.isNotEmpty ? bookAppointmentsVM.doctorsListGrouped.length : 1),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final isExpanded = expandedIndex == index; final isExpanded = bookAppointmentsVM.expandedGroupIndex == index;
return bookAppointmentsVM.isDoctorsListLoading return bookAppointmentsVM.isDoctorsListLoading
? DoctorCard( ? DoctorCard(
doctorsListResponseModel: DoctorsListResponseModel(), doctorsListResponseModel: DoctorsListResponseModel(),
@ -225,13 +220,11 @@ class _SelectDoctorPageState extends State<SelectDoctorPage> {
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
setState(() { bookAppointmentsVM.toggleGroupExpansion(index);
expandedIndex = isExpanded ? null : index;
});
// After rebuild, ensure the expanded item is visible // After rebuild, ensure the expanded item is visible
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final key = _itemKeys[index]; final key = _itemKeys[index];
if (key != null && key.currentContext != null && expandedIndex == index) { if (key != null && key.currentContext != null && bookAppointmentsVM.expandedGroupIndex == index) {
Scrollable.ensureVisible( Scrollable.ensureVisible(
key.currentContext!, key.currentContext!,
duration: Duration(milliseconds: 350), duration: Duration(milliseconds: 350),
@ -282,34 +275,41 @@ class _SelectDoctorPageState extends State<SelectDoctorPage> {
key: ValueKey<int>(index), key: ValueKey<int>(index),
padding: EdgeInsets.only(top: 12.h), padding: EdgeInsets.only(top: 12.h),
child: Column( child: Column(
children: bookAppointmentsVM.doctorsListGrouped[index].map<Widget>((doctor) { children: bookAppointmentsVM.doctorsListGrouped[index].asMap().entries.map<Widget>((entry) {
return Container( final doctorIndex = entry.key;
margin: EdgeInsets.only(bottom: 12.h), final doctor = entry.value;
child: DoctorCard( final isLastDoctor = doctorIndex == bookAppointmentsVM.doctorsListGrouped[index].length - 1;
doctorsListResponseModel: doctor,
isLoading: false, return Column(
bookAppointmentsViewModel: bookAppointmentsViewModel, children: [
).onPress(() async { DoctorCard(
bookAppointmentsVM.setSelectedDoctor(doctor); doctorsListResponseModel: doctor,
LoaderBottomSheet.showLoader(); isLoading: false,
await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) { bookAppointmentsViewModel: bookAppointmentsViewModel,
LoaderBottomSheet.hideLoader(); ).onPress(() async {
Navigator.of(context).push( bookAppointmentsVM.setSelectedDoctor(doctor);
CustomPageRoute( LoaderBottomSheet.showLoader();
page: DoctorProfilePage(), await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) {
), LoaderBottomSheet.hideLoader();
); Navigator.of(context).push(
}, onError: (err) { CustomPageRoute(
LoaderBottomSheet.hideLoader(); page: DoctorProfilePage(),
showCommonBottomSheetWithoutHeight( ),
context, );
child: Utils.getErrorWidget(loadingText: err), }, onError: (err) {
callBackFunc: () {}, LoaderBottomSheet.hideLoader();
isFullScreen: false, showCommonBottomSheetWithoutHeight(
isCloseButtonVisible: true, 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(), }).toList(),
), ),

@ -38,7 +38,7 @@ class DoctorCard extends StatelessWidget {
hasShadow: false, hasShadow: false,
), ),
child: Padding( child: Padding(
padding: EdgeInsets.all(14.h), padding: EdgeInsets.only(top: 14.h,bottom: 20.h),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

@ -216,6 +216,14 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
} }
Widget _historyCard(BuildContext context, {required List<DataPoint> history}) { Widget _historyCard(BuildContext context, {required List<DataPoint> history}) {
// For blood pressure, we need both systolic and diastolic series
List<DataPoint>? secondaryHistory;
if (args.metric == VitalSignMetric.bloodPressure) {
secondaryHistory = _buildBloodPressureDiastolicSeries(
context.read<HmgServicesViewModel>().vitalSignList,
);
}
return Container( return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor, color: AppColors.whiteColor,
@ -282,7 +290,7 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
if (history.isEmpty) if (history.isEmpty)
Utils.getNoDataWidget(context, noDataText: 'No history available'.needTranslation, isSmallWidget: true) Utils.getNoDataWidget(context, noDataText: 'No history available'.needTranslation, isSmallWidget: true)
else if (_isGraphVisible) else if (_isGraphVisible)
_buildHistoryGraph(history) _buildHistoryGraph(history, secondaryHistory: secondaryHistory)
else else
_buildHistoryList(context, history), _buildHistoryList(context, history),
], ],
@ -290,31 +298,25 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
); );
} }
Widget _buildHistoryGraph(List<DataPoint> history) { Widget _buildHistoryGraph(List<DataPoint> history, {List<DataPoint>? secondaryHistory}) {
final minY = _minY(history); final minY = _minY(history, secondaryHistory: secondaryHistory);
final maxY = _maxY(history); final maxY = _maxY(history, secondaryHistory: secondaryHistory);
final scheme = VitalSignUiModel.scheme(status: _statusForLatest(null), label: args.title); final scheme = VitalSignUiModel.scheme(status: _statusForLatest(null), label: args.title);
return CustomGraph( return CustomGraph(
dataPoints: history, dataPoints: history,
secondaryDataPoints: secondaryHistory,
makeGraphBasedOnActualValue: true, makeGraphBasedOnActualValue: true,
leftLabelReservedSize: 40, leftLabelReservedSize: 40,
showGridLines: true, showGridLines: true,
showShadow: true, showShadow: true,
leftLabelInterval: _leftInterval(history), leftLabelInterval: _leftInterval(history, secondaryHistory: secondaryHistory),
maxY: maxY, maxY: maxY,
minY: minY, minY: minY,
maxX: history.length.toDouble() - .75, maxX: history.length.toDouble() - .75,
horizontalInterval: _leftInterval(history), horizontalInterval: _leftInterval(history, secondaryHistory: secondaryHistory),
leftLabelFormatter: (value) { leftLabelFormatter: (value) {
// Show labels at interval points // Show only numeric labels at regular intervals
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
return _axisLabel(value.toStringAsFixed(0)); return _axisLabel(value.toStringAsFixed(0));
}, },
getDrawingHorizontalLine: (value) { getDrawingHorizontalLine: (value) {
@ -341,6 +343,7 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
); );
}, },
graphColor: AppColors.bgGreenColor, graphColor: AppColors.bgGreenColor,
secondaryGraphColor: AppColors.blueColor,
graphShadowColor: AppColors.lightGreenColor.withOpacity(.4), graphShadowColor: AppColors.lightGreenColor.withOpacity(.4),
graphGridColor: scheme.iconFg, graphGridColor: scheme.iconFg,
bottomLabelFormatter: (value, data) { bottomLabelFormatter: (value, data) {
@ -350,7 +353,7 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
if (value == ((data.length - 1) / 2)) return _bottomLabel(data[value.toInt()].label); if (value == ((data.length - 1) / 2)) return _bottomLabel(data[value.toInt()].label);
return const SizedBox.shrink(); return const SizedBox.shrink();
}, },
rangeAnnotations: _rangeAnnotations(history), rangeAnnotations: _rangeAnnotations(history, secondaryHistory: secondaryHistory),
minX: (history.length == 1) ? null : -.2, minX: (history.length == 1) ? null : -.2,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
height: 180.h, height: 180.h,
@ -361,6 +364,15 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
Widget _buildHistoryList(BuildContext context, List<DataPoint> history) { Widget _buildHistoryList(BuildContext context, List<DataPoint> history) {
final items = history.reversed.toList(); final items = history.reversed.toList();
final height = items.length < 3 ? items.length * 64.0 : 180.h; final height = items.length < 3 ? items.length * 64.0 : 180.h;
// Get diastolic values if this is blood pressure
List<DataPoint>? secondaryItems;
if (args.metric == VitalSignMetric.bloodPressure) {
final viewModel = context.read<HmgServicesViewModel>();
final secondaryHistory = _buildBloodPressureDiastolicSeries(viewModel.vitalSignList);
secondaryItems = secondaryHistory.reversed.toList();
}
return SizedBox( return SizedBox(
height: height, height: height,
child: ListView.separated( child: ListView.separated(
@ -372,13 +384,25 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
), ),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final dp = items[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( return Padding(
padding: EdgeInsets.symmetric(vertical: 12.h), padding: EdgeInsets.symmetric(vertical: 12.h),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
dp.displayTime.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), dp.displayTime.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500),
('${dp.actualValue} ${dp.unitOfMeasurement ?? ''}').toText12( valueText.toText12(
color: AppColors.textColor, color: AppColors.textColor,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
@ -390,34 +414,46 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
); );
} }
double _minY(List<DataPoint> points) { double _minY(List<DataPoint> points, {List<DataPoint>? secondaryHistory}) {
// IMPORTANT: y-axis uses actual numeric values (from actualValue). // IMPORTANT: y-axis uses actual numeric values (from actualValue).
final values = points.map((e) => double.tryParse(e.actualValue) ?? 0).toList(); 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 min = values.reduce((a, b) => a < b ? a : b);
final double boundLow = args.low ?? min; final double boundLow = args.low ?? min;
return (min < boundLow ? min : boundLow) - 1; return (min < boundLow ? min : boundLow) - 1;
} }
double _maxY(List<DataPoint> points) { double _maxY(List<DataPoint> points, {List<DataPoint>? secondaryHistory}) {
// IMPORTANT: y-axis uses actual numeric values (from actualValue). // IMPORTANT: y-axis uses actual numeric values (from actualValue).
final values = points.map((e) => double.tryParse(e.actualValue) ?? 0).toList(); 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 max = values.reduce((a, b) => a > b ? a : b);
final double boundHigh = args.high ?? max; final double boundHigh = args.high ?? max;
return (max > boundHigh ? max : boundHigh) + 1; return (max > boundHigh ? max : boundHigh) + 1;
} }
double _leftInterval(List<DataPoint> points) { double _leftInterval(List<DataPoint> points, {List<DataPoint>? secondaryHistory}) {
// Keep it stable; graph will mostly show just two labels. // 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; if (range <= 0) return 1;
return (range / 4).clamp(1, 20); return (range / 4).clamp(1, 20);
} }
RangeAnnotations? _rangeAnnotations(List<DataPoint> points) { RangeAnnotations? _rangeAnnotations(List<DataPoint> points, {List<DataPoint>? secondaryHistory}) {
if (args.low == null && args.high == null) return null; if (args.low == null && args.high == null) return null;
final minY = _minY(points); final minY = _minY(points, secondaryHistory: secondaryHistory);
final maxY = _maxY(points); final maxY = _maxY(points, secondaryHistory: secondaryHistory);
final List<HorizontalRangeAnnotation> ranges = []; final List<HorizontalRangeAnnotation> ranges = [];
@ -480,7 +516,7 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
case VitalSignMetric.respiratoryRate: case VitalSignMetric.respiratoryRate:
return _toDouble(v.respirationBeatPerMinute); return _toDouble(v.respirationBeatPerMinute);
case VitalSignMetric.bloodPressure: case VitalSignMetric.bloodPressure:
// Graph only systolic for now (simple single-series). // Graph systolic for primary series
return _toDouble(v.bloodPressureHigher); return _toDouble(v.bloodPressureHigher);
} }
} }
@ -513,6 +549,46 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
return points; return points;
} }
/// Build diastolic blood pressure series for dual-line graph
List<DataPoint> _buildBloodPressureDiastolicSeries(List<VitalSignResModel> vitals) {
final List<DataPoint> points = [];
// Build a chronological series (oldest -> newest), skipping null/zero values.
final sorted = List<VitalSignResModel>.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) { double? _toDouble(dynamic v) {
if (v == null) return null; if (v == null) return null;
if (v is num) return v.toDouble(); if (v is num) return v.toDouble();

@ -130,9 +130,23 @@ class CustomGraph extends StatelessWidget {
minX: minX, minX: minX,
lineTouchData: LineTouchData( lineTouchData: LineTouchData(
getTouchLineEnd: (_, __) => 0, getTouchLineEnd: (_, __) => 0,
handleBuiltInTouches: true,
touchCallback: (FlTouchEvent event, LineTouchResponse? touchResponse) {
// Let fl_chart handle the touch
},
getTouchedSpotIndicator: (barData, indicators) { 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) { 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( return TouchedSpotIndicatorData(
FlLine(color: Colors.transparent), FlLine(color: Colors.transparent),
FlDotData( FlDotData(
@ -140,7 +154,7 @@ class CustomGraph extends StatelessWidget {
getDotPainter: (spot, percent, barData, idx) { getDotPainter: (spot, percent, barData, idx) {
return FlDotCirclePainter( return FlDotCirclePainter(
radius: 8, radius: 8,
color: spotColor, color: dotColor,
strokeWidth: 2, strokeWidth: 2,
strokeColor: Colors.white, strokeColor: Colors.white,
); );
@ -154,17 +168,18 @@ class CustomGraph extends StatelessWidget {
getTooltipColor: (_) => Colors.white, getTooltipColor: (_) => Colors.white,
getTooltipItems: (touchedSpots) { getTooltipItems: (touchedSpots) {
if (touchedSpots.isEmpty) return []; if (touchedSpots.isEmpty) return [];
// Only show tooltip for the first touched spot, hide others // Show tooltip for each touched line
return touchedSpots.map((spot) { return touchedSpots.map((spot) {
if (spot == touchedSpots.first) { // Determine which dataset this spot belongs to
final dataPoint = dataPoints[spot.x.toInt()]; final isSecondary = secondaryDataPoints != null && spot.barIndex == 1;
final dataPoint = isSecondary
? secondaryDataPoints![spot.x.toInt()]
: dataPoints[spot.x.toInt()];
return LineTooltipItem( return LineTooltipItem(
'${dataPoint.actualValue} ${dataPoint.unitOfMeasurement ?? ""} - ${dataPoint.displayTime}', '${dataPoint.actualValue} ${dataPoint.unitOfMeasurement ?? ""} - ${dataPoint.displayTime}',
TextStyle(color: Colors.black, fontSize: 12.f, fontWeight: FontWeight.w500), TextStyle(color: Colors.black, fontSize: 12.f, fontWeight: FontWeight.w500),
); );
}
return null; // hides the rest
}).toList(); }).toList();
}, },
), ),

Loading…
Cancel
Save