From 948416da7415150f87ed8866f603be93e597621a Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 30 Jun 2026 10:10:41 +0300 Subject: [PATCH 01/12] Update to stores VersionID 21.4 --- lib/core/api_consts.dart | 2 +- lib/core/app_state.dart | 2 +- pubspec.yaml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 664432fd..089f371b 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -301,7 +301,7 @@ class ApiConsts { static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations"; // ************ static values for Api **************** - static final double appVersionID = 21.3; + static final double appVersionID = 21.4; // static final double appVersionID = 50.7; static final int appChannelId = 3; diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index 2731cc1a..a27b727e 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -56,7 +56,7 @@ class AppState { void setIsPayfortHostedPageEnabled(bool value) { isPayfortHostedPageEnabled = value; - // isPayfortHostedPageEnabled = true; + // isPayfortHostedPageEnabled = false; } int? _superUserID; diff --git a/pubspec.yaml b/pubspec.yaml index ba0c0ed5..eafbd513 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,8 +2,8 @@ name: hmg_patient_app_new description: "New HMG Patient App" publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 0.0.37+36 -#version: 0.0.6+3 +#version: 0.0.38+37 +version: 0.0.7+1 environment: sdk: ">=3.6.0 <4.0.0" From e867de7b71534f0fca8898fe0fa58a363deb2712 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 30 Jun 2026 15:57:28 +0300 Subject: [PATCH 02/12] updates --- lib/features/authentication/authentication_view_model.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index bb6def9e..b51b36e4 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -1209,13 +1209,15 @@ class AuthenticationViewModel extends ChangeNotifier { } // Fetch profile image after switching user - await _fetchProfileImage(activation.list!.first.patientId); + // await _fetchProfileImage(activation.list!.first.patientId); + _fetchProfileImage(activation.list!.first.patientId); } else { activation.list!.first.isParentUser = true; _appState.addUserSwitchPayload(activation.list!.first.toJson(), isSuperUser: true); // Fetch profile image after login - await _fetchProfileImage(activation.list!.first.patientId); + // await _fetchProfileImage(activation.list!.first.patientId); + _fetchProfileImage(activation.list!.first.patientId); } activation.list!.first.bloodGroup = activation.patientBloodType; activation.list!.first.zipCode = selectedCountrySignup == CountryEnum.others ? '0' : selectedCountrySignup.countryCode; From 13f2898a8bf30514103dd757f387fd4d3bf3e658 Mon Sep 17 00:00:00 2001 From: Haroon Amjad <> Date: Tue, 30 Jun 2026 20:47:33 +0300 Subject: [PATCH 03/12] Fixes for Apple Watch integration --- .../health_data_transformations.dart | 151 ++++++++++-------- .../health_service.dart | 16 +- .../model/vitals_data_model.dart | 14 +- .../health_connect_helper.dart | 49 +++++- .../smartwatches/activity_detail.dart | 3 +- 5 files changed, 148 insertions(+), 85 deletions(-) diff --git a/lib/features/smartwatch_health_data/health_data_transformations.dart b/lib/features/smartwatch_health_data/health_data_transformations.dart index 6f777353..1aeff65b 100644 --- a/lib/features/smartwatch_health_data/health_data_transformations.dart +++ b/lib/features/smartwatch_health_data/health_data_transformations.dart @@ -13,80 +13,90 @@ enum Durations { yearly("yearly"); final String value; + const Durations(this.value); } class HealthDataTransformation { - Map> transformVitalsToDataPoints(VitalsWRTType vitals, String filterType, String selectedSection,) { - final Map> dataPointMap = {}; + Map> transformVitalsToDataPoints( + VitalsWRTType vitals, + String filterType, + String selectedSection, + ) { + final Map> dataPointMap = {}; Map> data = vitals.getVitals(); // Group data based on the filter type Map> groupedData = {}; // List > items = data.values.toList(); List keys = data.keys.toList(); var count = 0; - List item = data[selectedSection] ?? []; + List item = data[selectedSection] ?? []; // for(var item in items) { - List dataPoints = []; - - for (var vital in item) { - String key = ""; - if (vital.value == "" || vital.timestamp == "") continue; - var parseDate = DateTime.parse(vital.timestamp); - var currentDate = normalizeToStartOfDay(DateTime.now()); - if (filterType == Durations.daily.value) { - if(isBetweenInclusive(parseDate, currentDate, DateTime.now())) { - key = DateFormat('yyyy-MM-dd HH').format(DateTime.parse(vital.timestamp)); - groupedData.putIfAbsent(key, () => []).add(vital); - }// Group by hour - } else if (filterType == Durations.weekly.value) { - if(isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 7)), DateTime.now())) { - key = DateFormat('yyyy-MM-dd').format(DateTime.parse(vital.timestamp)); - groupedData.putIfAbsent(key, () => []).add(vital); - - } // Group by day - } else if (filterType == Durations.monthly.value) { - if(isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 30)), DateTime.now())) { - print("the value for the monthly filter is ${vital.value} with the timestamp ${vital.timestamp} and the current date is $currentDate and the parse date is $parseDate"); - key = DateFormat('yyyy-MM-dd').format(DateTime.parse(vital.timestamp)); - groupedData.putIfAbsent(key, () => []).add(vital); - - } // Group by day - } else if (filterType == Durations.halfYearly.value || filterType == Durations.yearly.value) { - if(isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: filterType == Durations.halfYearly.value?180: 365)), DateTime.now())) { - key = DateFormat('yyyy-MM').format(DateTime.parse(vital.timestamp)); - groupedData.putIfAbsent(key, () => []).add(vital); - - } // Group by month - } else { - throw ArgumentError('Invalid filter type'); - } + List dataPoints = []; + + for (var vital in item) { + String key = ""; + if (vital.value == "" || vital.timestamp == "") continue; + + DateTime parseDate; + try { + parseDate = DateTime.parse(vital.timestamp); + } catch (e) { + print('Error parsing timestamp "${vital.timestamp}": $e'); + continue; // Skip this vital if timestamp is invalid + } + + var currentDate = normalizeToStartOfDay(DateTime.now()); + if (filterType == Durations.daily.value) { + if (isBetweenInclusive(parseDate, currentDate, DateTime.now())) { + key = DateFormat('yyyy-MM-dd HH').format(parseDate); + groupedData.putIfAbsent(key, () => []).add(vital); + } // Group by hour + } else if (filterType == Durations.weekly.value) { + if (isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 7)), DateTime.now())) { + key = DateFormat('yyyy-MM-dd').format(parseDate); + groupedData.putIfAbsent(key, () => []).add(vital); + } // Group by day + } else if (filterType == Durations.monthly.value) { + if (isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 30)), DateTime.now())) { + print("the value for the monthly filter is ${vital.value} with the timestamp ${vital.timestamp} and the current date is $currentDate and the parse date is $parseDate"); + key = DateFormat('yyyy-MM-dd').format(parseDate); + groupedData.putIfAbsent(key, () => []).add(vital); + } // Group by day + } else if (filterType == Durations.halfYearly.value || filterType == Durations.yearly.value) { + if (isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: filterType == Durations.halfYearly.value ? 180 : 365)), DateTime.now())) { + key = DateFormat('yyyy-MM').format(parseDate); + groupedData.putIfAbsent(key, () => []).add(vital); + } // Group by month + } else { + throw ArgumentError('Invalid filter type'); } - print("the size of groupData is ${groupedData.values.length}"); - - // Process grouped data - groupedData.forEach((key, values) { - double sum = values.fold(0, (acc, v) => acc + num.parse(v.value)); - double mean = sum / values.length; - if(selectedSection == "bodyOxygen" || selectedSection == "bodyTemperature") { - mean = sum / values.length; - }else { - mean = sum; - } - - double finalValue = mean; - print("the final value is $finalValue for the key $key with the original values ${values.map((v) => v.value).toList()} and uom is ${values.first.unitOfMeasure}"); - dataPoints.add(DataPoint( - value: smartScale(finalValue), - label: key, - actualValue: finalValue.toStringAsFixed(2), - displayTime: key, - unitOfMeasurement:values.first.unitOfMeasure , - time: DateTime.parse(values.first.timestamp), - )); - }); - - dataPointMap[filterType] = dataPoints; + } + print("the size of groupData is ${groupedData.values.length}"); + + // Process grouped data + groupedData.forEach((key, values) { + double sum = values.fold(0, (acc, v) => acc + num.parse(v.value)); + double mean = sum / values.length; + if (selectedSection == "bodyOxygen" || selectedSection == "bodyTemperature") { + mean = sum / values.length; + } else { + mean = sum; + } + + double finalValue = mean; + print("the final value is $finalValue for the key $key with the original values ${values.map((v) => v.value).toList()} and uom is ${values.first.unitOfMeasure}"); + dataPoints.add(DataPoint( + value: smartScale(finalValue), + label: key, + actualValue: finalValue.toStringAsFixed(2), + displayTime: key, + unitOfMeasurement: values.first.unitOfMeasure, + time: DateTime.parse(values.first.timestamp), + )); + }); + + dataPointMap[filterType] = dataPoints; // } return dataPointMap; } @@ -109,10 +119,10 @@ class HealthDataTransformation { final random = Random(); // Smooth compression scaling - double baseScaled = number <20 ? number:100 * (number / (number + 100)); + double baseScaled = number < 20 ? number : 100 * (number / (number + 100)); // Random factor between 0.9 and 1.1 (±10%) - double randomFactor = number <20 ? random.nextDouble() * 1.5: 0.9 + random.nextDouble() * 0.2; + double randomFactor = number < 20 ? random.nextDouble() * 1.5 : 0.9 + random.nextDouble() * 0.2; double result = baseScaled * randomFactor; @@ -122,13 +132,12 @@ class HealthDataTransformation { DateTime normalizeToStartOfDay(DateTime date) { return DateTime(date.year, date.month, date.day); } + bool isBetweenInclusive( - DateTime target, - DateTime start, - DateTime end, - ) { + DateTime target, + DateTime start, + DateTime end, + ) { return !normalizeToStartOfDay(target).isBefore(start) && !normalizeToStartOfDay(target).isAfter(end); } - - -} \ No newline at end of file +} diff --git a/lib/features/smartwatch_health_data/health_service.dart b/lib/features/smartwatch_health_data/health_service.dart index 78411bfb..9e6c91b2 100644 --- a/lib/features/smartwatch_health_data/health_service.dart +++ b/lib/features/smartwatch_health_data/health_service.dart @@ -172,12 +172,12 @@ class HealthService { } } - void addWatchHelper(SmartWatchTypes watchType){ - watchHelper = CreateWatchHelper.getWatchName(watchType) ; + void addWatchHelper(SmartWatchTypes watchType) { + watchHelper = CreateWatchHelper.getWatchName(watchType); } Future> initDevice() async { - if(watchHelper == null){ + if (watchHelper == null) { return Result.error('No watch helper found'); } return await watchHelper!.initDevice(); @@ -197,14 +197,14 @@ class HealthService { await watchHelper!.getDistance(); await watchHelper!.getBloodOxygen(); Result data = await watchHelper!.retrieveData(); - if(data.isError) { + if (data.isError) { print('Unable to get the data'); } - var response = jsonDecode(data.asValue?.value?.toString()?.trim().replaceAll("\n", "")??""); - VitalsWRTType vitals = VitalsWRTType.fromMap(response); - log("the data is ${vitals}"); + var response = jsonDecode(data.asValue?.value?.toString()?.trim().replaceAll("\n", "") ?? ""); + VitalsWRTType vitals = VitalsWRTType.fromMap(response); + // log("the data is ${vitals}"); return vitals; - }catch(e){ + } catch (e) { print('Error getting heart rate: $e'); } return null; diff --git a/lib/features/smartwatch_health_data/model/vitals_data_model.dart b/lib/features/smartwatch_health_data/model/vitals_data_model.dart index ba1ac9f1..83cd4ea6 100644 --- a/lib/features/smartwatch_health_data/model/vitals_data_model.dart +++ b/lib/features/smartwatch_health_data/model/vitals_data_model.dart @@ -9,10 +9,22 @@ class Vitals { this.unitOfMeasure = "", }); + Map toJson() { + return { + 'value': value, + 'timestamp': timestamp, + 'uom': unitOfMeasure, + }; + } + factory Vitals.fromMap(Map map) { + String timestamp = map['timestamp'] ?? map['timeStamp'] ?? ""; + if (timestamp.isEmpty) { + timestamp = DateTime.now().toIso8601String(); + } return Vitals( value: map['value'] ?? "", - timestamp: map['timeStamp'] ?? "", + timestamp: timestamp, unitOfMeasure: map['uom'] ?? "", ); } diff --git a/lib/features/smartwatch_health_data/watch_connectors/health_connect_helper.dart b/lib/features/smartwatch_health_data/watch_connectors/health_connect_helper.dart index b77b5430..aeca1e87 100644 --- a/lib/features/smartwatch_health_data/watch_connectors/health_connect_helper.dart +++ b/lib/features/smartwatch_health_data/watch_connectors/health_connect_helper.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:async/src/result/result.dart'; import 'package:flutter/material.dart'; @@ -80,7 +81,10 @@ class HealthConnectHelper extends WatchHelper { @override Future retrieveData() async { + // String retData; + // retData = Result.value(getMappedData()) as String; return Result.value(getMappedData()); + // return retData; } @override @@ -151,9 +155,22 @@ class HealthConnectHelper extends WatchHelper { for (var point in data) { if (point.value is NumericHealthValue) { final numericValue = (point.value as NumericHealthValue).numericValue; + String timestamp = DateTime.now().toIso8601String(); + + try { + if (point.dateFrom != null) { + String tempTimestamp = point.dateFrom.toIso8601String(); + if (tempTimestamp.isNotEmpty) { + timestamp = tempTimestamp; + } + } + } catch (e) { + debugPrint('Error converting dateFrom to ISO8601 for $s: $e'); + } + Vitals vitals = Vitals( - value: (point.value as NumericHealthValue).numericValue.toStringAsFixed(2), - timestamp: point.dateFrom.toString() + value: numericValue.toStringAsFixed(2), + timestamp: timestamp ); mappedData[s]?.add(vitals); } @@ -165,17 +182,41 @@ class HealthConnectHelper extends WatchHelper { for (var point in data) { if (point.value is NumericHealthValue) { final numericValue = (point.value as NumericHealthValue).numericValue; + String timestamp = DateTime.now().toIso8601String(); + + try { + if (point.dateFrom != null) { + String tempTimestamp = point.dateFrom.toIso8601String(); + if (tempTimestamp.isNotEmpty) { + timestamp = tempTimestamp; + } + } + } catch (e) { + debugPrint('Error converting dateFrom to ISO8601 for $s: $e'); + } + point.value = NumericHealthValue( numericValue: numericValue * 100, ); - Vitals vitals = Vitals(value: (point.value as NumericHealthValue).numericValue.toStringAsFixed(2), timestamp: point.dateFrom.toString()); + Vitals vitals = Vitals( + value: (point.value as NumericHealthValue).numericValue.toStringAsFixed(2), + timestamp: timestamp + ); mappedData[s]?.add(vitals); } } } getMappedData() { - return " { \"heartRate\": ${mappedData["heartRate"] ?? []}, \"sleep\": ${mappedData["sleep"] ?? []}, \"steps\": ${mappedData["steps"] ?? []}, \"activity\": ${mappedData["activity"] ?? []}, \"bloodOxygen\": ${mappedData["bloodOxygen"] ?? []}, \"bodyTemperature\": ${mappedData["bodyTemperature"] ?? []}, \"distance\": ${mappedData["distance"] ?? []} }"; + return jsonEncode({ + "heartRate": (mappedData["heartRate"] ?? []).map((v) => v.toJson()).toList(), + "sleep": (mappedData["sleep"] ?? []).map((v) => v.toJson()).toList(), + "steps": (mappedData["steps"] ?? []).map((v) => v.toJson()).toList(), + "activity": (mappedData["activity"] ?? []).map((v) => v.toJson()).toList(), + "bloodOxygen": (mappedData["bloodOxygen"] ?? []).map((v) => v.toJson()).toList(), + "bodyTemperature": (mappedData["bodyTemperature"] ?? []).map((v) => v.toJson()).toList(), + "distance": (mappedData["distance"] ?? []).map((v) => v.toJson()).toList(), + }); } getHeartData(DateTime startDate, DateTime endDate, HealthDataType types) async { diff --git a/lib/presentation/smartwatches/activity_detail.dart b/lib/presentation/smartwatches/activity_detail.dart index b023e04f..7911baf6 100644 --- a/lib/presentation/smartwatches/activity_detail.dart +++ b/lib/presentation/smartwatches/activity_detail.dart @@ -240,7 +240,8 @@ class _ActivityDetailsState extends State { return Selector>?>( selector: (_, model) => model.selectedData, builder: (_, data, __) { - if (context.read().selectedData.values.toList().first?.isEmpty == true) return SizedBox(); + // if (context.read().selectedData.values.toList().first.isEmpty == true) return SizedBox(); + if (context.read().selectedData.values.toList().isEmpty == true) return SizedBox(); return CustomBarChart( dataPoints: context.read().selectedData.values.toList().first, From 753968d3fa060b0302f0cc56688d50cf6bf1f9c0 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Wed, 1 Jul 2026 14:16:29 +0300 Subject: [PATCH 04/12] updates for dental & laser clinics --- .../search_doctor_by_name.dart | 1 + .../book_appointment/widgets/doctor_card.dart | 182 ++++++++++-------- 2 files changed, 107 insertions(+), 76 deletions(-) diff --git a/lib/presentation/book_appointment/search_doctor_by_name.dart b/lib/presentation/book_appointment/search_doctor_by_name.dart index 438cd746..165acfbf 100644 --- a/lib/presentation/book_appointment/search_doctor_by_name.dart +++ b/lib/presentation/book_appointment/search_doctor_by_name.dart @@ -212,6 +212,7 @@ class _SearchDoctorByNameState extends State { doctorsListResponseModel: bookAppointmentsVM.filteredDoctorList[index], isLoading: false, bookAppointmentsViewModel: bookAppointmentsViewModel, + isDoctorNameSearch: true, ).paddingSymmetrical(16.h, 0.h).onPress(() async { bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.filteredDoctorList[index]); LoaderBottomSheet.showLoader(); diff --git a/lib/presentation/book_appointment/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart index 7a39bc3e..fe7cf3e0 100644 --- a/lib/presentation/book_appointment/widgets/doctor_card.dart +++ b/lib/presentation/book_appointment/widgets/doctor_card.dart @@ -10,13 +10,17 @@ 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/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/dental_chief_complaints_page.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/laser/laser_appointment.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:lottie/lottie.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -25,11 +29,13 @@ class DoctorCard extends StatelessWidget { super.key, required this.doctorsListResponseModel, required this.isLoading, + this.isDoctorNameSearch = false, required this.bookAppointmentsViewModel, }); final DoctorsListResponseModel doctorsListResponseModel; final bool isLoading; + final bool isDoctorNameSearch; final BookAppointmentsViewModel bookAppointmentsViewModel; @@ -173,78 +179,47 @@ class DoctorCard extends StatelessWidget { CustomButton( text: LocaleKeys.bookAppo.tr(context: context), onPressed: () async { - bookAppointmentsViewModel.setSelectedDoctor(doctorsListResponseModel); - LoaderBottomSheet.showLoader(); - bookAppointmentsViewModel.isLiveCareSchedule - ? await bookAppointmentsViewModel.getLiveCareDoctorFreeSlots( - isBookingForLiveCare: true, - onSuccess: (dynamic respData) async { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - title: LocaleKeys.pickADate.tr(context: context), - context, - child: AppointmentCalendar(), - isFullScreen: false, - isCloseButtonVisible: true, - callBackFunc: () {}, - ); - }, - onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }) - : await bookAppointmentsViewModel.getDoctorFreeSlots( - isBookingForLiveCare: false, - onSuccess: (dynamic respData) async { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - title: LocaleKeys.pickADate.tr(context: context), - context, - child: AppointmentCalendar(), - isFullScreen: false, - isCloseButtonVisible: true, - callBackFunc: () {}, - ); - }, - onError: (err) { - LoaderBottomSheet.hideLoader(); - if (bookAppointmentsViewModel.selectedClinic.clinicID == 23) { + if (isDoctorNameSearch && doctorsListResponseModel.clinicID == 17) { + GetClinicsListResponseModel selectedClinic = GetClinicsListResponseModel( + clinicID: doctorsListResponseModel.clinicID, clinicDescription: doctorsListResponseModel.clinicName, isLiveCareClinicAndOnline: false, liveCareServiceID: 0, liveCareClinicID: 0); + bookAppointmentsViewModel.setProjectID(doctorsListResponseModel.projectID.toString()); + bookAppointmentsViewModel.setSelectedClinic(selectedClinic); + bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true); + Navigator.of(context).push( + CustomPageRoute( + page: DentalChiefComplaintsPage(), + ), + ); + } else if (isDoctorNameSearch && doctorsListResponseModel.clinicID == 253) { + GetClinicsListResponseModel selectedClinic = GetClinicsListResponseModel( + clinicID: doctorsListResponseModel.clinicID, clinicDescription: doctorsListResponseModel.clinicName, isLiveCareClinicAndOnline: false, liveCareServiceID: 0, liveCareClinicID: 0); + bookAppointmentsViewModel.setSelectedClinic(selectedClinic); + bookAppointmentsViewModel.getLaserClinic(); + bookAppointmentsViewModel.setIsDoctorsListLoading(true); + Navigator.of(context).push( + CustomPageRoute( + page: LaserAppointment(), + ), + ); + } else { + bookAppointmentsViewModel.setSelectedDoctor(doctorsListResponseModel); + LoaderBottomSheet.showLoader(); + bookAppointmentsViewModel.isLiveCareSchedule + ? await bookAppointmentsViewModel.getLiveCareDoctorFreeSlots( + isBookingForLiveCare: true, + onSuccess: (dynamic respData) async { + LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight( + title: LocaleKeys.pickADate.tr(context: context), context, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Lottie.asset(AppAnimations.errorAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), - SizedBox(height: 8.h), - ("For booking appointments in the Physiotherapy clinic, please contact: 920066666").toText16(color: AppColors.blackColor), - SizedBox(height: 16.h), - CustomButton( - height: 40.h, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - text: LocaleKeys.callNow.tr(), - textColor: AppColors.whiteColor, - iconColor: AppColors.whiteColor, - onPressed: () { - launchUrl( - Uri.parse("tel://920066666"), - ); - }, - icon: AppAssets.call), - ], - ).center, - callBackFunc: () {}, + child: AppointmentCalendar(), isFullScreen: false, isCloseButtonVisible: true, + callBackFunc: () {}, ); - } else { + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight( context, child: Utils.getErrorWidget(loadingText: err), @@ -252,15 +227,70 @@ class DoctorCard extends StatelessWidget { isFullScreen: false, isCloseButtonVisible: true, ); - } - // showCommonBottomSheetWithoutHeight( - // context, - // child: Utils.getErrorWidget(loadingText: err), - // callBackFunc: () {}, - // isFullScreen: false, - // isCloseButtonVisible: true, - // ); - }); + }) + : await bookAppointmentsViewModel.getDoctorFreeSlots( + isBookingForLiveCare: false, + onSuccess: (dynamic respData) async { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.pickADate.tr(context: context), + context, + child: AppointmentCalendar(), + isFullScreen: false, + isCloseButtonVisible: true, + callBackFunc: () {}, + ); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + if (bookAppointmentsViewModel.selectedClinic.clinicID == 23) { + showCommonBottomSheetWithoutHeight( + context, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Lottie.asset(AppAnimations.errorAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), + SizedBox(height: 8.h), + ("For booking appointments in the Physiotherapy clinic, please contact: 920066666").toText16(color: AppColors.blackColor), + SizedBox(height: 16.h), + CustomButton( + height: 40.h, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + text: LocaleKeys.callNow.tr(), + textColor: AppColors.whiteColor, + iconColor: AppColors.whiteColor, + onPressed: () { + launchUrl( + Uri.parse("tel://920066666"), + ); + }, + icon: AppAssets.call), + ], + ).center, + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + // showCommonBottomSheetWithoutHeight( + // context, + // child: Utils.getErrorWidget(loadingText: err), + // callBackFunc: () {}, + // isFullScreen: false, + // isCloseButtonVisible: true, + // ); + }); + } }, backgroundColor: Color(0xffFEE9EA), borderColor: Color(0xffFEE9EA), From 780cf271f2cb76f78c5705e96cbff93a4db431ec Mon Sep 17 00:00:00 2001 From: tahaalam Date: Mon, 6 Jul 2026 15:39:19 +0300 Subject: [PATCH 05/12] LAB RESULT CHANGES ADDED --- assets/langs/ar-SA.json | 3 +- assets/langs/en-US.json | 3 +- lib/core/api/api_client.dart | 4 +- lib/features/lab/lab_repo.dart | 50 ++++ lib/features/lab/lab_view_model.dart | 34 ++- .../lab/models/resp_models/lab_result.dart | 4 + lib/generated/locale_keys.g.dart | 1 + .../lab_result_via_clinic/LabResultList.dart | 268 ++++++++++++++++-- .../lab_order_result_item.dart | 107 ++++++- 9 files changed, 441 insertions(+), 33 deletions(-) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 96908b62..ed554e97 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1966,5 +1966,6 @@ "doctorSchedule": "جدول الطبيب", "weeklyShift": "الفترات الأسبوعية", "dayOff": "إجازة", - "notAvailable": "غير متاح" + "notAvailable": "غير متاح", + "hide": "إخفاء" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 0a34c8bf..ae344b01 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1955,7 +1955,8 @@ "doctorSchedule": "Doctor Schedule", "weeklyShift": "Weekly Shift", "dayOff": "Off", - "notAvailable": "Not Available" + "notAvailable": "Not Available", + "hide": "Hide" } diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 6a44cb0e..20be1f41 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -210,8 +210,8 @@ class ApiClientImp implements ApiClient { body['TokenID'] = "@dm!n"; } - // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 4769748; + body['TokenID'] = "@dm!n"; + body['PatientID'] = 1018977; // body['PatientID'] = 89754444; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; diff --git a/lib/features/lab/lab_repo.dart b/lib/features/lab/lab_repo.dart index 82468ada..94679047 100644 --- a/lib/features/lab/lab_repo.dart +++ b/lib/features/lab/lab_repo.dart @@ -29,6 +29,7 @@ abstract class LabRepo { Future>> getPatientAiOverViewLabOrders({required int langId, required dynamic payload}); Future>> getPatientAiOverViewLabOrder(var payloa); + Map> groupLabResultsByPackageName(List labResults); } class LabRepoImp implements LabRepo { @@ -37,6 +38,55 @@ class LabRepoImp implements LabRepo { LabRepoImp({required this.loggerService, required this.apiClient}); + /// Groups a list of LabResult objects by their PackageName + /// Returns a Map where the key is the PackageName and the value is a List of LabResult objects + /// Both the keys and the lists are sorted alphabetically + /// + /// Example usage: + /// ```dart + /// List labResults = [/* your lab results */]; + /// Map> groupedResults = labRepo.groupLabResultsByPackageName(labResults); + /// + /// // Access results by package name + /// groupedResults.forEach((packageName, results) { + /// print('Package: $packageName has ${results.length} results'); + /// }); + /// ``` + Map> groupLabResultsByPackageName(List labResults) { + Map> groupedResults = {}; + + // Group results by package name + for (var labResult in labResults) { + String packageName = labResult.PackageName ?? 'Unknown Package'; + + if (groupedResults.containsKey(packageName)) { + // Package name exists, add to existing list + groupedResults[packageName]!.add(labResult); + } else { + // Package name doesn't exist, create new key with new list + groupedResults[packageName] = [labResult]; + } + } + + // Sort each list alphabetically by description + groupedResults.forEach((key, list) { + list.sort((a, b) { + String descA = (a.description ?? '').toLowerCase(); + String descB = (b.description ?? '').toLowerCase(); + return descA.compareTo(descB); + }); + }); + + // Sort the map keys alphabetically and return a new sorted map + var sortedKeys = groupedResults.keys.toList()..sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase())); + Map> sortedGroupedResults = {}; + for (var key in sortedKeys) { + sortedGroupedResults[key] = groupedResults[key]!; + } + + return sortedGroupedResults; + } + @override Future>>> getPatientLabOrders() async { Map mapDevice = {}; diff --git a/lib/features/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index 09caf578..1c25ef9d 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -57,7 +57,11 @@ class LabViewModel extends ChangeNotifier { PatientLabOrdersResponseModel? currentlySelectedPatientOrder; - List mainLabResultsByHospitals = []; + // Flat list for UI compatibility + List mainLabResultsByHospitalsList = []; + + // Grouped map by package name for efficient lookups + Map> mainLabResultsByPackageName = {}; List mainLabResults = []; List mainGraphPoints = []; @@ -383,7 +387,8 @@ class LabViewModel extends ChangeNotifier { Future getPatientLabResultByHospital(PatientLabOrdersResponseModel laborder) async { isLabResultByHospitalLoading = true; notifyListeners(); - mainLabResultsByHospitals.clear(); + mainLabResultsByHospitalsList.clear(); + mainLabResultsByPackageName.clear(); final result = await labRepo.getPatientLabResultsByHospitals(laborder, Utils.isVidaPlusProject(int.parse(laborder.projectID ?? "0"))); @@ -397,7 +402,9 @@ class LabViewModel extends ChangeNotifier { isLabResultByHospitalLoading = false; if (apiResponse.messageStatus == 2) { } else if (apiResponse.messageStatus == 1) { - mainLabResultsByHospitals = apiResponse.data ?? []; + mainLabResultsByHospitalsList = apiResponse.data ?? []; + // Group the results by package name for efficient lookups + mainLabResultsByPackageName = labRepo.groupLabResultsByPackageName(mainLabResultsByHospitalsList); notifyListeners(); } }, @@ -842,6 +849,25 @@ class LabViewModel extends ChangeNotifier { notifyListeners(); } + // /// Finds a matching LabResult from the grouped map by package name + // /// Searches through all packages to find a result matching the test code or description + // LabResult? _findMatchingLabResult(TestDetails testDetail) { + // // Search through all packages in the grouped map + // for (var packageResults in mainLabResultsByPackageName.values) { + // try { + // var matchingResult = packageResults.firstWhere( + // (labResult) => labResult.testCode == testDetail.testCode || labResult.description == testDetail.description, + // ); + // return matchingResult; + // } catch (e) { + // // Continue searching in other packages + // continue; + // } + // } + // // Return null if no match found + // return LabResult(); + // } + Future getAiOverviewLabOrders({required int langId, required PatientLabOrdersResponseModel labOrder, required String loadingText}) async { isLabAIAnalysisNeedsToBeShown = true; notifyListeners(); @@ -858,7 +884,7 @@ class LabViewModel extends ChangeNotifier { if (labOrder.testDetails != null && labOrder.testDetails!.isNotEmpty) { for (var testDetail in labOrder.testDetails!) { LabResult? matchingResult = - mainLabResultsByHospitals.firstWhere((labResult) => labResult.testCode == testDetail.testCode || labResult.description == testDetail.description, orElse: () => LabResult()); + mainLabResultsByHospitalsList.firstWhere((labResult) => labResult.testCode == testDetail.testCode || labResult.description == testDetail.description, orElse: () => LabResult()); testResults.add({ "test_name": testDetail.description ?? "", "result_value": matchingResult.resultValue ?? "", diff --git a/lib/features/lab/models/resp_models/lab_result.dart b/lib/features/lab/models/resp_models/lab_result.dart index 2808dce9..78be004a 100644 --- a/lib/features/lab/models/resp_models/lab_result.dart +++ b/lib/features/lab/models/resp_models/lab_result.dart @@ -28,6 +28,7 @@ class LabResult { String? packageShortDescription; int? testId; String? testShortDescription; + String? PackageName; LabResult( {this.description, @@ -56,6 +57,7 @@ class LabResult { this.referenceLow, this.testId, this.testShortDescription, + this.PackageName }); LabResult.fromJson(Map json) { @@ -88,6 +90,7 @@ class LabResult { resultTypeID = json['ResultTypeID']; testId = json['TestID']; testShortDescription = json['TestShortDescription']; + PackageName = json['PackageName']; } Map toJson() { @@ -118,6 +121,7 @@ class LabResult { data['ReferenceLow'] = this.referenceLow; data['TestID'] = this.testId; data['TestShortDescription'] = this.testShortDescription; + data['PackageName'] = this.PackageName; return data; } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 9d48db25..80cea8b4 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1947,5 +1947,6 @@ abstract class LocaleKeys { static const doctorSchedule = 'doctorSchedule'; static const weeklyShift = 'weeklyShift'; static const dayOff = 'dayOff'; + static const hide = 'hide'; } diff --git a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart index 8aedd8a5..1d47ea2c 100644 --- a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart +++ b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart @@ -1,45 +1,271 @@ +import 'dart:ui' as ui; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.dart'; -import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart'; -import 'package:provider/provider.dart' show Selector, Provider, ReadContext; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; +import 'package:provider/provider.dart'; +import '../../../core/dependencies.dart' show getIt; +import '../../../core/utils/size_utils.dart' show ResponsiveExtension; +import '../../../generated/locale_keys.g.dart' show LocaleKeys; -import '../../../widgets/date_range_selector/viewmodel/date_range_view_model.dart'; +class LabResultList extends StatefulWidget { + @override + State createState() => _LabResultListState(); +} -class LabResultList extends StatelessWidget { +class _LabResultListState extends State { late LabViewModel model; + int expandedIndex = -1; + Map descriptionExpandedMap = {}; @override Widget build(BuildContext context) { model = Provider.of(context); - return Selector>( - selector: (_, model) => model.mainLabResultsByHospitals, - builder: (__, list, ___) { - if (list.isEmpty && context.read().labSpecialResult.isEmpty) { + return Selector>>( + selector: (_, model) => model.mainLabResultsByPackageName, + builder: (__, groupedResults, ___) { + if (groupedResults.isEmpty && context.read().labSpecialResult.isEmpty) { // return Utils.getNoDataWidget(context, // noDataText: LocaleKeys.noLabResults.tr(context: context)); return Container(); } else { + // Convert map entries to a list for indexed access + final packageEntries = groupedResults.entries.toList(); + return ListView.builder( physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, - shrinkWrap: true,itemCount: list.length,itemBuilder: (____, index) { - var labItem = list[index]; - return LabOrderResultItem(onTap: () { - context.read().flush(); - model.getPatientLabResult(model.currentlySelectedPatientOrder!, labItem.description ?? "", labItem.testShortDescription ?? "", labItem.uOM ?? ""); - }, - tests: labItem, - index: index, - iconColor: model.getColor(labItem.calculatedResultFlag ?? "N"), - severityText: model.getSeverityText(labItem.calculatedResultFlag ?? "N")); - }); + shrinkWrap: true, + itemCount: packageEntries.length, + itemBuilder: (context, packageIndex) { + final packageName = packageEntries[packageIndex].key; + final labResults = packageEntries[packageIndex].value; + final isExpanded = expandedIndex == packageIndex; + + return AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + margin: EdgeInsets.symmetric(vertical: 8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 10.r, hasShadow: false), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Package name as title/header + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.0.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: packageName.toText18( + isBold: true + ), + ), + AnimatedRotation( + turns: isExpanded ? 0.5 : 0, + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: Icon(Icons.expand_more), + ), + ], + ), + if(labResults.first.packageShortDescription != null) + _ExpandableDescriptionText( + description: labResults.first.packageShortDescription!, + isExpanded: descriptionExpandedMap[packageIndex] ?? false, + onToggle: () { + setState(() { + descriptionExpandedMap[packageIndex] = !(descriptionExpandedMap[packageIndex] ?? false); + }); + }, + ) + else + SizedBox.shrink() + ], + ),), + + // Lab results under this package with animation + AnimatedSize( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: isExpanded + ? Column( + children: labResults.asMap().entries.map((entry) { + final index = entry.key; + final labItem = entry.value; + + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: Duration(milliseconds: 300 + (index * 50)), + curve: Curves.easeOut, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.translate( + offset: Offset(0, 20 * (1 - value)), + child: child, + ), + ); + }, + child: Column( + children: [ + LabOrderResultItem( + onTap: () { + context.read().flush(); + model.getPatientLabResult( + model.currentlySelectedPatientOrder!, + labItem.description ?? "", + labItem.testShortDescription ?? "", + labItem.uOM ?? "" + ); + }, + tests: labItem, + index: index, + iconColor: model.getColor(labItem.calculatedResultFlag ?? "N"), + severityText: model.getSeverityText(labItem.calculatedResultFlag ?? "N") + ), + if(labResults.length>1) + ...[SizedBox( + height: 8.h, + ), + Divider( + color: AppColors.dividerColor, + thickness: 1.h, + )] + ], + ), + ); + }).toList(), + ) + : SizedBox.shrink(), + ), + ], + ), + ).onPress((){ + setState(() { + expandedIndex = isExpanded ? -1 : packageIndex; + }); + }); + } + ); + } + }, + ); + } +} + +class _ExpandableDescriptionText extends StatelessWidget { + final String description; + final bool isExpanded; + final VoidCallback onToggle; + + const _ExpandableDescriptionText({ + required this.description, + required this.isExpanded, + required this.onToggle, + }); + + static TextStyle get textStyle => TextStyle( + fontSize: 14.f, + color: AppColors.textColor, // or Colors.black87 + fontFamily: 'Poppins', + fontWeight: FontWeight.w400, + fontStyle: FontStyle.normal, + height: 1.4, + letterSpacing: 0.0, + decoration: TextDecoration.none, + ); + + @override + Widget build(BuildContext context) { + final readMoreText = isExpanded + ? ' ${LocaleKeys.hide.tr()}' + : ' ${LocaleKeys.readMore.tr(context: context)}'; + + final readMoreStyle = TextStyle( + fontSize: 12.f, + color: AppColors.primaryRedColor, + fontFamily: 'Poppins', + fontWeight: FontWeight.w500, + ); + + return LayoutBuilder( + builder: (context, constraints) { + // Check if description alone overflows 2 lines + final descriptionPainter = TextPainter( + text: TextSpan(text: description, style: textStyle), + maxLines: 2, + textDirection: Directionality.of(context), + )..layout(maxWidth: constraints.maxWidth); + + final isOverflowing = descriptionPainter.didExceedMaxLines; + + // No overflow, or already expanded -> show full text + if (!isOverflowing || isExpanded) { + return GestureDetector( + onTap: isOverflowing ? onToggle : null, + child: Text.rich( + TextSpan( + style: textStyle, + children: [ + TextSpan(text: description, style: textStyle), + if (isOverflowing) + TextSpan(text: readMoreText, style: readMoreStyle), + ], + ), + ), + ); + } + + // Binary search for max chars that fit in 2 lines with "Read more" appended + int low = 0, high = description.length; + while (low < high) { + final mid = (low + high + 1) ~/ 2; + final testPainter = TextPainter( + text: TextSpan( + style: textStyle, + children: [ + TextSpan(text: '${description.substring(0, mid)}...'), + TextSpan(text: readMoreText, style: readMoreStyle), + ], + ), + maxLines: 2, + textDirection: Directionality.of(context), + )..layout(maxWidth: constraints.maxWidth); + + if (testPainter.didExceedMaxLines) { + high = mid - 1; + } else { + low = mid; + } } + + final truncated = description.substring(0, low).trimRight(); + + return GestureDetector( + onTap: onToggle, + child: Text.rich( + TextSpan( + style: textStyle, + children: [ + TextSpan(text: '$truncated...'), + TextSpan(text: readMoreText, style: readMoreStyle), + ], + ), + ), + ); }, ); } -} \ No newline at end of file +} diff --git a/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart b/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart index ad2ad77f..e3f07e54 100644 --- a/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart +++ b/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart @@ -4,6 +4,7 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; @@ -12,6 +13,7 @@ import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.d import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:provider/provider.dart'; import 'dart:ui' as ui; @@ -24,7 +26,16 @@ class LabOrderResultItem extends StatelessWidget { final bool isExpanded; final Color iconColor; - const LabOrderResultItem({super.key, required this.onTap, this.tests, required this.index, this.isLoading = false, this.isExpanded = false, required this.iconColor, required this.severityText}); + const LabOrderResultItem({ + super.key, + required this.onTap, + this.tests, + required this.index, + this.isLoading = false, + this.isExpanded = false, + required this.iconColor, + required this.severityText + }); @override build(BuildContext context) { @@ -32,7 +43,7 @@ class LabOrderResultItem extends StatelessWidget { duration: Duration(milliseconds: 300), curve: Curves.easeInOut, margin: EdgeInsets.symmetric(vertical: 8.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), + // decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false), child: Container( key: ValueKey(index), padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 16.h), @@ -84,7 +95,20 @@ class LabOrderResultItem extends StatelessWidget { ), softWrap: true, ), - ) + ), + Visibility( + visible: tests?.notes != null && tests?.notes?.isNotEmpty == true, + child: Text( + tests?.notes??"", + style: TextStyle( + fontSize: 12.f, + fontWeight: FontWeight.w600, + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', + color: AppColors.greyTextColor, + ), + softWrap: true, + ), + ), ], ), ), @@ -104,7 +128,9 @@ class LabOrderResultItem extends StatelessWidget { severityText.tr().toText10(isBold: true, color: AppColors.greyTextColor), Utils.buildSvgWithAssets(icon: AppAssets.lab_result_indicator, width: 21, height: 23, iconColor: iconColor), ], - ), + ).onPress((){ + _buildSeverityDetailsBottomsheet(severity: tests?.calculatedResultFlag ?? "N", context: context); + }), ), ), Flexible( @@ -136,4 +162,77 @@ class LabOrderResultItem extends StatelessWidget { ), )); } + + _buildSeverityDetailsBottomsheet({required String severity,required BuildContext context}) { + return showCommonBottomSheetWithoutHeight( + title: LocaleKeys.colorScience.tr(context: context), + context, + child: Container( + padding: EdgeInsets.all(16.w), + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + _buildInfoTile(severityValue: severity, context: context), + ], + ), + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + Widget _buildInfoTile({required String severityValue, required BuildContext context}) { + + String title = LocaleKeys.emergency.tr(context: context); + String description = LocaleKeys.emergencyDescription.tr(context: context); + Color dotColor = AppColors.chipColorEmergency; + switch (severityValue) { + case 'CL': + case 'LCL': + title = LocaleKeys.emergency.tr(context: context); + description = LocaleKeys.emergencyDescription.tr(context: context); + dotColor = AppColors.chipColorEmergency; + case 'L': + title = LocaleKeys.seekMedicalAdvice.tr(context: context); + description = LocaleKeys.seekMedicalAdviceDescription.tr(context: context); + dotColor = AppColors.chipColorSeekMedicalAdvice; + case 'N': + title = LocaleKeys.monitor.tr(context: context); + description = LocaleKeys.monitorDescription.tr(context: context); + dotColor = AppColors.chipColorMonitor; + case 'H': + title = LocaleKeys.seekMedicalAdvice.tr(context: context); + description = LocaleKeys.seekMedicalAdviceDescription.tr(context: context); + dotColor = AppColors.chipColorSeekMedicalAdvice; + case 'CH': + case 'HCH': + title = LocaleKeys.emergency.tr(context: context); + description = LocaleKeys.emergencyDescription.tr(context: context); + dotColor = AppColors.chipColorEmergency; + } + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 14.h, + width: 14.w, + decoration: BoxDecoration(shape: BoxShape.circle, color: dotColor), + ).paddingOnly(top: 3.h), + SizedBox(width: 8.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.toText14(isBold: true, color: AppColors.textColor), + SizedBox(height: 4.h), + description.toText12(isBold: true, color: AppColors.greyTextColor), + ], + ), + ), + ], + ); + } } From 7025d331936ffb7ad2da28fa2788d0908b176dd0 Mon Sep 17 00:00:00 2001 From: tahaalam Date: Wed, 8 Jul 2026 15:00:01 +0300 Subject: [PATCH 06/12] LAB RESULT CHANGES ADDED --- .../lab/lab_result_via_clinic/LabResultList.dart | 13 ++++++++----- .../lab_order_result_item.dart | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart index 1d47ea2c..608f70a5 100644 --- a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart +++ b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart @@ -53,13 +53,13 @@ class _LabResultListState extends State { duration: Duration(milliseconds: 300), curve: Curves.easeInOut, margin: EdgeInsets.symmetric(vertical: 8.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 10.r, hasShadow: false), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Package name as title/header Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.0.h), + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.0.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, spacing: 8.h, @@ -139,9 +139,12 @@ class _LabResultListState extends State { ...[SizedBox( height: 8.h, ), - Divider( - color: AppColors.dividerColor, - thickness: 1.h, + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.h), + child: Divider( + color: AppColors.dividerColor, + thickness: 1.h, + ), )] ], ), diff --git a/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart b/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart index e3f07e54..2f6ee667 100644 --- a/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart +++ b/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart @@ -46,7 +46,7 @@ class LabOrderResultItem extends StatelessWidget { // decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false), child: Container( key: ValueKey(index), - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 16.h), + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 4.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ From be78a662c795a67f73b1e7016ef6e2ff74f3fd13 Mon Sep 17 00:00:00 2001 From: tahaalam Date: Wed, 8 Jul 2026 16:02:06 +0300 Subject: [PATCH 07/12] hardcoded admin removed --- lib/core/api/api_client.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 20be1f41..06d9cbf8 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -210,8 +210,8 @@ class ApiClientImp implements ApiClient { body['TokenID'] = "@dm!n"; } - body['TokenID'] = "@dm!n"; - body['PatientID'] = 1018977; + // body['TokenID'] = "@dm!n"; + // body['PatientID'] = 1018977; // body['PatientID'] = 89754444; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; From 9d8744ec17a4bcbc01f421e42e69a0cbfca3a97a Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Wed, 8 Jul 2026 16:27:34 +0300 Subject: [PATCH 08/12] Updates --- lib/core/api/api_client.dart | 2 +- lib/core/api_consts.dart | 4 ++-- lib/core/utils/date_util.dart | 3 ++- .../medical_file/medical_file_view_model.dart | 19 ++++++++++--------- .../my_appointments_view_model.dart | 5 ++++- .../prescriptions_view_model.dart | 10 +++++----- lib/main.dart | 6 +----- .../medical_file/medical_file_page.dart | 2 +- .../widgets/medical_report_card.dart | 2 +- .../medical_report/medical_reports_page.dart | 2 +- 10 files changed, 28 insertions(+), 27 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 6a44cb0e..5537b8b4 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -211,8 +211,8 @@ class ApiClientImp implements ApiClient { } // body['TokenID'] = "@dm!n"; + // body['PatientID'] = 53320; // body['PatientID'] = 4769748; - // body['PatientID'] = 89754444; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; // body['SessionID'] = "45786230487560q"; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 089f371b..c4feec0b 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -5,7 +5,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -301,7 +301,7 @@ class ApiConsts { static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations"; // ************ static values for Api **************** - static final double appVersionID = 21.4; + static final double appVersionID = 21.5; // static final double appVersionID = 50.7; static final int appChannelId = 3; diff --git a/lib/core/utils/date_util.dart b/lib/core/utils/date_util.dart index 0994171f..97c8dad0 100644 --- a/lib/core/utils/date_util.dart +++ b/lib/core/utils/date_util.dart @@ -18,7 +18,8 @@ class DateUtil { final endIndex = date.indexOf(end, startIndex + start.length); return DateTime.fromMillisecondsSinceEpoch(int.parse( date.substring(startIndex + start.length, endIndex), - ), isUtc: true).add(Duration(hours: 3)); + // ), isUtc: true).add(Duration(hours: 3)); + ), isUtc: true).toLocal(); } static DateTime convertStringToDateSaudiTimezone(String date, int projectId) { diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 41a726d9..e0abd9d7 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -42,7 +42,7 @@ class MedicalFileViewModel extends ChangeNotifier { bool isSickLeavesSortByClinic = true; - // bool isSickLeavesDataNeedsReloading = true; + bool isSickLeavesDataNeedsReloading = true; List patientAllergiesList = []; @@ -165,15 +165,15 @@ class MedicalFileViewModel extends ChangeNotifier { } setIsPatientSickLeaveListLoading(bool val) { - // if (val && isSickLeavesDataNeedsReloading) { - patientSickLeaveList.clear(); - patientSickLeavesByClinic.clear(); + if (val && isSickLeavesDataNeedsReloading) { + patientSickLeaveList.clear(); + patientSickLeavesByClinic.clear(); patientSickLeavesByHospital.clear(); patientSickLeavesViewList.clear(); patientSickLeavePDFBase64 = ""; isSickLeavesSortByClinic = true; isPatientSickLeaveListLoading = val; - // } + } notifyListeners(); } @@ -282,9 +282,9 @@ class MedicalFileViewModel extends ChangeNotifier { } Future getPatientSickLeaveList({Function(dynamic)? onSuccess, Function(String)? onError}) async { - // if (!isSickLeavesDataNeedsReloading) { - // return; - // } + if (!isSickLeavesDataNeedsReloading) { + return; + } patientSickLeaveList.clear(); notifyListeners(); @@ -300,6 +300,7 @@ class MedicalFileViewModel extends ChangeNotifier { // ), (failure) async { isPatientSickLeaveListLoading = false; + isSickLeavesDataNeedsReloading = true; notifyListeners(); }, (apiResponse) { @@ -330,7 +331,7 @@ class MedicalFileViewModel extends ChangeNotifier { } } patientSickLeavesViewList = patientSickLeavesByClinic; - // isSickLeavesDataNeedsReloading = false; + isSickLeavesDataNeedsReloading = false; notifyListeners(); if (onSuccess != null) { diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index 9182f9e7..f28eb49b 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -54,6 +54,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { bool isFavouriteDoctorsDataFetched = false; bool isAppointmentDataToBeLoaded = true; + bool isMyDoctorsDataToBeLoaded = true; bool isEyeMeasurementsAppointmentsLoading = false; @@ -213,6 +214,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { setIsAppointmentDataToBeLoaded(bool val) { if (val) { isMyAppointmentsLoading = true; + isMyDoctorsDataToBeLoaded = true; } isAppointmentDataToBeLoaded = val; notifyListeners(); @@ -674,7 +676,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { } Future getPatientMyDoctors({Function(dynamic)? onSuccess, Function(String)? onError}) async { - // if (!isAppointmentDataToBeLoaded) return; + if (!isMyDoctorsDataToBeLoaded) return; isPatientMyDoctorsLoading = true; patientMyDoctorsList.clear(); notifyListeners(); @@ -692,6 +694,7 @@ class MyAppointmentsViewModel extends ChangeNotifier { } else if (apiResponse.messageStatus == 1) { patientMyDoctorsList = apiResponse.data!; isPatientMyDoctorsLoading = false; + isMyDoctorsDataToBeLoaded = false; notifyListeners(); if (onSuccess != null) { onSuccess(apiResponse); diff --git a/lib/features/prescriptions/prescriptions_view_model.dart b/lib/features/prescriptions/prescriptions_view_model.dart index bfb83a2c..5354daad 100644 --- a/lib/features/prescriptions/prescriptions_view_model.dart +++ b/lib/features/prescriptions/prescriptions_view_model.dart @@ -64,7 +64,7 @@ class PrescriptionsViewModel extends ChangeNotifier { PrescriptionsViewModel({required this.prescriptionsRepo, required this.errorHandlerService, required this.navServices}); initPrescriptionsViewModel() { - // if (isPrescriptionsDataNeedsReloading) { + if (isPrescriptionsDataNeedsReloading) { patientPrescriptionOrders.clear(); patientPrescriptionOrdersByClinic.clear(); patientPrescriptionOrdersByHospital.clear(); @@ -72,7 +72,7 @@ class PrescriptionsViewModel extends ChangeNotifier { prescriptionsOrderList.clear(); isPrescriptionsOrdersLoading = true; getPatientPrescriptionOrders(); - // } + } isSortByClinic = true; isPrescriptionsDeliveryOrdersLoading = true; notifyListeners(); @@ -138,9 +138,9 @@ class PrescriptionsViewModel extends ChangeNotifier { } Future getPatientPrescriptionOrders({Function(dynamic)? onSuccess, Function(String)? onError}) async { - // if (!isPrescriptionsDataNeedsReloading) { - // return; - // } + if (!isPrescriptionsDataNeedsReloading) { + return; + } final result = await prescriptionsRepo.getPatientPrescriptionOrders(patientId: "1231755"); diff --git a/lib/main.dart b/lib/main.dart index 6e3ef8cc..aa266871 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -63,6 +63,7 @@ import 'core/utils/size_utils.dart'; import 'features/monthly_reports/terms_conditions_view_model.dart'; import 'firebase_options.dart'; + @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); @@ -109,11 +110,6 @@ Future callInitializations() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); - // Download the latest JSONs from your server - // await TranslationService.downloadTranslations(); - - // await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - try { // Attempt to get the default app. If it exists, this avoids the error. await Firebase.app(); diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 66135dda..a6a335fc 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -1099,7 +1099,7 @@ class _MedicalFilePageState extends State { itemBuilder: (context, index) { return AnimationConfiguration.staggeredList( position: index, - duration: const Duration(milliseconds: 1000), + duration: const Duration(milliseconds: 250), child: SlideAnimation( horizontalOffset: 100.0, child: FadeInAnimation( diff --git a/lib/presentation/medical_file/widgets/medical_report_card.dart b/lib/presentation/medical_file/widgets/medical_report_card.dart index 1dc080c7..1a43b83b 100644 --- a/lib/presentation/medical_file/widgets/medical_report_card.dart +++ b/lib/presentation/medical_file/widgets/medical_report_card.dart @@ -92,7 +92,7 @@ class MedicalReportCard extends StatelessWidget { // fontWeight: FontWeight.w600, isBold: true, borderRadius: 12.r, - height: 48.h, + height: 40.h, ).toShimmer2(isShow: isLoading), ], ), diff --git a/lib/presentation/medical_report/medical_reports_page.dart b/lib/presentation/medical_report/medical_reports_page.dart index e03a53a0..9463debe 100644 --- a/lib/presentation/medical_report/medical_reports_page.dart +++ b/lib/presentation/medical_report/medical_reports_page.dart @@ -242,7 +242,7 @@ class _MedicalReportsPageState extends State { mainAxisSize: MainAxisSize.min, children: [ Utils.buildImgWithNetwork( - url: report.doctorImageURL!, + url: report.doctorImageURL ?? "", iconColor: AppColors.transparent, width: 24.h, height: 24.h, From 4972295606b1bfa83c3060580599606cac028d6e Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Wed, 8 Jul 2026 16:41:02 +0300 Subject: [PATCH 09/12] update --- lib/presentation/appointments/widgets/appointment_card.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 39251cd6..e98eda82 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -277,7 +277,7 @@ class _AppointmentCardState extends State { (widget.isLoading ? 'Dr' : "${widget.patientAppointmentHistoryResponseModel.doctorTitle}").toText16(isBold: true, maxlines: 1), Expanded( child: (widget.isLoading ? 'John Doe' : " ${widget.patientAppointmentHistoryResponseModel.doctorNameObj!.truncate(30)}").toText16( - isBold: true, maxlines: 2, textOverflow: TextOverflow.ellipsis, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")), + isBold: true, maxlines: 1, textOverflow: TextOverflow.ellipsis, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")), ), SizedBox(width: 12.w), (widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null && From adde5c203653c8ef91ddee87515a296a946be4ff Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Mon, 13 Jul 2026 11:04:12 +0300 Subject: [PATCH 10/12] updates --- lib/core/api/api_client.dart | 4 +--- lib/features/contact_us/contact_us_view_model.dart | 11 ++++++++++- .../my_appointments/my_appointments_repo.dart | 5 +++-- .../my_appointments/my_appointments_view_model.dart | 9 +++++++-- lib/presentation/appointments/my_doctors_page.dart | 2 +- .../appointments/widgets/appointment_doctor_card.dart | 2 +- .../book_appointment/book_appointment_page.dart | 5 +++-- lib/presentation/lab/lab_orders_page.dart | 2 +- .../lab/lab_result_via_clinic/LabResultList.dart | 11 ++++++----- lib/presentation/medical_file/medical_file_page.dart | 3 ++- 10 files changed, 35 insertions(+), 19 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 59b5dfff..4015a4dc 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -211,10 +211,8 @@ class ApiClientImp implements ApiClient { } // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 1018977; - // body['PatientID'] = 89754444; + // body['PatientID'] = 5974668; // body['PatientID'] = 53320; - // body['PatientID'] = 4769748; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; // body['SessionID'] = "45786230487560q"; diff --git a/lib/features/contact_us/contact_us_view_model.dart b/lib/features/contact_us/contact_us_view_model.dart index 648fab6b..323eb782 100644 --- a/lib/features/contact_us/contact_us_view_model.dart +++ b/lib/features/contact_us/contact_us_view_model.dart @@ -12,6 +12,7 @@ import 'package:hmg_patient_app_new/features/contact_us/models/resp_models/get_p import 'package:hmg_patient_app_new/features/contact_us/models/resp_models/get_status_coc_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:permission_handler/permission_handler.dart'; class ContactUsViewModel extends ChangeNotifier { @@ -308,7 +309,15 @@ class ContactUsViewModel extends ChangeNotifier { final result = await contactUsRepo.insertCOCItem(requestInsertCOCItem: requestInsertCOCItem, patientSelectedAppointment: patientFeedbackSelectedAppointment); result.fold( - (failure) async => await errorHandlerService.handleError(failure: failure), + (failure) async { + LoaderBottomSheet.hideLoader(); + await errorHandlerService.handleError(failure: failure); + }, + // (failure) async { + // if(onError != null) { + // onError(failure.toString()); + // } + // }, (apiResponse) { if (apiResponse.messageStatus == 2) { // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); diff --git a/lib/features/my_appointments/my_appointments_repo.dart b/lib/features/my_appointments/my_appointments_repo.dart index d7a025fc..87484e06 100644 --- a/lib/features/my_appointments/my_appointments_repo.dart +++ b/lib/features/my_appointments/my_appointments_repo.dart @@ -53,7 +53,7 @@ abstract class MyAppointmentsRepo { Future>>> getPatientAppointmentsForTimeLine(); - Future>>> getPatientDoctorsList(); + Future>>> getPatientDoctorsList({bool isTop8 = false}); Future>>> getFavouriteDoctorsList(); @@ -514,11 +514,12 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { } @override - Future>>> getPatientDoctorsList() async { + Future>>> getPatientDoctorsList({bool isTop8 = false}) async { Map mapDevice = { "Top": 0, "beforeDays": 0, "exludType": 4, + "IsTop8": isTop8, }; try { diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index f28eb49b..b480a354 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -211,6 +211,11 @@ class MyAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } + setIsMyDoctorsDataToBeLoaded(bool val) { + isMyDoctorsDataToBeLoaded = val; + notifyListeners(); + } + setIsAppointmentDataToBeLoaded(bool val) { if (val) { isMyAppointmentsLoading = true; @@ -675,13 +680,13 @@ class MyAppointmentsViewModel extends ChangeNotifier { ); } - Future getPatientMyDoctors({Function(dynamic)? onSuccess, Function(String)? onError}) async { + Future getPatientMyDoctors({bool isTop8 = false, Function(dynamic)? onSuccess, Function(String)? onError}) async { if (!isMyDoctorsDataToBeLoaded) return; isPatientMyDoctorsLoading = true; patientMyDoctorsList.clear(); notifyListeners(); - final result = await myAppointmentsRepo.getPatientDoctorsList(); + final result = await myAppointmentsRepo.getPatientDoctorsList(isTop8: isTop8); result.fold( // (failure) async => await errorHandlerService.handleError(failure: failure), diff --git a/lib/presentation/appointments/my_doctors_page.dart b/lib/presentation/appointments/my_doctors_page.dart index 9ccf471e..d05270e5 100644 --- a/lib/presentation/appointments/my_doctors_page.dart +++ b/lib/presentation/appointments/my_doctors_page.dart @@ -39,7 +39,7 @@ class _MyDoctorsPageState extends State { @override void initState() { scheduleMicrotask(() { - myAppointmentsViewModel.getPatientMyDoctors(); + myAppointmentsViewModel.getPatientMyDoctors(isTop8: false); }); super.initState(); } diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index f4d20944..f0dd2f5d 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -101,7 +101,7 @@ class AppointmentDoctorCard extends StatelessWidget { Row( children: [ Expanded( - child: patientAppointmentHistoryResponseModel.doctorNameObj!.toText16( + child: (patientAppointmentHistoryResponseModel.doctorNameObj ?? "").toText16( isBold: true, isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? ""), textOverflow: TextOverflow.ellipsis, diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index d542bb7f..55769fbe 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -66,14 +66,14 @@ class _BookAppointmentPageState extends State { // bookAppointmentsViewModel.getLocation(); immediateLiveCareViewModel.initImmediateLiveCare(); if (appState.isAuthenticated) { - getIt.get().getPatientMyDoctors(); + getIt.get().getPatientMyDoctors(isTop8: true); getIt.get().getPatientFavouriteDoctors(); } }); WidgetsBinding.instance.addPostFrameCallback((_) { if (bookAppointmentsViewModel.selectedTabIndex == 1) { if (appState.isAuthenticated) { - getIt.get().getPatientMyDoctors(); + getIt.get().getPatientMyDoctors(isTop8: true); getIt.get().getPatientFavouriteDoctors(); showUnKnownClinicBottomSheet(); } @@ -166,6 +166,7 @@ class _BookAppointmentPageState extends State { Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 14.h), ], ).paddingSymmetrical(24.h, 0.h).onPress(() { + myAppointmentsVM.setIsMyDoctorsDataToBeLoaded(true); Navigator.of(context).push( CustomPageRoute( page: MyDoctorsPage(), diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index 29aa9dc7..c3a2adfe 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -1 +1 @@ -import 'dart:async'; import 'dart:ui' as ui; 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'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import 'alphabeticScroll.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return CollapsingListView( title: LocaleKeys.labResults.tr(context: context), search: () async { if (labProvider.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: labProvider.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; labProvider.filterLabReports(value); } } }, child: Consumer( builder: (context, labViewModel, child) { return SingleChildScrollView( physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.all(24.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.byVisit.tr()), CustomTabBarModel(null, LocaleKeys.byTest.tr()), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), ), ], ), if (activeIndex == 0) Padding( padding: EdgeInsets.symmetric(vertical: 10.h), child: Row( children: [ // CustomButton( // text: LocaleKeys.byClinic.tr(context: context), // onPressed: () { // labViewModel.setIsSortByClinic(true); // }, // backgroundColor: labViewModel.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, // borderColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), // textColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, // fontSize: 12.f, // isBold: true, // borderRadius: 10, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), // SizedBox(width: 8.h), // CustomButton( // text: LocaleKeys.byHospital.tr(context: context), // onPressed: () { // labViewModel.setIsSortByClinic(false); // }, // backgroundColor: labViewModel.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, // borderColor: labViewModel.isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, // textColor: labViewModel.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, // fontSize: 12, // isBold: true, // borderRadius: 10, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), ], ), ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? Column( children: [ AppCustomChipWidget( labelText: selectedFilterText!, backgroundColor: AppColors.alertColor, textColor: AppColors.whiteColor, deleteIcon: AppAssets.close_bottom_sheet_icon, deleteIconColor: AppColors.whiteColor, deleteIconHasColor: true, isEnglishOnly: true, onDeleteTap: () { selectedFilterText = ""; labProvider.filterLabReports(""); }, ), SizedBox(height: 8.h), ], ) : SizedBox(), activeIndex == 0 ? // By Visit - show grouped view when available labViewModel.isLabOrdersLoading ? ListView.builder( shrinkWrap: true, physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: 5, itemBuilder: (context, index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ), ) : (labViewModel.patientLabOrders.isNotEmpty ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: labViewModel.filteredLabOrders.length, itemBuilder: (context, index) { final group = labViewModel.filteredLabOrders[index]; final isExpanded = expandedIndex == index; return AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( duration: 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; }); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.all(16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ AppCustomChipWidget( richText: Row( children: [ "${group.testDetails!.length} ".toText10( color: AppColors.successColor, isEnglishOnly: true, weight: FontWeight.w600), LocaleKeys.tests .tr(context: context) .toText10(color: AppColors.successColor, weight: FontWeight.w600), ], ), labelText: "${group.testDetails!.length} ${LocaleKeys.tests.tr(context: context)}", backgroundColor: AppColors.successColor.withOpacity(0.1), textColor: AppColors.successColor, ), AppCustomChipWidget( labelText: "${_appState.isArabic() ? group.isInOutPatientDescriptionN : group.isInOutPatientDescription}", backgroundColor: AppColors.warningColorYellow.withOpacity(0.1), textColor: AppColors.warningColorYellow, ) ], ), Icon(isExpanded ? Icons.expand_less : Icons.expand_more), ], ), SizedBox(height: 8.h), Row( mainAxisSize: MainAxisSize.min, children: [ Image.network( group.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: (group.doctorName ?? group.doctorNameEnglish ?? "") .toString() .toText14(isBold: true)), ], ), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ Directionality( textDirection: ui.TextDirection.ltr, child: AppCustomChipWidget( labelText: DateUtil.formatDateToDate( DateUtil.convertStringToDate(group.orderDate ?? ""), false), isEnglishOnly: true, )), AppCustomChipWidget( labelText: (group.projectName ?? ""), ), AppCustomChipWidget( labelText: (group.clinicDescription ?? ""), ), ], ), ], ), ), AnimatedSwitcher( duration: 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, vertical: 0.h), child: Column( children: [ ListView.separated( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemBuilder: (cxt, index) { PatientLabOrdersResponseModel order = group; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ "• ${order.testDetails![index].description!}" .toText14(isBold: true, isEnglishOnly: true), SizedBox(height: 4.h), order.testDetails![index].testDescriptionEn!.toText12( isBold: true, color: AppColors.textColorLight, isEnglishOnly: true), ], ).paddingOnly(top: 16, bottom: 16); }, separatorBuilder: (cxt, index) => Divider( color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), itemCount: group.testDetails!.length > 3 ? 3 : group.testDetails!.length), SizedBox(height: 16.h), CustomButton( text: "${LocaleKeys.viewResults.tr()} (${group.testDetails!.length})", onPressed: () { labProvider.currentlySelectedPatientOrder = group; labProvider.getPatientLabResultByHospital(group); labProvider.getPatientSpecialResult(group); Navigator.of(context).push( CustomPageRoute( page: LabResultByClinic(labOrder: group), ), ); }, backgroundColor: AppColors.infoColor.withAlpha(20), borderColor: AppColors.infoColor.withAlpha(0), textColor: AppColors.infoColor, fontSize: (isFoldable || isTablet) ? 12.f : 14.f, isBold: true, borderRadius: 12.r, padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0), height: 40.h, iconSize: 14.h, icon: AppAssets.view_report_icon, iconColor: AppColors.infoColor, ), SizedBox(height: 16.h), ], ), ) : SizedBox.shrink(), ), ], ), ), ), ), )); }, ) : Utils.getNoDataWidget(context, noDataText: LocaleKeys.noLabResults.tr(context: context))) : // By Test or other tabs keep existing behavior (labViewModel.isLabOrdersLoading) ? Column( children: List.generate( 5, (index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, )), ) : AlphabeticScroll( alpahbetsAvailable: labViewModel.filteredIndexedCharacterForUniqueTest, details: labViewModel.filteredUniqueTestsList, labViewModel: labViewModel, rangeViewModel: rangeViewModel, appState: _appState, ) ], )); }, ), ); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file +import 'dart:async'; import 'dart:ui' as ui; 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'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import 'alphabeticScroll.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return CollapsingListView( title: LocaleKeys.labResults.tr(context: context), search: () async { if (labProvider.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: labProvider.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; labProvider.filterLabReports(value); } } }, child: Consumer( builder: (context, labViewModel, child) { return SingleChildScrollView( physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.all(24.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.byVisit.tr()), CustomTabBarModel(null, LocaleKeys.byTest.tr()), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), ), ], ), if (activeIndex == 0) Padding( padding: EdgeInsets.symmetric(vertical: 10.h), child: Row( children: [ // CustomButton( // text: LocaleKeys.byClinic.tr(context: context), // onPressed: () { // labViewModel.setIsSortByClinic(true); // }, // backgroundColor: labViewModel.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, // borderColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), // textColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, // fontSize: 12.f, // isBold: true, // borderRadius: 10, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), // SizedBox(width: 8.h), // CustomButton( // text: LocaleKeys.byHospital.tr(context: context), // onPressed: () { // labViewModel.setIsSortByClinic(false); // }, // backgroundColor: labViewModel.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, // borderColor: labViewModel.isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, // textColor: labViewModel.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, // fontSize: 12, // isBold: true, // borderRadius: 10, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), ], ), ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? Column( children: [ AppCustomChipWidget( labelText: selectedFilterText!, backgroundColor: AppColors.alertColor, textColor: AppColors.whiteColor, deleteIcon: AppAssets.close_bottom_sheet_icon, deleteIconColor: AppColors.whiteColor, deleteIconHasColor: true, isEnglishOnly: true, onDeleteTap: () { selectedFilterText = ""; labProvider.filterLabReports(""); }, ), SizedBox(height: 8.h), ], ) : SizedBox(), activeIndex == 0 ? // By Visit - show grouped view when available labViewModel.isLabOrdersLoading ? ListView.builder( shrinkWrap: true, physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: 5, itemBuilder: (context, index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ), ) : (labViewModel.patientLabOrders.isNotEmpty ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: labViewModel.filteredLabOrders.length, itemBuilder: (context, index) { final group = labViewModel.filteredLabOrders[index]; final isExpanded = expandedIndex == index; return AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( duration: 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; }); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.all(16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ AppCustomChipWidget( richText: Row( children: [ "${group.testDetails!.length} ".toText10( color: AppColors.successColor, isEnglishOnly: true, weight: FontWeight.w600), LocaleKeys.tests .tr(context: context) .toText10(color: AppColors.successColor, weight: FontWeight.w600), ], ), labelText: "${group.testDetails!.length} ${LocaleKeys.tests.tr(context: context)}", backgroundColor: AppColors.successColor.withOpacity(0.1), textColor: AppColors.successColor, ), AppCustomChipWidget( labelText: "${_appState.isArabic() ? group.isInOutPatientDescriptionN : group.isInOutPatientDescription}", backgroundColor: AppColors.warningColorYellow.withOpacity(0.1), textColor: AppColors.warningColorYellow, ) ], ), Icon(isExpanded ? Icons.expand_less : Icons.expand_more), ], ), SizedBox(height: 8.h), Row( mainAxisSize: MainAxisSize.min, children: [ Image.network( group.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: (group.doctorName ?? group.doctorNameEnglish ?? "") .toString() .toText14(isBold: true)), ], ), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ Directionality( textDirection: ui.TextDirection.ltr, child: AppCustomChipWidget( labelText: DateUtil.formatDateToDate( DateUtil.convertStringToDate(group.orderDate ?? ""), false), isEnglishOnly: true, )), AppCustomChipWidget( labelText: (group.projectName ?? ""), ), AppCustomChipWidget( labelText: (group.clinicDescription ?? ""), ), ], ), ], ), ), AnimatedSwitcher( duration: 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, vertical: 0.h), child: Column( children: [ ListView.separated( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemBuilder: (cxt, index) { PatientLabOrdersResponseModel order = group; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ "• ${order.testDetails![index].description!}" .toText14(isBold: true, isEnglishOnly: true), SizedBox(height: 4.h), (_appState.isArabic() ? order.testDetails![index].testDescriptionAr! : order.testDetails![index].testDescriptionEn!).toText12( isBold: true, color: AppColors.textColorLight, isEnglishOnly: true), ], ).paddingOnly(top: 16, bottom: 16); }, separatorBuilder: (cxt, index) => Divider( color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), itemCount: group.testDetails!.length > 3 ? 3 : group.testDetails!.length), SizedBox(height: 16.h), CustomButton( text: "${LocaleKeys.viewResults.tr()} (${group.testDetails!.length})", onPressed: () { labProvider.currentlySelectedPatientOrder = group; labProvider.getPatientLabResultByHospital(group); labProvider.getPatientSpecialResult(group); Navigator.of(context).push( CustomPageRoute( page: LabResultByClinic(labOrder: group), ), ); }, backgroundColor: AppColors.infoColor.withAlpha(20), borderColor: AppColors.infoColor.withAlpha(0), textColor: AppColors.infoColor, fontSize: (isFoldable || isTablet) ? 12.f : 14.f, isBold: true, borderRadius: 12.r, padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0), height: 40.h, iconSize: 14.h, icon: AppAssets.view_report_icon, iconColor: AppColors.infoColor, ), SizedBox(height: 16.h), ], ), ) : SizedBox.shrink(), ), ], ), ), ), ), )); }, ) : Utils.getNoDataWidget(context, noDataText: LocaleKeys.noLabResults.tr(context: context))) : // By Test or other tabs keep existing behavior (labViewModel.isLabOrdersLoading) ? Column( children: List.generate( 5, (index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, )), ) : AlphabeticScroll( alpahbetsAvailable: labViewModel.filteredIndexedCharacterForUniqueTest, details: labViewModel.filteredUniqueTestsList, labViewModel: labViewModel, rangeViewModel: rangeViewModel, appState: _appState, ) ], )); }, ), ); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file diff --git a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart index 608f70a5..88b9cb6a 100644 --- a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart +++ b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart @@ -181,10 +181,11 @@ class _ExpandableDescriptionText extends StatelessWidget { static TextStyle get textStyle => TextStyle( fontSize: 14.f, - color: AppColors.textColor, // or Colors.black87 - fontFamily: 'Poppins', - fontWeight: FontWeight.w400, - fontStyle: FontStyle.normal, + color: AppColors.textColorLight, + // or Colors.black87 + // fontFamily: 'Poppins', + fontWeight: FontWeight.w600, + fontStyle: FontStyle.normal, height: 1.4, letterSpacing: 0.0, decoration: TextDecoration.none, @@ -199,7 +200,7 @@ class _ExpandableDescriptionText extends StatelessWidget { final readMoreStyle = TextStyle( fontSize: 12.f, color: AppColors.primaryRedColor, - fontFamily: 'Poppins', + // fontFamily: 'Poppins', fontWeight: FontWeight.w500, ); diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index a6a335fc..b382bf01 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -110,7 +110,7 @@ class _MedicalFilePageState extends State { appState = getIt.get(); scheduleMicrotask(() { if (appState.isAuthenticated) { - myAppointmentsViewModel.getPatientMyDoctors(); + myAppointmentsViewModel.getPatientMyDoctors(isTop8: true); prescriptionsViewModel.initPrescriptionsViewModel(); insuranceViewModel.initInsuranceProvider(); medicalFileViewModel.setIsPatientSickLeaveListLoading(true); @@ -1046,6 +1046,7 @@ class _MedicalFilePageState extends State { ], ).onPress(() { // myAppointmentsViewModel.getPatientMyDoctors(); + myAppointmentsViewModel.setIsMyDoctorsDataToBeLoaded(true); Navigator.of(context).push( CustomPageRoute( page: MyDoctorsPage(), From 1a6975fd8a0e8547e75391f7ec4813cfc823013e Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Tue, 14 Jul 2026 14:27:34 +0300 Subject: [PATCH 11/12] Update to stores VersionID 21.6 --- lib/core/api/api_client.dart | 2 +- lib/core/api_consts.dart | 4 +- .../prescriptions_view_model.dart | 9 +- .../appointments/my_appointments_page.dart | 6 +- .../immediate_livecare_payment_page.dart | 8 +- .../notification_details_page.dart | 83 ++++++++++--------- .../notifications_list_page.dart | 3 +- pubspec.yaml | 5 +- 8 files changed, 68 insertions(+), 52 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 4015a4dc..3bfbf6cb 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -211,7 +211,7 @@ class ApiClientImp implements ApiClient { } // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 5974668; + // body['PatientID'] = 2122445; // body['PatientID'] = 53320; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index c4feec0b..2c64b13e 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -5,7 +5,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -301,7 +301,7 @@ class ApiConsts { static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations"; // ************ static values for Api **************** - static final double appVersionID = 21.5; + static final double appVersionID = 21.6; // static final double appVersionID = 50.7; static final int appChannelId = 3; diff --git a/lib/features/prescriptions/prescriptions_view_model.dart b/lib/features/prescriptions/prescriptions_view_model.dart index 5354daad..cdd85b9f 100644 --- a/lib/features/prescriptions/prescriptions_view_model.dart +++ b/lib/features/prescriptions/prescriptions_view_model.dart @@ -235,11 +235,16 @@ class PrescriptionsViewModel extends ChangeNotifier { result.fold( (failure) async { - onError!(failure.message); + if (onError != null) { + onError(failure.message); + } }, (apiResponse) { if (apiResponse.messageStatus == 2) { - onError!(apiResponse.errorMessage!); + if (onError != null) { + onError(apiResponse.errorMessage ?? "An error occurred while fetching the prescription instructions PDF."); + } + // onError!(apiResponse.errorMessage!); // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); } else if (apiResponse.messageStatus == 1) { prescriptionInstructionsPDFLink = apiResponse.data; diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index 1f54e3d1..3512472d 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -43,8 +43,10 @@ class _MyAppointmentsPageState extends State { @override void initState() { scheduleMicrotask(() { - myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); + if (!myAppointmentsViewModel.isMyAppointmentsLoading) { + myAppointmentsViewModel.initAppointmentsViewModel(); + myAppointmentsViewModel.getPatientAppointments(true, false); + } }); super.initState(); } diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart index 972606a9..ef21ccbd 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -544,12 +544,12 @@ class _ImmediateLiveCarePaymentPageState extends State { child: ListView.builder( itemCount: notificationsVM.isNotificationsLoading ? 4 - : notificationsVM.notificationsList.length + (notificationsVM.hasMoreNotifications ? 1 : 0), + // : notificationsVM.notificationsList.length + (notificationsVM.hasMoreNotifications ? 1 : 0), + : notificationsVM.notificationsList.length, physics: NeverScrollableScrollPhysics(), shrinkWrap: true, padding: EdgeInsetsGeometry.zero, diff --git a/pubspec.yaml b/pubspec.yaml index eafbd513..a86f25ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,8 +2,8 @@ name: hmg_patient_app_new description: "New HMG Patient App" publish_to: 'none' # Remove this line if you wish to publish to pub.dev -#version: 0.0.38+37 -version: 0.0.7+1 +version: 0.0.39+38 +#version: 0.0.8+1 environment: sdk: ">=3.6.0 <4.0.0" @@ -108,6 +108,7 @@ dependencies: clarity_flutter: 1.9.0 flutter_amazonpaymentservices: ^0.0.13 screen_brightness: ^1.0.1 + flutter_screenshot_blocker: ^1.0.4 dev_dependencies: flutter_test: From b793426dd66faab663be9c4af3fc57a0ca163c72 Mon Sep 17 00:00:00 2001 From: haroon amjad Date: Wed, 15 Jul 2026 11:07:45 +0300 Subject: [PATCH 12/12] Implemented CloudFlare TurnStile --- lib/core/dependencies.dart | 5 +- lib/core/services/turnstile_service.dart | 56 +++++++++++++++++++ .../authentication/authentication_repo.dart | 3 + .../authentication_view_model.dart | 15 ++++- .../send_activation_request_model.dart | 8 ++- pubspec.yaml | 1 + 6 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 lib/core/services/turnstile_service.dart diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index c4f7402c..ecfe1324 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -84,6 +84,7 @@ import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/services/notification_service.dart'; import 'package:hmg_patient_app_new/services/permission_service.dart'; +import 'package:hmg_patient_app_new/core/services/turnstile_service.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:local_auth/local_auth.dart'; @@ -155,6 +156,7 @@ class AppDependencies { ); getIt.registerLazySingleton(() => PermissionService()); + getIt.registerLazySingleton(() => TurnstileService(getIt())); // Repositories getIt.registerLazySingleton(() => CommonRepoImp(loggerService: getIt())); @@ -255,7 +257,8 @@ class AppDependencies { dialogService: getIt(), appState: getIt(), errorHandlerService: getIt(), - localAuthService: getIt()), + localAuthService: getIt(), + turnstileService: getIt()), ); getIt.registerLazySingleton(() => ProfileSettingsViewModel( diff --git a/lib/core/services/turnstile_service.dart b/lib/core/services/turnstile_service.dart new file mode 100644 index 00000000..f6665190 --- /dev/null +++ b/lib/core/services/turnstile_service.dart @@ -0,0 +1,56 @@ +import 'dart:async'; +import 'package:cloudflare_turnstile/cloudflare_turnstile.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +/// Service to handle Cloudflare Turnstile captcha verification +class TurnstileService { + final LoggerService _loggerService; + + // Replace with your Cloudflare Turnstile site key + static const String _siteKey = '0x4AAAAAAAkmbvEy63pcxBBt'; + + TurnstileService(this._loggerService); + + /// Get Turnstile token using invisible mode + /// Returns the token on success, null on failure + Future getTurnstileToken() async { + // Initialize an instance of invisible Cloudflare Turnstile with your site key + final turnstile = CloudflareTurnstile.invisible( + siteKey: _siteKey, + ); + + try { + // Get the Turnstile token + final token = await turnstile.getToken(); + _loggerService.logInfo('Turnstile token retrieved successfully'); + return token; // Return the token upon success + } on TurnstileException catch (e) { + // Handle Turnstile failure + _loggerService.logError('Turnstile challenge failed: ${e.message}'); + if (kDebugMode) { + print('Challenge failed: ${e.message}'); + } + } catch (e) { + // Handle any other exceptions + _loggerService.logError('Unexpected error during Turnstile challenge: $e'); + if (kDebugMode) { + print('Unexpected error: $e'); + } + } finally { + // Ensure the Turnstile instance is properly disposed of + turnstile.dispose(); + } + + // Return null if the token couldn't be generated + return null; + } + + /// Get the site key for Turnstile widget + String getSiteKey() => _siteKey; + + /// Validate if Turnstile is properly configured + bool isConfigured() { + return _siteKey.isNotEmpty && _siteKey != '0x4AAAAAAAkmbvEy63pcxBBt'; + } +} diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index c355819e..291e4a9b 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -147,6 +147,9 @@ class AuthenticationRepoImp implements AuthenticationRepo { bool isRegister = false, bool isFormFamilyFile = false, }) async { + + + int isOutKsa = (sendActivationCodeReq.zipCode == '966' || sendActivationCodeReq.zipCode == '+966') ? 0 : 1; sendActivationCodeReq.patientOutSA = isOutKsa; sendActivationCodeReq.isDentalAllowedBackend = false; diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index b51b36e4..6ec5d2bf 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -42,6 +42,7 @@ import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/localauth_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/core/services/turnstile_service.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; @@ -60,6 +61,7 @@ class AuthenticationViewModel extends ChangeNotifier { final DialogService _dialogService; final NavigationService _navigationService; final LocalAuthService _localAuthService; + final TurnstileService _turnstileService; AuthenticationViewModel({ required AppState appState, @@ -69,12 +71,14 @@ class AuthenticationViewModel extends ChangeNotifier { required NavigationService navigationService, required CacheService cacheService, required LocalAuthService localAuthService, + required TurnstileService turnstileService, }) : _navigationService = navigationService, _dialogService = dialogService, _errorHandlerService = errorHandlerService, _appState = appState, _authenticationRepo = authenticationRepo, - _localAuthService = localAuthService {} + _localAuthService = localAuthService, + _turnstileService = turnstileService {} final TextEditingController nationalIdController = TextEditingController(), phoneNumberController = TextEditingController(), dobController = TextEditingController(), @@ -974,6 +978,15 @@ class AuthenticationViewModel extends ChangeNotifier { // TODO: GET APP SMS SIGNATURE HERE request.sMSSignature = await getSignature(); + + // Get Turnstile token if configured + final turnstileToken = await _turnstileService.getTurnstileToken(); + if (turnstileToken != null) { + request.turnstileToken = turnstileToken; + } else { + request.turnstileToken = ""; + } + if (isNeedLoading) { LoaderBottomSheet.showLoader(); } diff --git a/lib/features/authentication/models/request_models/send_activation_request_model.dart b/lib/features/authentication/models/request_models/send_activation_request_model.dart index 2908f7ec..6e2ddb93 100644 --- a/lib/features/authentication/models/request_models/send_activation_request_model.dart +++ b/lib/features/authentication/models/request_models/send_activation_request_model.dart @@ -30,6 +30,7 @@ class SendActivationRequest { int? status; int? familyRegionID; bool? isPatientExcluded; + String? turnstileToken; SendActivationRequest( {this.patientMobileNumber, @@ -62,7 +63,8 @@ class SendActivationRequest { this.responseID, this.status, this.familyRegionID, - this.isPatientExcluded}); + this.isPatientExcluded, + this.turnstileToken}); SendActivationRequest.fromJson(Map json) { patientMobileNumber = json['PatientMobileNumber']; @@ -96,6 +98,7 @@ class SendActivationRequest { status = json['Status']; familyRegionID = json['FamilyRegionID']; isPatientExcluded = json['IsPatientExcluded']; + turnstileToken = json['TurnstileToken']; } Map toJson() { @@ -135,6 +138,9 @@ class SendActivationRequest { data['Status'] = status; data['FamilyRegionID'] = familyRegionID; data['IsPatientExcluded'] = isPatientExcluded; + if (turnstileToken != null) { + data['TurnstileToken'] = turnstileToken; + } return data; } } diff --git a/pubspec.yaml b/pubspec.yaml index a86f25ac..bdce8130 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -109,6 +109,7 @@ dependencies: flutter_amazonpaymentservices: ^0.0.13 screen_brightness: ^1.0.1 flutter_screenshot_blocker: ^1.0.4 + cloudflare_turnstile: ^3.7.2 dev_dependencies: flutter_test: