From 16e898bb6cf353ee44ff916412d736f831c85142 Mon Sep 17 00:00:00 2001 From: Sultan khan Date: Mon, 13 Apr 2026 16:06:09 +0300 Subject: [PATCH] updates and fixes --- lib/core/api_consts.dart | 2 +- lib/extensions/string_extensions.dart | 4 +- .../medical_file/medical_file_view_model.dart | 39 ++ .../models/family_file_response_model.dart | 6 +- .../profile_settings_view_model.dart | 14 +- .../appointment_details_page.dart | 2 +- .../widgets/appointment_card.dart | 395 +++++++++++++----- .../widgets/appointment_doctor_card.dart | 2 +- lib/presentation/authentication/login.dart | 2 +- .../doctor_filter/doctors_filter.dart | 8 +- .../contact_us/feedback_page.dart | 4 +- .../lab_order_result_item.dart | 6 +- .../lab/lab_results/lab_result_details.dart | 6 +- .../lab/lab_results/lab_result_list_item.dart | 6 +- .../medical_file_appointment_card.dart | 336 ++++++++++++--- .../medical_report/medical_reports_page.dart | 8 +- .../my_family/widget/family_cards.dart | 389 ++++++++++++++++- .../profile_settings/profile_settings.dart | 111 ++++- .../widgets/family_card_widget.dart | 5 +- .../widgets/profile_picture_widget.dart | 119 +++++- .../radiology/radiology_orders_page.dart | 26 +- .../symptoms_checker/risk_factors_screen.dart | 2 +- .../symptoms_checker/triage_screen.dart | 8 +- .../vital_sign/vital_sign_details_page.dart | 45 +- lib/widgets/input_widget.dart | 5 +- lib/widgets/user_avatar_widget.dart | 16 +- 26 files changed, 1301 insertions(+), 265 deletions(-) diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index ef1cd36a..819691be 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -4,7 +4,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.preProd; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 5db5b289..7a71b345 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -322,8 +322,10 @@ extension EmailValidator on String { Widget toText32({bool isEnglishOnly = false, FontWeight? weight, Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, + style: TextStyle( - height: 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.f, letterSpacing: -1, fontFamily: getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins', fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal), + + height: getIt.get().getLanguageCode() == "ar" ? 1.2 : 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.f, letterSpacing: -1, fontFamily: getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins', fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal,), ); Widget toText44({Color? color, bool isBold = false, bool isEnglishOnly = false,}) => Text( diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 05f82208..b94d1103 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -456,6 +456,7 @@ class MedicalFileViewModel extends ChangeNotifier { patientIdenficationNumber: currentUser.patientIdentificationNo, emaiLAddress: currentUser.emailAddress, genderDescription: currentUser.genderDescription, + patientImageData: _appState.getProfileImageData, // Set main user's profile image ); // Clear and start fresh with current user @@ -499,6 +500,7 @@ class MedicalFileViewModel extends ChangeNotifier { genderDescription: element.genderDescription, genderImage: element.genderImage, emaiLAddress: element.emaiLAddress, + patientImageData: element.patientImageData, // Include profile image data ); if (isPending) { @@ -774,4 +776,41 @@ class MedicalFileViewModel extends ChangeNotifier { }, ); } + + /// Update family member's profile image in cached family files + void updateFamilyMemberProfileImage(int patientID, String imageData) { + try { + bool updated = false; + + // Update in patientFamilyFiles list + for (var i = 0; i < patientFamilyFiles.length; i++) { + if (patientFamilyFiles[i].patientId == patientID || + patientFamilyFiles[i].responseId == patientID) { + patientFamilyFiles[i].patientImageData = imageData; + updated = true; + print("✅ Updated profile image for family member in patientFamilyFiles (index: $i, patientID: $patientID)"); + break; + } + } + + // Update in pendingFamilyFiles list if exists + for (var i = 0; i < pendingFamilyFiles.length; i++) { + if (pendingFamilyFiles[i].patientId == patientID || + pendingFamilyFiles[i].responseId == patientID) { + pendingFamilyFiles[i].patientImageData = imageData; + print("✅ Updated profile image for pending family member (index: $i, patientID: $patientID)"); + break; + } + } + + if (updated) { + // Notify listeners to update UI + notifyListeners(); + } else { + print("⚠️ Family member not found in cache for patientID: $patientID"); + } + } catch (e) { + print("❌ Error updating family member profile image in cache: $e"); + } + } } diff --git a/lib/features/medical_file/models/family_file_response_model.dart b/lib/features/medical_file/models/family_file_response_model.dart index f4aa557d..59aa2bd2 100644 --- a/lib/features/medical_file/models/family_file_response_model.dart +++ b/lib/features/medical_file/models/family_file_response_model.dart @@ -25,6 +25,7 @@ class FamilyFileResponseModelLists { String? statusDescription; bool? isSuperUser = false; bool? isRequestFromMySide; + String? patientImageData; FamilyFileResponseModelLists( {this.id, @@ -50,7 +51,8 @@ class FamilyFileResponseModelLists { this.patientName, this.statusDescription, this.isSuperUser, - this.isRequestFromMySide}); + this.isRequestFromMySide, + this.patientImageData}); factory FamilyFileResponseModelLists.fromRawJson(String str) => FamilyFileResponseModelLists.fromJson(json.decode(str)); @@ -81,6 +83,7 @@ class FamilyFileResponseModelLists { statusDescription: json["StatusDescription"], isSuperUser: json["isSuperUser"] ?? false, isRequestFromMySide: json["isRequestFromMySide"] ?? false, + patientImageData: json["PatientImageData"], ); Map toJson() => { @@ -108,5 +111,6 @@ class FamilyFileResponseModelLists { "StatusDescription": statusDescription, "isSuperUser": isSuperUser, "isRequestFromMySide": isRequestFromMySide, + "PatientImageData": patientImageData, }; } diff --git a/lib/features/profile_settings/profile_settings_view_model.dart b/lib/features/profile_settings/profile_settings_view_model.dart index 77960569..b208494a 100644 --- a/lib/features/profile_settings/profile_settings_view_model.dart +++ b/lib/features/profile_settings/profile_settings_view_model.dart @@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/models/get_patient_info_response_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_repo.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; @@ -219,9 +220,10 @@ class ProfileSettingsViewModel extends ChangeNotifier { if (response.data != null && response.data['Patient_GetProfileImageDataList'] != null) { var imageList = response.data['Patient_GetProfileImageDataList']; if (imageList is List && imageList.isNotEmpty) { - profileImageData = imageList[0]['PatientsProfileImageData']; + profileImageData = imageList[0]['ImageData']; // Store in AppState for global access GetIt.instance().setProfileImageData = profileImageData; + } else { profileImageData = null; GetIt.instance().setProfileImageData = null; @@ -286,6 +288,16 @@ class ProfileSettingsViewModel extends ChangeNotifier { profileImageData = imageData; // Store in AppState for global access GetIt.instance().setProfileImageData = imageData; + + // Update the family files cache with the new profile image + try { + final medicalFileViewModel = GetIt.instance.get(); + medicalFileViewModel.updateFamilyMemberProfileImage(patientID, imageData); + print("✅ Updated profile image in family files cache for patient: $patientID"); + } catch (e) { + print("⚠️ Could not update family files cache: $e"); + } + notifyListeners(); onSuccess?.call(response.data); }, diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index 5344fead..78532cce 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -1072,7 +1072,7 @@ class _AppointmentDetailsPageState extends State { }, backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction), borderColor: - AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.01), + AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), textColor: widget.patientAppointmentHistoryResponseModel.nextAction == 15 ? AppColors.textColor : Colors.white, fontSize: 16.f, fontWeight: FontWeight.w600, diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index d551c444..8923e9fa 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -17,7 +18,6 @@ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_vie import 'package:hmg_patient_app_new/features/my_appointments/utils/appointment_type.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/appointment_details_page.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/ask_doctor_request_type_select.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/eye_measurement_details_page.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -27,8 +27,9 @@ 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 'dart:ui' as ui; +import 'package:hmg_patient_app_new/presentation/appointments/appointment_payment_page.dart'; -class AppointmentCard extends StatelessWidget { +class AppointmentCard extends StatefulWidget { final PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel; final MyAppointmentsViewModel myAppointmentsViewModel; final bool isLoading; @@ -56,6 +57,97 @@ class AppointmentCard extends StatelessWidget { this.contactUsViewModel, this.isForRate = false}); + @override + State createState() => _AppointmentCardState(); +} + +class _AppointmentCardState extends State { + Timer? _countdownTimer; + Duration? _timeRemaining; + Function(void Function())? _modalSetState; + + @override + void initState() { + super.initState(); + // Start countdown timer if appointment requires payment + if (widget.patientAppointmentHistoryResponseModel.nextAction == 15) { + _startCountdownTimer(); + } + } + + @override + void dispose() { + _countdownTimer?.cancel(); + super.dispose(); + } + + void _startCountdownTimer() { + final appointmentDate = DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate!); + final expiryDate = appointmentDate.subtract(const Duration(hours: 4)); + + setState(() { + _timeRemaining = expiryDate.difference(DateTime.now()); + }); + + _countdownTimer?.cancel(); + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + final newTimeRemaining = expiryDate.difference(DateTime.now()); + + if (newTimeRemaining.isNegative) { + timer.cancel(); + setState(() { + _timeRemaining = Duration.zero; + }); + _modalSetState?.call(() { + _timeRemaining = Duration.zero; + }); + } else { + setState(() { + _timeRemaining = newTimeRemaining; + }); + _modalSetState?.call(() { + _timeRemaining = newTimeRemaining; + }); + } + }); + } + + // Helper method to build each time unit (number + label) + Widget _buildTimeUnit(String value, String label) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + alignment: Alignment.center, + child: value.toText32( + isBold: true, + color: AppColors.blackColor, + isEnglishOnly: true, + ), + ), + SizedBox(height: 4.h), + Container( + alignment: Alignment.center, + child: label.toText12( + color: AppColors.greyTextColor, + ), + ), + ], + ); + } + + // Helper method to build time separator (:) + Widget _buildTimeSeparator() { + return Padding( + padding: EdgeInsets.only(bottom: 16.h, left: 6.w, right: 6.w), + child: ':'.toText32( + isBold: true, + color: AppColors.blackColor, + ), + ); + } + @override Widget build(BuildContext context) { final appState = getIt.get(); @@ -66,11 +158,11 @@ class AppointmentCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - isForRate ? SizedBox() : _buildHeader(context, appState), + widget.isForRate ? SizedBox() : _buildHeader(context, appState), SizedBox(height: 16.h), _buildDoctorRow(context), SizedBox(height: 16.h), - isForRate ? SizedBox() : _buildActionArea(context, appState), + widget.isForRate ? SizedBox() : _buildActionArea(context, appState), ], ), ), @@ -87,7 +179,7 @@ class AppointmentCard extends StatelessWidget { } Widget _buildChips(BuildContext context, AppState appState) { - final isLiveCare = !isLoading && patientAppointmentHistoryResponseModel.isLiveCareAppointment!; + final isLiveCare = !widget.isLoading && widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment!; return Wrap( alignment: WrapAlignment.start, @@ -96,25 +188,25 @@ class AppointmentCard extends StatelessWidget { runSpacing: 6.h, children: [ AppCustomChipWidget( - icon: isLoading ? AppAssets.walkin_appointment_icon : (isLiveCare ? AppAssets.small_livecare_icon : AppAssets.walkin_appointment_icon), - iconColor: isLoading ? AppColors.textColor : (isLiveCare ? Colors.white : AppColors.textColor), - labelText: isLoading ? LocaleKeys.walkin.tr(context: context) : (isLiveCare ? LocaleKeys.livecare.tr(context: context) : LocaleKeys.walkin.tr(context: context)), - backgroundColor: isLoading ? AppColors.greyColor : (isLiveCare ? AppColors.successColor : AppColors.greyColor), - textColor: isLoading ? AppColors.textColor : (isLiveCare ? Colors.white : AppColors.textColor), - ).toShimmer2(isShow: isLoading), + icon: widget.isLoading ? AppAssets.walkin_appointment_icon : (isLiveCare ? AppAssets.small_livecare_icon : AppAssets.walkin_appointment_icon), + iconColor: widget.isLoading ? AppColors.textColor : (isLiveCare ? Colors.white : AppColors.textColor), + labelText: widget.isLoading ? LocaleKeys.walkin.tr(context: context) : (isLiveCare ? LocaleKeys.livecare.tr(context: context) : LocaleKeys.walkin.tr(context: context)), + backgroundColor: widget.isLoading ? AppColors.greyColor : (isLiveCare ? AppColors.successColor : AppColors.greyColor), + textColor: widget.isLoading ? AppColors.textColor : (isLiveCare ? Colors.white : AppColors.textColor), + ).toShimmer2(isShow: widget.isLoading), AppCustomChipWidget( labelText: - isLoading ? 'OutPatient' : (appState.isArabic() ? patientAppointmentHistoryResponseModel.isInOutPatientDescriptionN! : patientAppointmentHistoryResponseModel.isInOutPatientDescription!), + widget.isLoading ? 'OutPatient' : (appState.isArabic() ? widget.patientAppointmentHistoryResponseModel.isInOutPatientDescriptionN! : widget.patientAppointmentHistoryResponseModel.isInOutPatientDescription!), backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.1), textColor: AppColors.warningColorYellow, - ).toShimmer2(isShow: isLoading), + ).toShimmer2(isShow: widget.isLoading), AppCustomChipWidget( - labelText: isLoading ? 'Booked' : AppointmentType.getAppointmentStatusType(patientAppointmentHistoryResponseModel.patientStatusType!), + labelText: widget.isLoading ? 'Booked' : AppointmentType.getAppointmentStatusType(widget.patientAppointmentHistoryResponseModel.patientStatusType!), backgroundColor: AppColors.successColor.withValues(alpha: 0.1), textColor: AppColors.successColor, - ).toShimmer2(isShow: isLoading), + ).toShimmer2(isShow: widget.isLoading), ], - ).toShimmer2(isShow: isLoading); + ).toShimmer2(isShow: widget.isLoading); } Widget _buildDoctorRow(BuildContext context) { @@ -125,11 +217,11 @@ class AppointmentCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Image.network( - isLoading ? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png' : patientAppointmentHistoryResponseModel.doctorImageURL!, + widget.isLoading ? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png' : widget.patientAppointmentHistoryResponseModel.doctorImageURL!, width: 63.h, height: 63.h, fit: BoxFit.cover, - ).circle(100.r).toShimmer2(isShow: isLoading), + ).circle(100.r).toShimmer2(isShow: widget.isLoading), Transform.translate( offset: Offset(0.0, -20.h), child: Container( @@ -149,11 +241,11 @@ class AppointmentCard extends StatelessWidget { Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h, iconColor: AppColors.ratingColorYellow), SizedBox(height: 2.h), (isFoldable || isTablet) - ? "${patientAppointmentHistoryResponseModel.decimalDoctorRate}".toText9(isBold: true, color: AppColors.textColor, isEnglishOnly: true) - : "${patientAppointmentHistoryResponseModel.decimalDoctorRate ?? "0.0"}".toText11(isBold: true, color: AppColors.textColor, isEnglishOnly: true), + ? "${widget.patientAppointmentHistoryResponseModel.decimalDoctorRate}".toText9(isBold: true, color: AppColors.textColor, isEnglishOnly: true) + : "${widget.patientAppointmentHistoryResponseModel.decimalDoctorRate ?? "0.0"}".toText11(isBold: true, color: AppColors.textColor, isEnglishOnly: true), ], ), - ).circle(100).toShimmer2(isShow: isLoading), + ).circle(100).toShimmer2(isShow: widget.isLoading), ), ], ), @@ -164,20 +256,20 @@ class AppointmentCard extends StatelessWidget { children: [ Row( children: [ - (isLoading ? 'Dr' : "${patientAppointmentHistoryResponseModel.doctorTitle}").toText16(isBold: true, maxlines: 1), - (isLoading ? 'John Doe' : " ${patientAppointmentHistoryResponseModel.doctorNameObj!.truncate(20)}") - .toText16(isBold: true, maxlines: 1, isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")), + (widget.isLoading ? 'Dr' : "${widget.patientAppointmentHistoryResponseModel.doctorTitle}").toText16(isBold: true, maxlines: 1), + (widget.isLoading ? 'John Doe' : " ${widget.patientAppointmentHistoryResponseModel.doctorNameObj!.truncate(20)}") + .toText16(isBold: true, maxlines: 1, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")), SizedBox(width: 12.w), - (patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null && patientAppointmentHistoryResponseModel.doctorNationalityFlagURL!.isNotEmpty) + (widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null && widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL!.isNotEmpty) ? Image.network( - patientAppointmentHistoryResponseModel.doctorNationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png", + widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png", width: 20.h, height: 15.h, fit: BoxFit.cover, ) : SizedBox.shrink(), ], - ).toShimmer2(isShow: isLoading), + ).toShimmer2(isShow: widget.isLoading), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, @@ -185,29 +277,29 @@ class AppointmentCard extends StatelessWidget { runSpacing: 4.h, children: [ AppCustomChipWidget( - labelText: isLoading + labelText: widget.isLoading ? 'Cardiology' - : (patientAppointmentHistoryResponseModel.clinicName!.length > 20 - ? '${patientAppointmentHistoryResponseModel.clinicName!.substring(0, 20)}...' - : patientAppointmentHistoryResponseModel.clinicName!), - ).toShimmer2(isShow: isLoading), + : (widget.patientAppointmentHistoryResponseModel.clinicName!.length > 20 + ? '${widget.patientAppointmentHistoryResponseModel.clinicName!.substring(0, 20)}...' + : widget.patientAppointmentHistoryResponseModel.clinicName!), + ).toShimmer2(isShow: widget.isLoading), AppCustomChipWidget( - labelText: isLoading + labelText: widget.isLoading ? 'Olaya' - : (patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital").length > 15 - ? '${(patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital").substring(0, 15)}...' - : patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital") - .toShimmer2(isShow: isLoading), + : (widget.patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital").length > 15 + ? '${(widget.patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital").substring(0, 15)}...' + : widget.patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital") + .toShimmer2(isShow: widget.isLoading), Directionality( textDirection: ui.TextDirection.ltr, child: AppCustomChipWidget( labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), icon: AppAssets.appointment_calendar_icon, - richText: isLoading - ? 'Cardiology'.toText10().toShimmer2(isShow: isLoading) - : "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)}" + richText: widget.isLoading + ? 'Cardiology'.toText10().toShimmer2(isShow: widget.isLoading) + : "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false)}" .toText10(isEnglishOnly: true, isBold: true), - ).toShimmer2(isShow: isLoading), + ).toShimmer2(isShow: widget.isLoading), ), // AppCustomChipWidget( @@ -240,10 +332,10 @@ class AppointmentCard extends StatelessWidget { } Widget _buildActionArea(BuildContext context, AppState appState) { - if ((((patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) || - (patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) || - !Utils.isClinicAllowedForRebook(patientAppointmentHistoryResponseModel.clinicID ?? 0))) && - AppointmentType.isArrived(patientAppointmentHistoryResponseModel)) { + if ((((widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) || + (widget.patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) || + !Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID ?? 0))) && + AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) { // if (((patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) && // DateTime.now().difference(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate)).inDays <= 15)) { // return Row( @@ -325,25 +417,25 @@ class AppointmentCard extends StatelessWidget { // ); // } else { return CustomButton( - text: isFromMedicalReport ? LocaleKeys.selectAppointment.tr(context: context) : LocaleKeys.viewDetails.tr(context: context), + text: widget.isFromMedicalReport ? LocaleKeys.selectAppointment.tr(context: context) : LocaleKeys.viewDetails.tr(context: context), onPressed: () { - if (isFromMedicalReport) { - if (isForFeedback) { - contactUsViewModel!.setPatientFeedbackSelectedAppointment(patientAppointmentHistoryResponseModel); + if (widget.isFromMedicalReport) { + if (widget.isForFeedback) { + widget.contactUsViewModel!.setPatientFeedbackSelectedAppointment(widget.patientAppointmentHistoryResponseModel); } else { - medicalFileViewModel!.setSelectedMedicalReportAppointment(patientAppointmentHistoryResponseModel); + widget.medicalFileViewModel!.setSelectedMedicalReportAppointment(widget.patientAppointmentHistoryResponseModel); } Navigator.pop(context, false); } else { Navigator.of(context) .push( CustomPageRoute( - page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), ), ) .then((_) { - myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); + widget.myAppointmentsViewModel.initAppointmentsViewModel(); + widget.myAppointmentsViewModel.getPatientAppointments(true, false); }); } }, @@ -356,23 +448,23 @@ class AppointmentCard extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 10.w), // height: isTablet || isFoldable ? 46.h : 40.h, height: 40.h, - icon: isFromMedicalReport ? AppAssets.checkmark_icon : null, + icon: widget.isFromMedicalReport ? AppAssets.checkmark_icon : null, iconColor: AppColors.primaryRedColor, iconSize: 16.h, ); // } } else { - if (isFromMedicalReport) { - if (isForEyeMeasurements) { + if (widget.isFromMedicalReport) { + if (widget.isForEyeMeasurements) { return SizedBox.shrink(); } else { return CustomButton( text: LocaleKeys.selectAppointment.tr(context: context), onPressed: () { - if (isForFeedback) { - contactUsViewModel!.setPatientFeedbackSelectedAppointment(patientAppointmentHistoryResponseModel); + if (widget.isForFeedback) { + widget.contactUsViewModel!.setPatientFeedbackSelectedAppointment(widget.patientAppointmentHistoryResponseModel); } else { - medicalFileViewModel!.setSelectedMedicalReportAppointment(patientAppointmentHistoryResponseModel); + widget.medicalFileViewModel!.setSelectedMedicalReportAppointment(widget.patientAppointmentHistoryResponseModel); } Navigator.pop(context, false); }, @@ -391,33 +483,33 @@ class AppointmentCard extends StatelessWidget { ); } } - return (patientAppointmentHistoryResponseModel.isActiveDoctor ?? true) - ? (AppointmentType.isArrived(patientAppointmentHistoryResponseModel) && - patientAppointmentHistoryResponseModel.isClinicReBookingAllowed == false) + return (widget.patientAppointmentHistoryResponseModel.isActiveDoctor ?? true) + ? (AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) && + widget.patientAppointmentHistoryResponseModel.isClinicReBookingAllowed == false) ? // Show only View Details button without arrow when rebooking not allowed _getArrivedButton(context) : Row( children: [ Expanded( flex: 6, - child: (AppointmentType.isArrived(patientAppointmentHistoryResponseModel) + child: (AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? _getArrivedButton(context) : CustomButton( - text: AppointmentType.getNextActionText(patientAppointmentHistoryResponseModel.nextAction), - onPressed: () => _goToDetails(context), - backgroundColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.15), - borderColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), - textColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), + text: AppointmentType.getNextActionText(widget.patientAppointmentHistoryResponseModel.nextAction), + onPressed: () => handleAppointmentNextAction(widget.patientAppointmentHistoryResponseModel.nextAction, context), + backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.15), + borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), + textColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction), fontSize: (isFoldable || isTablet) ? 12.f : 14.f, fontWeight: FontWeight.w600, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), height: 40.h, - icon: AppointmentType.getNextActionIcon(patientAppointmentHistoryResponseModel.nextAction), - iconColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), + icon: AppointmentType.getNextActionIcon(widget.patientAppointmentHistoryResponseModel.nextAction), + iconColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction), iconSize: 15.h, )) - .toShimmer2(isShow: isLoading), + .toShimmer2(isShow: widget.isLoading), ), SizedBox(width: 8.h), Expanded( @@ -442,7 +534,7 @@ class AppointmentCard extends StatelessWidget { fit: BoxFit.contain, ), ), - ).toShimmer2(isShow: isLoading).onPress(() { + ).toShimmer2(isShow: widget.isLoading).onPress(() { _goToDetails(context); }), ), @@ -454,12 +546,12 @@ class AppointmentCard extends StatelessWidget { Navigator.of(context) .push( CustomPageRoute( - page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), ), ) .then((_) { - myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); + widget.myAppointmentsViewModel.initAppointmentsViewModel(); + widget.myAppointmentsViewModel.getPatientAppointments(true, false); }); }, backgroundColor: AppColors.secondaryLightRedColor, @@ -471,7 +563,7 @@ class AppointmentCard extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 10.w), // height: isTablet || isFoldable ? 46.h : 40.h, height: 40.h, - icon: isFromMedicalReport ? AppAssets.checkmark_icon : null, + icon: widget.isFromMedicalReport ? AppAssets.checkmark_icon : null, iconColor: AppColors.primaryRedColor, iconSize: 16.h, ); @@ -479,7 +571,7 @@ class AppointmentCard extends StatelessWidget { } Widget _getArrivedButton(BuildContext context) { - if (patientAppointmentHistoryResponseModel.isClinicReBookingAllowed == false) { + if (widget.patientAppointmentHistoryResponseModel.isClinicReBookingAllowed == false) { return CustomButton( text: LocaleKeys.viewDetails.tr(context: context), onPressed: () => _goToDetails(context), @@ -518,21 +610,21 @@ class AppointmentCard extends StatelessWidget { } void _goToDetails(BuildContext context) { - if (isFromMedicalReport) return; - if (isForEyeMeasurements) { + if (widget.isFromMedicalReport) return; + if (widget.isForEyeMeasurements) { Navigator.of(context).push( CustomPageRoute( - page: EyeMeasurementDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), + page: EyeMeasurementDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), ), ); } else { - if (!AppointmentType.isArrived(patientAppointmentHistoryResponseModel)) { - bookAppointmentsViewModel.getAppointmentNearestGate(projectID: patientAppointmentHistoryResponseModel.projectID, clinicID: patientAppointmentHistoryResponseModel.clinicID); + if (!AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) { + widget.bookAppointmentsViewModel.getAppointmentNearestGate(projectID: widget.patientAppointmentHistoryResponseModel.projectID, clinicID: widget.patientAppointmentHistoryResponseModel.clinicID); } Navigator.of(context) .push( CustomPageRoute( - page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), ), ) .then((_) { @@ -544,22 +636,22 @@ class AppointmentCard extends StatelessWidget { void openDoctorScheduleCalendar(BuildContext context) async { final doctor = DoctorsListResponseModel( - clinicID: patientAppointmentHistoryResponseModel.clinicID, - projectID: patientAppointmentHistoryResponseModel.projectID, - doctorID: patientAppointmentHistoryResponseModel.doctorID, - doctorImageURL: patientAppointmentHistoryResponseModel.doctorImageURL, - doctorTitle: patientAppointmentHistoryResponseModel.doctorTitle, - name: patientAppointmentHistoryResponseModel.doctorNameObj, + clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + doctorID: widget.patientAppointmentHistoryResponseModel.doctorID, + doctorImageURL: widget.patientAppointmentHistoryResponseModel.doctorImageURL, + doctorTitle: widget.patientAppointmentHistoryResponseModel.doctorTitle, + name: widget.patientAppointmentHistoryResponseModel.doctorNameObj, nationalityFlagURL: '', speciality: [], - clinicName: patientAppointmentHistoryResponseModel.clinicName, - projectName: patientAppointmentHistoryResponseModel.projectName, + clinicName: widget.patientAppointmentHistoryResponseModel.clinicName, + projectName: widget.patientAppointmentHistoryResponseModel.projectName, ); - bookAppointmentsViewModel.setSelectedDoctor(doctor); + widget.bookAppointmentsViewModel.setSelectedDoctor(doctor); LoaderBottomSheet.showLoader(); - await bookAppointmentsViewModel.getDoctorFreeSlots( + await widget.bookAppointmentsViewModel.getDoctorFreeSlots( isBookingForLiveCare: false, onSuccess: (respData) async { LoaderBottomSheet.hideLoader(); @@ -584,4 +676,121 @@ class AppointmentCard extends StatelessWidget { }, ); } + + Future handleAppointmentNextAction(nextAction, BuildContext context) async { + switch (nextAction) { + case 0: + // No action needed + _goToDetails(context); + break; + case 10: + // Confirm appointment - go to details + _goToDetails(context); + break; + case 15: + // Pending payment - show waiting modal + // Ensure timer is running before showing modal + if (_countdownTimer == null || !_countdownTimer!.isActive) { + _startCountdownTimer(); + } + + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.notice.tr(), + child: StatefulBuilder( + builder: (context, setModalState) { + // Store the modal setState callback + _modalSetState = setModalState; + + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Message text + LocaleKeys.upcomingPaymentPending.tr(context: context).toText14( + color: AppColors.textColor, + isCenter: true, + ), + SizedBox(height: 24.h), + // Countdown Timer - DD : HH : MM : SS format with labels + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Days + _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inDays.toString().padLeft(2, '0') : '00', + LocaleKeys.days.tr(context: context), + ), + _buildTimeSeparator(), + // Hours + _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inHours.remainder(24).toString().padLeft(2, '0') : '00', + LocaleKeys.hours.tr(context: context), + ), + _buildTimeSeparator(), + // Minutes + _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inMinutes.remainder(60).toString().padLeft(2, '0') : '00', + LocaleKeys.minutes.tr(context: context), + ), + _buildTimeSeparator(), + // Seconds + _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inSeconds.remainder(60).toString().padLeft(2, '0') : '00', + LocaleKeys.seconds.tr(context: context), + ), + ], + ), + SizedBox(height: 24.h), + // Green Acknowledge button with checkmark icon + CustomButton( + text: LocaleKeys.acknowledged.tr(context: context), + onPressed: () { + _modalSetState = null; // Clear callback when closing + Navigator.of(context).pop(); + }, + backgroundColor: AppColors.successColor, + borderColor: AppColors.successColor, + textColor: Colors.white, + fontSize: 16.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + height: 50.h, + icon: AppAssets.checkmark_icon, + iconColor: Colors.white, + ), + ], + ); + }, + ), + callBackFunc: () { + _modalSetState = null; // Clear callback when closing + }, + isFullScreen: false, + isCloseButtonVisible: true, + ); + break; + case 20: + // Pay now - navigate to payment page + widget.myAppointmentsViewModel.setIsPatientAppointmentShareLoading(true); + Navigator.of(context).push( + CustomPageRoute( + page: AppointmentPaymentPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + ), + ); + break; + case 50: + // Confirm livecare - go to details + _goToDetails(context); + break; + case 90: + // Check-in - go to details + _goToDetails(context); + break; + default: + // Default - go to details + _goToDetails(context); + } + } } + diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index 4b5df07b..293f58ac 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -142,7 +142,7 @@ class AppointmentDoctorCard extends StatelessWidget { ], ), ), - patientAppointmentHistoryResponseModel.isLiveCareAppointment! + patientAppointmentHistoryResponseModel.isLiveCareAppointment! || patientAppointmentHistoryResponseModel.isClinicReBookingAllowed! ==false ? SizedBox.shrink() : Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown).onPress(() async { DoctorsListResponseModel selectedDoctor = DoctorsListResponseModel(); diff --git a/lib/presentation/authentication/login.dart b/lib/presentation/authentication/login.dart index d559ebbc..b2979235 100644 --- a/lib/presentation/authentication/login.dart +++ b/lib/presentation/authentication/login.dart @@ -71,7 +71,7 @@ class LoginScreenState extends State { children: [ Utils.showLottie(context: context, assetPath: AppAnimations.login, width: 200.h, height: 200.h, repeat: true, fit: BoxFit.cover), SizedBox(height: 130.h), // Adjusted to sizer unit - LocaleKeys.welcomeToDrSulaiman.tr(context: context).toText32(isBold: true, color: AppColors.textColor), + LocaleKeys.welcomeToDrSulaiman.tr(context: context).toText32(isBold: true, color: AppColors.textColor ), SizedBox(height: 32.h), Localizations.override(context: context, locale: Locale('en', 'US'), child: Container()), // Force English locale for this widget TextInputWidget( diff --git a/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart b/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart index 02dd9b97..c09fbfef 100644 --- a/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart +++ b/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart @@ -1,6 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -59,7 +61,7 @@ class DoctorsFilters extends StatelessWidget{ Text( LocaleKeys.filters.tr(), style:TextStyle( - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', fontWeight: FontWeight.w600, fontSize: 27.f, color: AppColors.textColor, @@ -69,7 +71,7 @@ class DoctorsFilters extends StatelessWidget{ Text( LocaleKeys.clearAllFilters.tr(), style:TextStyle( - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', fontWeight: FontWeight.w600, fontSize: 14.f, color: AppColors.errorColor @@ -199,7 +201,7 @@ class DoctorsFilters extends StatelessWidget{ Text( title, style:TextStyle( - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', fontWeight: FontWeight.w600, fontSize: 16.f, color: AppColors.textColor, diff --git a/lib/presentation/contact_us/feedback_page.dart b/lib/presentation/contact_us/feedback_page.dart index cc70f28e..8ca6619c 100644 --- a/lib/presentation/contact_us/feedback_page.dart +++ b/lib/presentation/contact_us/feedback_page.dart @@ -475,12 +475,12 @@ class FeedbackPage extends StatelessWidget { ), SizedBox(height: 8.h), "${contactUsViewModel.cocItemsList[index].cOCTitle}" - .toText16(weight: FontWeight.bold, fontFamily: Utils.isArabicText(contactUsViewModel.cocItemsList[index].cOCTitle ?? "") ? "GESSTwo" : "Poppins"), + .toText16(weight: FontWeight.bold, fontFamily: Utils.isArabicText(contactUsViewModel.cocItemsList[index].cOCTitle ?? "") ? "CairoArabic" : "Poppins"), SizedBox(height: 8.h), "${contactUsViewModel.cocItemsList[index].detail}".toText14( weight: FontWeight.w600, color: AppColors.textColorLight, - fontFamily: Utils.isArabicText(contactUsViewModel.cocItemsList[index].detail ?? "") ? "GESSTwo" : "Poppins"), + fontFamily: Utils.isArabicText(contactUsViewModel.cocItemsList[index].detail ?? "") ? "CairoArabic" : "Poppins"), ], ), ).paddingOnly(bottom: 16.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 52dba2dd..ad2ad77f 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 @@ -2,6 +2,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; @@ -58,7 +60,7 @@ class LabOrderResultItem extends StatelessWidget { style: TextStyle( fontSize: 24.f, fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', color: tests!.checkIfGraphShouldBeDisplayed() ? context.read().getColor( tests?.calculatedResultFlag ?? "", @@ -77,7 +79,7 @@ class LabOrderResultItem extends StatelessWidget { style: TextStyle( fontSize: 12.f, fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', color: AppColors.greyTextColor, ), softWrap: true, diff --git a/lib/presentation/lab/lab_results/lab_result_details.dart b/lib/presentation/lab/lab_results/lab_result_details.dart index fb5fd583..9fbb49a8 100644 --- a/lib/presentation/lab/lab_results/lab_result_details.dart +++ b/lib/presentation/lab/lab_results/lab_result_details.dart @@ -286,7 +286,7 @@ class _LabResultDetailsState extends State { style: TextStyle( fontSize: 12.f, fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', color: AppColors.greyTextColor, ), softWrap: true, @@ -364,7 +364,7 @@ class _LabResultDetailsState extends State { value, style: TextStyle( fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', fontSize: 8.f, color: AppColors.textColor, ), @@ -376,7 +376,7 @@ class _LabResultDetailsState extends State { padding: const EdgeInsets.only(top: 8.0), child: Text( label, - style: TextStyle(fontSize: 8.f, fontFamily: 'Poppins', fontWeight: FontWeight.w600, color: AppColors.labelTextColor), + style: TextStyle(fontSize: 8.f, fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', fontWeight: FontWeight.w600, color: AppColors.labelTextColor), ), ); } diff --git a/lib/presentation/lab/lab_results/lab_result_list_item.dart b/lib/presentation/lab/lab_results/lab_result_list_item.dart index 908d129d..78369463 100644 --- a/lib/presentation/lab/lab_results/lab_result_list_item.dart +++ b/lib/presentation/lab/lab_results/lab_result_list_item.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.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/utils/utils.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -25,7 +27,7 @@ class LabHistoryItem extends StatelessWidget{ style: TextStyle( fontSize: 14.f, fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', color: AppColors.labelTextColor ), ), @@ -34,7 +36,7 @@ class LabHistoryItem extends StatelessWidget{ style: TextStyle( fontSize: 18.f, fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', color: AppColors.textColor ), ) diff --git a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart index 13ea39dd..2e769609 100644 --- a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart +++ b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -13,14 +14,16 @@ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_vie import 'package:hmg_patient_app_new/features/my_appointments/utils/appointment_type.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/appointment_details_page.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/appointment_payment_page.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/routes/custom_page_route.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'dart:ui' as ui; -class MedicalFileAppointmentCard extends StatelessWidget { +class MedicalFileAppointmentCard extends StatefulWidget { final PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel; final MyAppointmentsViewModel myAppointmentsViewModel; final Function onRescheduleTap; @@ -34,6 +37,97 @@ class MedicalFileAppointmentCard extends StatelessWidget { required this.onAskDoctorTap, }); + @override + State createState() => _MedicalFileAppointmentCardState(); +} + +class _MedicalFileAppointmentCardState extends State { + Timer? _countdownTimer; + Duration? _timeRemaining; + Function(void Function())? _modalSetState; + + @override + void initState() { + super.initState(); + // Start countdown timer if appointment requires payment + if (widget.patientAppointmentHistoryResponseModel.nextAction == 15) { + _startCountdownTimer(); + } + } + + @override + void dispose() { + _countdownTimer?.cancel(); + super.dispose(); + } + + void _startCountdownTimer() { + final appointmentDate = DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate!); + final expiryDate = appointmentDate.subtract(const Duration(hours: 4)); + + setState(() { + _timeRemaining = expiryDate.difference(DateTime.now()); + }); + + _countdownTimer?.cancel(); + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + final newTimeRemaining = expiryDate.difference(DateTime.now()); + + if (newTimeRemaining.isNegative) { + timer.cancel(); + setState(() { + _timeRemaining = Duration.zero; + }); + _modalSetState?.call(() { + _timeRemaining = Duration.zero; + }); + } else { + setState(() { + _timeRemaining = newTimeRemaining; + }); + _modalSetState?.call(() { + _timeRemaining = newTimeRemaining; + }); + } + }); + } + + // Helper method to build each time unit (number + label) + Widget _buildTimeUnit(String value, String label) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + alignment: Alignment.center, + child: value.toText32( + isBold: true, + color: AppColors.blackColor, + isEnglishOnly: true, + ), + ), + SizedBox(height: 4.h), + Container( + alignment: Alignment.center, + child: label.toText12( + color: AppColors.greyTextColor, + ), + ), + ], + ); + } + + // Helper method to build time separator (:) + Widget _buildTimeSeparator() { + return Padding( + padding: EdgeInsets.only(bottom: 16.h, left: 6.w, right: 6.w), + child: ':'.toText32( + isBold: true, + color: AppColors.blackColor, + ), + ); + } + @override Widget build(BuildContext context) { AppState appState = getIt.get(); @@ -43,17 +137,17 @@ class MedicalFileAppointmentCard extends StatelessWidget { AppCustomChipWidget( richText: Directionality( textDirection: ui.TextDirection.ltr, - child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false) - .toText12(color: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor, isBold: true, isEnglishOnly: true) + child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false) + .toText12(color: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor, isBold: true, isEnglishOnly: true) .paddingSymmetrical(8.w, 0), ), - icon: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) ? AppAssets.appointment_calendar_icon : AppAssets.alarm_clock_icon, - iconColor: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor, + icon: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? AppAssets.appointment_calendar_icon : AppAssets.alarm_clock_icon, + iconColor: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor, iconSize: 16.w, - backgroundColor: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) ? AppColors.greyColor : AppColors.secondaryLightRedColor, - textColor: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor, + backgroundColor: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? AppColors.greyColor : AppColors.secondaryLightRedColor, + textColor: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor, padding: EdgeInsets.only(top: 12.h, bottom: 12.h, left: 8.w, right: 8.w), - ).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading), + ).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading), SizedBox(height: 16.h), Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false), @@ -64,20 +158,20 @@ class MedicalFileAppointmentCard extends StatelessWidget { Row( children: [ Image.network( - patientAppointmentHistoryResponseModel.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", + widget.patientAppointmentHistoryResponseModel.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", width: 25.w, height: 27.h, fit: BoxFit.fill, - ).circle(100).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading), + ).circle(100).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading), SizedBox(width: 8.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - (patientAppointmentHistoryResponseModel.doctorNameObj ?? "").toText14(isBold: true, maxlines: 1, isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? "")).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading), - (patientAppointmentHistoryResponseModel.clinicName ?? "") + (widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "").toText14(isBold: true, maxlines: 1, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "")).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading), + (widget.patientAppointmentHistoryResponseModel.clinicName ?? "") .toText12(maxLine: 1, isBold: true, color: AppColors.greyTextColor) - .toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading), + .toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading), ], ), ), @@ -86,42 +180,35 @@ class MedicalFileAppointmentCard extends StatelessWidget { SizedBox(height: 12.h), Row( children: [ - myAppointmentsViewModel.isMyAppointmentsLoading + widget.myAppointmentsViewModel.isMyAppointmentsLoading ? Container().toShimmer2(isShow: true, height: 40.h, width: 100.w, radius: 12.r) : Expanded( flex: 7, - child: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) - ? getArrivedAppointmentButton(context).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading) + child: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) + ? getArrivedAppointmentButton(context).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading) : CustomButton( - text: AppointmentType.getNextActionText(patientAppointmentHistoryResponseModel.nextAction), + text: AppointmentType.getNextActionText(widget.patientAppointmentHistoryResponseModel.nextAction), onPressed: () { - Navigator.of(context) - .push(CustomPageRoute( - page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), - )) - .then((val) { - // widget.myAppointmentsViewModel.initAppointmentsViewModel(); - // widget.myAppointmentsViewModel.getPatientAppointments(true, false); - }); + handleAppointmentNextAction(widget.patientAppointmentHistoryResponseModel.nextAction, context); }, - backgroundColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.15), - borderColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.01), - textColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), + backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.15), + borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), + textColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction), fontSize: 14.f, fontWeight: FontWeight.w600, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), height: 40.h, - icon: AppointmentType.getNextActionIcon(patientAppointmentHistoryResponseModel.nextAction), - iconColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), + icon: AppointmentType.getNextActionIcon(widget.patientAppointmentHistoryResponseModel.nextAction), + iconColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction), iconSize: 14.h, - ).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading), + ).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading), ), SizedBox(width: 8.w), - ((((patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) || - (patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) || - !Utils.isClinicAllowedForRebook(patientAppointmentHistoryResponseModel.clinicID ?? 0))) && - AppointmentType.isArrived(patientAppointmentHistoryResponseModel)) + ((((widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) || + (widget.patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) || + !Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID ?? 0))) && + AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) ? SizedBox.shrink() : Expanded( flex: 2, @@ -145,11 +232,11 @@ class MedicalFileAppointmentCard extends StatelessWidget { ), ), ), - ).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading).onPress(() { + ).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading).onPress(() { Navigator.of(context) .push( CustomPageRoute( - page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), ), ) .then((val) { @@ -168,22 +255,22 @@ class MedicalFileAppointmentCard extends StatelessWidget { } Widget getArrivedAppointmentButton(BuildContext context) { - if ((((patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) || - (patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) || - !Utils.isClinicAllowedForRebook(patientAppointmentHistoryResponseModel.clinicID ?? 0))) && - AppointmentType.isArrived(patientAppointmentHistoryResponseModel)) { + if ((((widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) || + (widget.patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) || + !Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID ?? 0))) && + AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) { return CustomButton( text: LocaleKeys.viewDetails.tr(context: context), onPressed: () { Navigator.of(context) .push( CustomPageRoute( - page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), ), ) .then((_) { - myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); + widget.myAppointmentsViewModel.initAppointmentsViewModel(); + widget.myAppointmentsViewModel.getPatientAppointments(true, false); }); }, backgroundColor: AppColors.secondaryLightRedColor, @@ -200,30 +287,10 @@ class MedicalFileAppointmentCard extends StatelessWidget { iconSize: 16.h, ); } else { - return - // DateTime.now().difference(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate)).inDays <= 15 - // ? CustomButton( - // text: LocaleKeys.askDoctor.tr(context: context), - // onPressed: () { - // onAskDoctorTap(); - // }, - // backgroundColor: AppColors.secondaryLightRedColor, - // borderColor: AppColors.secondaryLightRedColor, - // textColor: AppColors.primaryRedColor, - // fontSize: 14.f, - // fontWeight: FontWeight.w600, - // borderRadius: 12.r, - // padding: EdgeInsets.symmetric(horizontal: 10.w), - // height: 40.h, - // icon: AppAssets.ask_doctor_icon, - // iconColor: AppColors.primaryRedColor, - // iconSize: 16.h, - // ) - // : - CustomButton( + return CustomButton( text: LocaleKeys.rebook.tr(context: context), onPressed: () { - onRescheduleTap(); + widget.onRescheduleTap(); }, backgroundColor: AppColors.greyColor, borderColor: AppColors.greyColor, @@ -239,4 +306,141 @@ class MedicalFileAppointmentCard extends StatelessWidget { ); } } + + Future handleAppointmentNextAction(nextAction, BuildContext context) async { + switch (nextAction) { + case 0: + // No action needed - go to details + Navigator.of(context) + .push(CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + )) + .then((val) {}); + break; + case 10: + // Confirm appointment - go to details + Navigator.of(context) + .push(CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + )) + .then((val) {}); + break; + case 15: + // Pending payment - show waiting modal + // Ensure timer is running before showing modal + if (_countdownTimer == null || !_countdownTimer!.isActive) { + _startCountdownTimer(); + } + + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.notice.tr(), + child: StatefulBuilder( + builder: (context, setModalState) { + // Store the modal setState callback + _modalSetState = setModalState; + + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Message text + LocaleKeys.upcomingPaymentPending.tr(context: context).toText14( + color: AppColors.textColor, + isCenter: true, + ), + SizedBox(height: 24.h), + // Countdown Timer - DD : HH : MM : SS format with labels + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Days + _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inDays.toString().padLeft(2, '0') : '00', + LocaleKeys.days.tr(context: context), + ), + _buildTimeSeparator(), + // Hours + _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inHours.remainder(24).toString().padLeft(2, '0') : '00', + LocaleKeys.hours.tr(context: context), + ), + _buildTimeSeparator(), + // Minutes + _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inMinutes.remainder(60).toString().padLeft(2, '0') : '00', + LocaleKeys.minutes.tr(context: context), + ), + _buildTimeSeparator(), + // Seconds + _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inSeconds.remainder(60).toString().padLeft(2, '0') : '00', + LocaleKeys.seconds.tr(context: context), + ), + ], + ), + SizedBox(height: 24.h), + // Green Acknowledge button with checkmark icon + CustomButton( + text: LocaleKeys.acknowledged.tr(context: context), + onPressed: () { + _modalSetState = null; // Clear callback when closing + Navigator.of(context).pop(); + }, + backgroundColor: AppColors.successColor, + borderColor: AppColors.successColor, + textColor: Colors.white, + fontSize: 16.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + height: 50.h, + icon: AppAssets.checkmark_icon, + iconColor: Colors.white, + ), + ], + ); + }, + ), + callBackFunc: () { + _modalSetState = null; // Clear callback when closing + }, + isFullScreen: false, + isCloseButtonVisible: true, + ); + break; + case 20: + // Pay now - navigate to payment page + widget.myAppointmentsViewModel.setIsPatientAppointmentShareLoading(true); + Navigator.of(context).push( + CustomPageRoute( + page: AppointmentPaymentPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + ), + ); + break; + case 50: + // Confirm livecare - go to details + Navigator.of(context) + .push(CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + )) + .then((val) {}); + break; + case 90: + // Check-in - go to details + Navigator.of(context) + .push(CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + )) + .then((val) {}); + break; + default: + // Default - go to details + Navigator.of(context) + .push(CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + )) + .then((val) {}); + } + } } + diff --git a/lib/presentation/medical_report/medical_reports_page.dart b/lib/presentation/medical_report/medical_reports_page.dart index f1b55e02..ee287662 100644 --- a/lib/presentation/medical_report/medical_reports_page.dart +++ b/lib/presentation/medical_report/medical_reports_page.dart @@ -398,14 +398,18 @@ class _MedicalReportsPageState extends State { onConfirmTap: () async { Navigator.pop(context); LoaderBottomSheet.showLoader(); - await medicalFileViewModel.insertRequestForMedicalReport(onSuccess: (val) { + await medicalFileViewModel.insertRequestForMedicalReport(onSuccess: (val) async{ LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight(context, child: Utils.getSuccessWidget(loadingText: LocaleKeys.yourMedicalReportRequestSubmittedSuccessfully.tr(context: context)), - callBackFunc: () { + callBackFunc: () async{ medicalFileViewModel.setIsPatientMedicalReportsLoading(true); medicalFileViewModel.onMedicalReportTabChange(0); medicalFileViewModel.getPatientMedicalReportList(); + + }); + await Future.delayed(Duration(milliseconds: 2000)).then((value) { + if (mounted) Navigator.pop(context); }); }, onError: (err) { LoaderBottomSheet.hideLoader(); diff --git a/lib/presentation/my_family/widget/family_cards.dart b/lib/presentation/my_family/widget/family_cards.dart index c8553207..84406aad 100644 --- a/lib/presentation/my_family/widget/family_cards.dart +++ b/lib/presentation/my_family/widget/family_cards.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -9,11 +10,13 @@ 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/core/utils/image_compression_helper.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/habib_wallet/habib_wallet_view_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; +import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; @@ -24,6 +27,8 @@ import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/user_avatar_widget.dart'; +import 'package:hmg_patient_app_new/widgets/image_picker.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; class FamilyCards extends StatefulWidget { @@ -58,6 +63,9 @@ class FamilyCards extends StatefulWidget { class _FamilyCardsState extends State { AppState appState = getIt(); late InsuranceViewModel insuranceViewModel; + late ProfileSettingsViewModel profileSettingsViewModel; + File? _selectedImage; + bool _isUploadingImage = false; @override void initState() { @@ -67,6 +75,321 @@ class _FamilyCardsState extends State { super.initState(); } + void _pickImage() { + // Show image picker options without checking permissions first + ImageOptions.showImageOptionsNew( + context, + false, // Don't show files option, only camera and gallery + (base64String, file) async { + try { + print('=== Starting image processing ==='); + print('File path: ${file.path}'); + print('File exists: ${await file.exists()}'); + print('Original file size: ${await file.length() / 1024} KB'); + + // Compress and resize the image + print('Calling compressAndResizeImage...'); + final compressedFile = await ImageCompressionHelper.compressAndResizeImage(file); + + File finalFile; + String finalBase64; + + if (compressedFile == null) { + print('⚠️ Compression failed - using original file as fallback'); + + // Fallback: use original image if compression fails + final originalSize = await file.length(); + final maxSize = 1048576; // 1MB + + if (originalSize > maxSize) { + print('❌ Original file is too large: ${originalSize / 1024} KB'); + if (mounted) { + Utils.showToast( + LocaleKeys.imageSizeTooLarge.tr(context: context), + ); + } + return; + } + + print('✅ Using original file (${originalSize / 1024} KB)'); + finalFile = file; + var bytes = await file.readAsBytes(); + finalBase64 = base64.encode(bytes); + } else { + // Check compressed file size + final fileSize = await compressedFile.length(); + final maxSize = 1048576; // 1MB + print('✅ Compression successful: ${fileSize / 1024} KB'); + + if (fileSize > maxSize) { + print('❌ Compressed file still too large'); + if (mounted) { + Utils.showToast( + LocaleKeys.imageSizeTooLarge.tr(context: context), + ); + } + return; + } + + finalFile = compressedFile; + var bytes = await compressedFile.readAsBytes(); + finalBase64 = base64.encode(bytes); + } + + print('Converting to base64... Length: ${finalBase64.length}'); + + if (mounted) { + setState(() { + _selectedImage = finalFile; + }); + + print('📤 Starting upload...'); + // Upload the image + _uploadImage(finalBase64); + } + + print('=== Image processing complete ==='); + } catch (e, stackTrace) { + print('❌ Error in _pickImage: $e'); + print('Stack trace: $stackTrace'); + if (mounted) { + Utils.showToast( + LocaleKeys.failedToProcessImage.tr(context: context), + ); + } + } + }, + checkCameraPermission: _checkCameraPermission, + checkGalleryPermission: _checkGalleryPermission, + ); + } + + Future _checkCameraPermission() async { + try { + print('=== Checking camera permission ==='); + + // First check current status + PermissionStatus currentStatus = await Permission.camera.status; + print('Current camera permission status: $currentStatus'); + + // If already granted, return true + if (currentStatus.isGranted) { + print('✅ Camera permission already granted'); + return true; + } + + // If denied or permanently denied, show settings dialog + if (currentStatus.isDenied || currentStatus.isPermanentlyDenied) { + // Request permission first + PermissionStatus newStatus = await Permission.camera.request(); + print('Camera permission after request: $newStatus'); + + if (newStatus.isGranted) { + print('✅ Camera permission granted'); + return true; + } + + // Still denied - show settings dialog + print('⚠️ Camera permission denied - showing settings dialog'); + if (mounted) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.cameraPermissionMessage.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + return false; + } + + // Request permission for the first time + PermissionStatus newStatus = await Permission.camera.request(); + print('Camera permission after request: $newStatus'); + + if (newStatus.isGranted) { + print('✅ Camera permission granted'); + return true; + } + + // Denied - show settings dialog + print('❌ Camera permission denied - showing settings dialog'); + if (mounted) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.cameraPermissionMessage.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + return false; + } catch (e) { + print('❌ Error checking camera permission: $e'); + if (mounted) { + Utils.showToast( + LocaleKeys.failedToCheckPermissions.tr(context: context), + ); + } + return false; + } + } + + Future _checkGalleryPermission() async { + try { + print('=== Checking gallery permission ==='); + + // Determine which permission to check based on platform and Android version + Permission galleryPermission; + + if (Platform.isIOS) { + galleryPermission = Permission.photos; + } else { + // Android: use photos permission which handles API level differences automatically + galleryPermission = Permission.photos; + } + + // First check current status + PermissionStatus currentStatus = await galleryPermission.status; + print('Current gallery permission status: $currentStatus'); + + // If already granted, return true + if (currentStatus.isGranted || currentStatus.isLimited) { + print('✅ Gallery permission already granted'); + return true; + } + + // If denied or permanently denied, request permission first + if (currentStatus.isDenied || currentStatus.isPermanentlyDenied) { + // Request permission first + PermissionStatus newStatus = await galleryPermission.request(); + print('Gallery permission after request: $newStatus'); + + if (newStatus.isGranted || newStatus.isLimited) { + print('✅ Gallery permission granted'); + return true; + } + + // Still denied - show settings dialog + print('⚠️ Gallery permission denied - showing settings dialog'); + if (mounted) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.galleryPermissionMessage.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + return false; + } + + // Request permission for the first time + PermissionStatus newStatus = await galleryPermission.request(); + print('Gallery permission after request: $newStatus'); + + if (newStatus.isGranted || newStatus.isLimited) { + print('✅ Gallery permission granted'); + return true; + } + + // Denied - show settings dialog + print('❌ Gallery permission denied - showing settings dialog'); + if (mounted) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.galleryPermissionMessage.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + return false; + } catch (e) { + print('❌ Error checking gallery permission: $e'); + if (mounted) { + Utils.showToast( + LocaleKeys.failedToCheckPermissions.tr(context: context), + ); + } + return false; + } + } + + void _uploadImage(String base64String) { + final patientID = appState.getAuthenticatedUser()?.patientId; + + if (patientID != null) { + setState(() { + _isUploadingImage = true; + }); + + profileSettingsViewModel.uploadProfileImage( + patientID: patientID, + imageData: base64String, + onSuccess: (data) { + if (mounted) { + setState(() { + _selectedImage = null; // Clear selected image after successful upload + _isUploadingImage = false; + }); + Utils.showToast( + LocaleKeys.profileImageUpdatedSuccessfully.tr(context: context), + ); + } + }, + onError: (error) { + if (mounted) { + setState(() { + _isUploadingImage = false; + }); + } + Utils.showToast(error); + }, + ); + } + } + double _calculateAspectRatio(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; @@ -96,6 +419,7 @@ class _FamilyCardsState extends State { widget.profileViewList!.addAll(widget.profiles); widget.profileViewList!.removeWhere((element) => element.responseId == appState.getAuthenticatedUser()?.patientId); insuranceViewModel = Provider.of(context, listen: false); + profileSettingsViewModel = Provider.of(context, listen: false); DialogService dialogService = getIt.get(); if (widget.isRequestDesign) { return Column( @@ -192,17 +516,61 @@ class _FamilyCardsState extends State { decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r), child: Padding( padding: EdgeInsets.all(16.w), - child: Column( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - UserAvatarWidget( - width: 56.w, - height: 56.h, - fit: BoxFit.cover, - isCircular: true, + Stack( + children: [ + UserAvatarWidget( + width: 56.w, + height: 56.h, + fit: BoxFit.cover, + isCircular: true, + ), + // Camera/Edit button + Positioned( + right: 0, + bottom: 0, + child: GestureDetector( + onTap: () { + if (!_isUploadingImage) { + _pickImage(); + } + }, + child: Container( + width: 20.w, + height: 20.h, + decoration: BoxDecoration( + color: AppColors.primaryRedColor, + shape: BoxShape.circle, + border: Border.all( + color: AppColors.whiteColor, + width: 1.5.w, + ), + ), + child: _isUploadingImage + ? SizedBox( + width: 10.w, + height: 10.h, + child: CircularProgressIndicator( + strokeWidth: 1.5.w, + valueColor: AlwaysStoppedAnimation( + AppColors.whiteColor, + ), + ).paddingAll(4.w), + ) + : Icon( + Icons.camera_alt, + color: AppColors.whiteColor, + size: 10.w, + ), + ), + ), + ), + ], ), SizedBox(width: 8.w), Column( @@ -311,10 +679,10 @@ class _FamilyCardsState extends State { } }, backgroundColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor.withOpacity(0.1) + ? AppColors.primaryRedColor.withValues(alpha: 0.1) : insuranceVM.isInsuranceActive - ? AppColors.successColor.withOpacity(0.1) - : AppColors.warningColorYellow.withOpacity(0.1), + ? AppColors.successColor.withValues(alpha: 0.1) + : AppColors.warningColorYellow.withValues(alpha: 0.1), labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: insuranceVM.isInsuranceActive ? 6.w : 0.w), ).toShimmer2(isShow: insuranceVM.isInsuranceLoading); }), @@ -360,8 +728,9 @@ class _FamilyCardsState extends State { gender: profile.gender, age: profile.age, fit: BoxFit.cover, - customProfileImageData: "", // Prevent showing logged-in user's image for family members + customProfileImageData: profile.patientImageData, isCircular: true, + isFamilyMember: true, // Prevent AppState fallback ), SizedBox(height: 8.h), (profile.patientName ?? "Unknown").toText14(isBold: true, isCenter: true, maxlines: 1), diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index 377014ac..750ea8c4 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -422,7 +422,7 @@ class ProfileSettingsState extends State { } } -class FamilyCardWidget extends StatelessWidget { +class FamilyCardWidget extends StatefulWidget { final Function() onAddFamilyMemberPress; final Function(FamilyFileResponseModelLists member) onFamilySwitchPress; final FamilyFileResponseModelLists profile; @@ -434,12 +434,62 @@ class FamilyCardWidget extends StatelessWidget { required this.onFamilySwitchPress(FamilyFileResponseModelLists member), }); + @override + State createState() => _FamilyCardWidgetState(); +} + +class _FamilyCardWidgetState extends State { + @override + void initState() { + super.initState(); + // Sync ProfileSettingsViewModel with AppState when widget is created + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _syncProfileImageFromAppState(); + } + }); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Also sync when dependencies change (e.g., after user switch) + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _syncProfileImageFromAppState(); + } + }); + } + + void _syncProfileImageFromAppState() { + try { + final profileVm = context.read(); + final appState = getIt.get(); + + final appStateImageData = appState.getProfileImageData; + + // Sync profile image from AppState if it's different + if (appStateImageData != null && appStateImageData.isNotEmpty) { + if (profileVm.profileImageData != appStateImageData) { + print("🔄 Syncing profile image from AppState to ProfileSettingsViewModel"); + profileVm.syncProfileImageFromAppState(); + } + } else if (profileVm.profileImageData != null) { + // AppState is null but ViewModel has data - clear it + print("🧹 Clearing ProfileSettingsViewModel image data (AppState is null)"); + profileVm.clearProfileImageCache(); + } + } catch (e) { + print("⚠️ Error syncing profile image: $e"); + } + } + @override Widget build(BuildContext context) { AppState appState = getIt.get(); - final isActive = (profile.responseId == appState.getAuthenticatedUser()?.patientId); + final isActive = (widget.profile.responseId == appState.getAuthenticatedUser()?.patientId); // final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false; - // final canSwitch = isParentUser || (!isParentUser && profile.responseId == appState.getSuperUserID); + // final canSwitch = isParentUser || (!isParentUser && widget.profile.responseId == appState.getSuperUserID); return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, @@ -453,19 +503,44 @@ class FamilyCardWidget extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - UserAvatarWidget( - width: 56.w, - height: 56.h, - gender: profile.gender, - age: profile.age, - fit: BoxFit.cover, - isCircular: true, + // Wrap in Consumer to rebuild when ProfileSettingsViewModel changes + Consumer( + builder: (context, profileVm, _) { + final currentUserId = appState.getAuthenticatedUser()?.patientId; + final isCurrentUser = widget.profile.responseId == currentUserId; + + if (isCurrentUser) { + // Current user - use AppState directly (like home screen profile icon) + return UserAvatarWidget( + width: 56.w, + height: 56.h, + gender: widget.profile.gender, + age: widget.profile.age, + fit: BoxFit.cover, + // Don't pass customProfileImageData - let it use AppState + isCircular: true, + // Don't pass isFamilyMember - allow AppState fallback + ); + } else { + // Family member - use their specific image data + return UserAvatarWidget( + width: 56.w, + height: 56.h, + gender: widget.profile.gender, + age: widget.profile.age, + fit: BoxFit.cover, + customProfileImageData: widget.profile.patientImageData, + isCircular: true, + isFamilyMember: true, // Prevent AppState fallback + ); + } + }, ), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - "${profile.patientName}".toText18(isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 1, isEnglishOnly: true), + "${widget.profile.patientName}".toText18(isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 1, isEnglishOnly: true), Wrap( direction: Axis.horizontal, spacing: 4.w, @@ -474,7 +549,7 @@ class FamilyCardWidget extends StatelessWidget { AppCustomChipWidget( labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w), icon: AppAssets.file_icon, - labelText: "${LocaleKeys.fileno.tr(context: context)}: ${profile.responseId}", + labelText: "${LocaleKeys.fileno.tr(context: context)}: ${widget.profile.responseId}", iconSize: 12.w, ), isActive ? AppCustomChipWidget( @@ -488,7 +563,7 @@ class FamilyCardWidget extends StatelessWidget { ], ).expanded, // Icon(Icons.qr_code, size: 56.h) - Image.network("https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${profile.responseId.toString()}", fit: BoxFit.contain, height: 56.h, width: 56.w) + Image.network("https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${widget.profile.responseId.toString()}", fit: BoxFit.contain, height: 56.h, width: 56.w) ], ), SizedBox(height: 4.h), @@ -499,7 +574,7 @@ class FamilyCardWidget extends StatelessWidget { runSpacing: 4.h, children: [ AppCustomChipWidget( - labelText: LocaleKeys.ageYearsOld.tr(namedArgs: {'age': profile.age.toString(), 'yearsOld': LocaleKeys.yearsOld.tr(context: context)}), + labelText: LocaleKeys.ageYearsOld.tr(namedArgs: {'age': widget.profile.age.toString(), 'yearsOld': LocaleKeys.yearsOld.tr(context: context)}), ), // isActive && appState.getAuthenticatedUser()!.bloodGroup != null // ? @@ -598,12 +673,12 @@ class FamilyCardWidget extends StatelessWidget { } Widget _buildParentUserButton(int? currentUserId) { - final canSwitch = profile.responseId != currentUserId; + final canSwitch = widget.profile.responseId != currentUserId; return CustomButton( icon: canSwitch ? AppAssets.switch_user : AppAssets.add_family, text: canSwitch ? LocaleKeys.switchAccount.tr() : LocaleKeys.addANewFamilyMember.tr(), - onPressed: canSwitch ? () => onFamilySwitchPress(profile) : onAddFamilyMemberPress, + onPressed: canSwitch ? () => widget.onFamilySwitchPress(widget.profile) : widget.onAddFamilyMemberPress, backgroundColor: canSwitch ? AppColors.secondaryLightRedColor : AppColors.primaryRedColor, borderColor: canSwitch ? AppColors.secondaryLightRedColor : AppColors.primaryRedColor, textColor: canSwitch ? AppColors.primaryRedColor : Colors.white, @@ -614,7 +689,7 @@ class FamilyCardWidget extends StatelessWidget { } Widget _buildNonParentUserButton(int? superUserId) { - final canSwitchBack = superUserId != null && superUserId == profile.responseId; + final canSwitchBack = superUserId != null && superUserId == widget.profile.responseId; return CustomButton( icon: AppAssets.switch_user, @@ -623,7 +698,7 @@ class FamilyCardWidget extends StatelessWidget { borderColor: canSwitchBack ? AppColors.primaryRedColor : Colors.grey.shade200, textColor: canSwitchBack ? AppColors.whiteColor : AppColors.greyTextColor, iconColor: canSwitchBack ? AppColors.whiteColor : AppColors.greyTextColor, - onPressed: canSwitchBack ? () => onFamilySwitchPress(profile) : () {}, + onPressed: canSwitchBack ? () => widget.onFamilySwitchPress(widget.profile) : () {}, height: isFoldable ? 50.h : 40.h, fontSize: 14.f, ).paddingOnly(top: 12.h, right: 16.w, left: 16.w, bottom: 16.h); diff --git a/lib/presentation/profile_settings/widgets/family_card_widget.dart b/lib/presentation/profile_settings/widgets/family_card_widget.dart index 768fe538..fbd1d58b 100644 --- a/lib/presentation/profile_settings/widgets/family_card_widget.dart +++ b/lib/presentation/profile_settings/widgets/family_card_widget.dart @@ -56,8 +56,9 @@ class FamilyCardWidget extends StatelessWidget { gender: profile.gender, age: profile.age, fit: BoxFit.cover, - customProfileImageData: "", - isCircular: true,// Prevent showing logged-in user's image for family members + customProfileImageData: profile.patientImageData, + isCircular: true, + isFamilyMember: true, // Prevent AppState fallback ), Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart index 1b63fef3..b057591a 100644 --- a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart +++ b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart @@ -28,34 +28,100 @@ class ProfilePictureWidget extends StatefulWidget { class _ProfilePictureWidgetState extends State { final AppState _appState = getIt.get(); File? _selectedImage; + int? _currentPatientId; @override void initState() { super.initState(); + _currentPatientId = _appState.getAuthenticatedUser()?.patientId; + print('🎬 ProfilePictureWidget initState - patient: $_currentPatientId'); + // Use addPostFrameCallback to ensure widget is built before loading WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; final profileVm = context.read(); + final patientID = _appState.getAuthenticatedUser()?.patientId; - // Only sync if AppState has data and ViewModel doesn't - if (_appState.getProfileImageData != null && - _appState.getProfileImageData!.isNotEmpty && - (profileVm.profileImageData == null || profileVm.profileImageData!.isEmpty)) { - profileVm.syncProfileImageFromAppState(); + if (patientID == null) { + print('⚠️ No authenticated user found'); + return; } - // Only load if no data exists in both AppState and ViewModel - if ((_appState.getProfileImageData == null || _appState.getProfileImageData!.isEmpty) && - (profileVm.profileImageData == null || profileVm.profileImageData!.isEmpty)) { - _loadProfileImage(); + // Check if we have data in AppState that matches current user + final appStateImageData = _appState.getProfileImageData; + + if (appStateImageData != null && appStateImageData.isNotEmpty) { + // Sync to ViewModel if it doesn't have data + if (profileVm.profileImageData == null || profileVm.profileImageData!.isEmpty) { + print('🔄 Syncing AppState data to ViewModel'); + profileVm.syncProfileImageFromAppState(); + } + } else { + // No cached data - load from API + print('📥 No cached data - loading from API for patient: $patientID'); + _loadProfileImage(forceRefresh: false); } }); } - void _loadProfileImage() { - // Check if profile image is already loaded in AppState - if (_appState.getProfileImageData != null && _appState.getProfileImageData!.isNotEmpty) { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _checkAndUpdateUserImage(); + } + + @override + void didUpdateWidget(ProfilePictureWidget oldWidget) { + super.didUpdateWidget(oldWidget); + _checkAndUpdateUserImage(); + } + + void _checkAndUpdateUserImage() { + // Check if the authenticated user has changed (family member switch) + final currentPatientId = _appState.getAuthenticatedUser()?.patientId; + + if (currentPatientId != null && currentPatientId != _currentPatientId) { + print('🔄 User switched detected: $_currentPatientId -> $currentPatientId'); + + // Update patient ID IMMEDIATELY before any other operations + final oldPatientId = _currentPatientId; + _currentPatientId = currentPatientId; + + // Clear the old profile image data + try { + final profileVm = context.read(); + + print('🧹 Clearing cache for old user: $oldPatientId'); + profileVm.clearProfileImageCache(); + + print('📥 Loading profile image for new user: $currentPatientId'); + // Load the new user's profile image immediately + profileVm.getProfileImage( + patientID: currentPatientId, + forceRefresh: true, + onSuccess: (data) { + print('✅ Profile image loaded successfully for user: $currentPatientId'); + if (mounted) { + setState(() {}); // Force rebuild to show new data + } + }, + onError: (error) { + print('❌ Error loading profile image: $error'); + if (mounted) { + setState(() {}); // Force rebuild to show default avatar + } + }, + ); + } catch (e) { + print('❌ Error in _checkAndUpdateUserImage: $e'); + } + } + } + + void _loadProfileImage({bool forceRefresh = false}) { + // Check if profile image is already loaded in AppState (skip if forcing refresh) + if (!forceRefresh && _appState.getProfileImageData != null && _appState.getProfileImageData!.isNotEmpty) { // Image already loaded, no need to call API return; } @@ -64,12 +130,16 @@ class _ProfilePictureWidgetState extends State { final patientID = _appState.getAuthenticatedUser()?.patientId; if (patientID != null) { + print('📥 Loading profile image for patient: $patientID (forceRefresh: $forceRefresh)'); profileVm.getProfileImage( patientID: patientID, + forceRefresh: forceRefresh, onSuccess: (data) { + print('✅ Profile image loaded successfully'); // Image loaded successfully }, onError: (error) { + print('❌ Error loading profile image: $error'); // Error loading image }, ); @@ -383,8 +453,10 @@ class _ProfilePictureWidgetState extends State { } Widget _buildProfileImage(ProfileSettingsViewModel profileVm) { - final gender = _appState.getAuthenticatedUser()?.gender ?? 1; - final age = _appState.getAuthenticatedUser()?.age ?? 0; + // Always get fresh user data + final currentUser = _appState.getAuthenticatedUser(); + final gender = currentUser?.gender ?? 1; + final age = currentUser?.age ?? 0; // Determine the default image based on gender and age final String defaultImage; @@ -396,7 +468,7 @@ class _ProfilePictureWidgetState extends State { defaultImage = age < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg; } - // Show selected image if available + // Show selected image if available (only during upload) if (_selectedImage != null) { return ClipOval( child: Image.file( @@ -408,9 +480,12 @@ class _ProfilePictureWidgetState extends State { ); } - // Check both ViewModel and AppState for profile image + // Use ViewModel data if available, otherwise fall back to AppState + // This ensures we show the current logged-in user's image (same as homepage profile icon) final String? imageData = profileVm.profileImageData ?? _appState.getProfileImageData; + print('🖼️ Building profile image - has data: ${imageData != null && imageData.isNotEmpty}, patient: ${currentUser?.patientId}'); + // Show uploaded image if available if (imageData != null && imageData.isNotEmpty) { try { @@ -424,7 +499,7 @@ class _ProfilePictureWidgetState extends State { ), ); } catch (e) { - print('Error decoding profile image: $e'); + print('❌ Error decoding profile image: $e'); // If decoding fails, show default image return Image.asset( defaultImage, @@ -434,7 +509,8 @@ class _ProfilePictureWidgetState extends State { } } - // Show default image + // Show default image (no image data or user has no uploaded image) + print('📷 Showing default avatar for user ${currentUser?.patientId}'); return Image.asset( defaultImage, width: 136.w, @@ -444,6 +520,13 @@ class _ProfilePictureWidgetState extends State { @override Widget build(BuildContext context) { + // Check for user change on every build (handles navigation scenarios) + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _checkAndUpdateUserImage(); + } + }); + return Consumer( builder: (context, profileVm, _) { return Center( diff --git a/lib/presentation/radiology/radiology_orders_page.dart b/lib/presentation/radiology/radiology_orders_page.dart index 80f6aed5..e8c1cedc 100644 --- a/lib/presentation/radiology/radiology_orders_page.dart +++ b/lib/presentation/radiology/radiology_orders_page.dart @@ -21,6 +21,7 @@ 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/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../../features/radiology/radiology_view_model.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -439,12 +440,25 @@ class _RadiologyOrdersPageState extends State { SizedBox(height: 16.h), CustomButton( text: LocaleKeys.viewReport.tr(), - onPressed: () { - model.navigationService.push( - CustomPageRoute( - page: RadiologyResultPage(patientRadiologyResponseModel: group), - ), - ); + onPressed: () async { + // Check if reportData is empty or null + if (group.reportData == null || group.reportData!.trim().isEmpty) { + // Open image directly + await model.getRadiologyImage(patientRadiologyResponseModel: group); + if (model.radiologyImageURL.isNotEmpty) { + Uri uri = Uri.parse(model.radiologyImageURL); + await launchUrl(uri, mode: LaunchMode.platformDefault, webOnlyWindowName: ""); + } else { + Utils.showToast(LocaleKeys.noDataAvailable.tr()); + } + } else { + // Navigate to details page + model.navigationService.push( + CustomPageRoute( + page: RadiologyResultPage(patientRadiologyResponseModel: group), + ), + ); + } }, backgroundColor: AppColors.infoColor.withAlpha(20), borderColor: AppColors.infoColor.withAlpha(0), diff --git a/lib/presentation/symptoms_checker/risk_factors_screen.dart b/lib/presentation/symptoms_checker/risk_factors_screen.dart index 969a884d..49aa5150 100644 --- a/lib/presentation/symptoms_checker/risk_factors_screen.dart +++ b/lib/presentation/symptoms_checker/risk_factors_screen.dart @@ -125,7 +125,7 @@ class _RiskFactorsScreenState extends State { fontSize: 13.f, fontWeight: FontWeight.w600, color: AppColors.greyInfoTextColor, - fontFamily: isArabic ? 'GESSTwo' : 'Poppins' + fontFamily: isArabic ? 'CairoArabic' : 'Poppins' ), children: [ TextSpan( diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index 76fad545..c9291275 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -468,14 +468,14 @@ class _TriagePageState extends State { text: TextSpan( text: LocaleKeys.possibleSymptom.tr(context: context), style: TextStyle( - color: AppColors.greyTextColor, fontWeight: FontWeight.w600, fontSize: 14.f, fontFamily: isArabic ? 'GESSTwo' : 'Poppins'), + color: AppColors.greyTextColor, fontWeight: FontWeight.w600, fontSize: 14.f, fontFamily: isArabic ? 'CairoArabic' : 'Poppins'), children: [ TextSpan( text: suggestedCondition, style: TextStyle( color: AppColors.textColor, fontWeight: FontWeight.w600, - fontSize: 14.f, fontFamily: isArabic ? 'GESSTwo' : 'Poppins'), + fontSize: 14.f, fontFamily: isArabic ? 'CairoArabic' : 'Poppins'), ), ], ), @@ -491,7 +491,7 @@ class _TriagePageState extends State { text: TextSpan( text: "${probability.toStringAsFixed(1)}% ", style: TextStyle( - fontFamily: 'Poppins', + fontFamily: isArabic ? 'CairoArabic' : 'Poppins', color: AppColors.primaryRedColor, fontWeight: FontWeight.w600, fontSize: 14.f, @@ -503,7 +503,7 @@ class _TriagePageState extends State { color: AppColors.textColor, fontWeight: FontWeight.w600, fontSize: 13.f, - fontFamily: isArabic ? 'GESSTwo' : 'Poppins', + fontFamily: isArabic ? 'CairoArabic' : 'Poppins', ), ), ], diff --git a/lib/presentation/vital_sign/vital_sign_details_page.dart b/lib/presentation/vital_sign/vital_sign_details_page.dart index eb137e47..63b21291 100644 --- a/lib/presentation/vital_sign/vital_sign_details_page.dart +++ b/lib/presentation/vital_sign/vital_sign_details_page.dart @@ -196,7 +196,7 @@ class _VitalSignDetailsPageState extends State { style: TextStyle( fontSize: 12.f, fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', color: AppColors.greyTextColor, ), softWrap: true, @@ -335,7 +335,6 @@ class _VitalSignDetailsPageState extends State { Widget _buildHistoryGraph(List history, {List? secondaryHistory}) { final minY = _minY(history, secondaryHistory: secondaryHistory); final maxY = _maxY(history, secondaryHistory: secondaryHistory); - final scheme = VitalSignUiModel.scheme(status: _statusForLatest(null), label: args.title); final isRTL = getIt.get().isArabic(); return CustomGraph( @@ -344,44 +343,43 @@ class _VitalSignDetailsPageState extends State { makeGraphBasedOnActualValue: true, leftLabelReservedSize: 40, showGridLines: true, - showShadow: true, + showShadow: false, isRTL: isRTL, leftLabelInterval: _leftInterval(history, secondaryHistory: secondaryHistory), maxY: maxY, minY: minY, maxX: history.length.toDouble() - .75, - horizontalInterval: _leftInterval(history, secondaryHistory: secondaryHistory), + horizontalInterval: .01, leftLabelFormatter: (value) { - // Show only numeric labels at regular intervals - return _axisLabel(value.toStringAsFixed(0)); + // Format value to 2 decimal places for consistency + value = double.parse(value.toStringAsFixed(2)); + return _axisLabel(value.toStringAsFixed(2)); }, getDrawingHorizontalLine: (value) { - // Draw reference lines for high/low bounds + value = double.parse(value.toStringAsFixed(2)); + // Draw solid reference lines for high/low bounds (matching lab result style) if (args.high != null && (value - args.high!).abs() < 0.1) { return FlLine( - color: AppColors.bgGreenColor.withOpacity(0.2), + color: AppColors.bgGreenColor.withValues(alpha: 0.6), strokeWidth: 1, - dashArray: [5, 5], ); } if (args.low != null && (value - args.low!).abs() < 0.1) { return FlLine( - color: AppColors.bgGreenColor.withOpacity(0.2), + color: AppColors.bgGreenColor.withValues(alpha: 0.6), strokeWidth: 1, - dashArray: [5, 5], ); } - // Draw grid lines at intervals + // Hide other grid lines return FlLine( - color: AppColors.bgGreenColor.withOpacity(0.2), + color: Colors.transparent, strokeWidth: 1, - dashArray: [5, 5], ); }, - graphColor: AppColors.bgGreenColor, - secondaryGraphColor: AppColors.blueColor, - graphShadowColor: AppColors.lightGreenColor.withOpacity(.4), - graphGridColor: scheme.iconFg, + graphColor: AppColors.textColor.withValues(alpha: 90 / 255), + secondaryGraphColor: AppColors.textColor.withValues(alpha: 90 / 255), + graphShadowColor: AppColors.greyColor.withValues(alpha: 0.5), + graphGridColor: AppColors.textColor.withValues(alpha: 0.4), bottomLabelFormatter: (value, data) { if (data.isEmpty) return const SizedBox.shrink(); if (value == 0) return _bottomLabel(data[value.toInt()].label); @@ -497,32 +495,35 @@ class _VitalSignDetailsPageState extends State { final List ranges = []; + // Below low reference (transparent - matching lab result style) if (args.low != null) { ranges.add( HorizontalRangeAnnotation( y1: minY, y2: args.low!, - color: AppColors.highAndLow.withOpacity(0.05), + color: Colors.transparent, ), ); } + // Normal range (light green shading - matching lab result style) if (args.low != null && args.high != null) { ranges.add( HorizontalRangeAnnotation( y1: args.low!, y2: args.high!, - color: AppColors.bgGreenColor.withOpacity(0.05), + color: AppColors.bgGreenColor.withValues(alpha: 0.05), ), ); } + // Above high reference (transparent - matching lab result style) if (args.high != null) { ranges.add( HorizontalRangeAnnotation( y1: args.high!, y2: maxY, - color: AppColors.criticalLowAndHigh.withOpacity(0.05), + color: Colors.transparent, ), ); } @@ -755,7 +756,7 @@ class _VitalSignDetailsPageState extends State { value, style: TextStyle( fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', fontSize: 8.f, color: AppColors.textColor, ), diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index a134ac79..d8a74d17 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -301,9 +301,11 @@ class TextInputWidget extends StatelessWidget { return Directionality( textDirection: isArabic ? TextDirection.rtl : TextDirection.ltr, child: Localizations.override( + context: context, locale: const Locale('en', 'US'), // Force English locale for TextField child: TextField( + hintLocales: const [Locale('en', 'US')], enabled: isEnable, scrollPadding: EdgeInsets.zero, @@ -344,6 +346,7 @@ class TextInputWidget extends StatelessWidget { isDense: true, hintText: hintText, hintStyle: TextStyle( + fontFamily: isArabic ? 'CairoArabic' : 'Poppins', fontSize: 14.f, height: 21 / 16, @@ -353,7 +356,7 @@ class TextInputWidget extends StatelessWidget { ), prefixIconConstraints: BoxConstraints(minWidth: 30.h), prefixIcon: prefix == null ? null : "+${prefix!}".toText14(letterSpacing: -1, color: AppColors.textColor, isBold: true), - contentPadding: EdgeInsets.zero, + contentPadding: EdgeInsets.only(right:isArabic ? 10.w :0), border: InputBorder.none, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, diff --git a/lib/widgets/user_avatar_widget.dart b/lib/widgets/user_avatar_widget.dart index 6132401c..3271adf4 100644 --- a/lib/widgets/user_avatar_widget.dart +++ b/lib/widgets/user_avatar_widget.dart @@ -14,6 +14,7 @@ class UserAvatarWidget extends StatelessWidget { final bool isCircular; final double? borderRadius; final String? customProfileImageData; // For family members or other users + final bool isFamilyMember; // New flag to prevent AppState fallback const UserAvatarWidget({ Key? key, @@ -25,6 +26,7 @@ class UserAvatarWidget extends StatelessWidget { this.isCircular = false, this.borderRadius, this.customProfileImageData, + this.isFamilyMember = false, // Default to false for backward compatibility }) : super(key: key); @override @@ -33,9 +35,17 @@ class UserAvatarWidget extends StatelessWidget { final userGender = gender ?? appState.getAuthenticatedUser()?.gender ?? 1; final userAge = age ?? appState.getAuthenticatedUser()?.age ?? 0; - // Use custom profile image data if provided, otherwise use AppState data - final profileImageData = customProfileImageData ?? appState.getProfileImageData; - + // Determine which profile image data to use: + // - For family members: ONLY use customProfileImageData (no AppState fallback) + // - For main user: Use customProfileImageData if provided, otherwise AppState + final String? profileImageData; + if (isFamilyMember) { + // Family member mode: only use their specific image data, no fallback + profileImageData = customProfileImageData; + } else { + // Main user mode: use custom data or fall back to AppState + profileImageData = customProfileImageData ?? appState.getProfileImageData; + } // Determine the default image based on gender and age final String defaultImage;