Merge branch 'master' into dev_aamir

# Conflicts:
#	lib/features/medical_file/medical_file_view_model.dart
#	lib/presentation/appointments/appointment_details_page.dart
dev_aamir
aamir-csol 3 hours ago
commit f0b8db0318

@ -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

@ -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<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins', fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal),
height: getIt.get<AppState>().getLanguageCode() == "ar" ? 1.2 : 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.f, letterSpacing: -1, fontFamily: getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins', fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal,),
);
Widget toText44({Color? color, bool isBold = false, bool isEnglishOnly = false,}) => Text(

@ -457,6 +457,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
@ -500,6 +501,7 @@ class MedicalFileViewModel extends ChangeNotifier {
genderDescription: element.genderDescription,
genderImage: element.genderImage,
emaiLAddress: element.emaiLAddress,
patientImageData: element.patientImageData, // Include profile image data
);
if (isPending) {
@ -776,5 +778,40 @@ 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");
}
}
}

@ -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<String, dynamic> toJson() => {
@ -108,5 +111,6 @@ class FamilyFileResponseModelLists {
"StatusDescription": statusDescription,
"isSuperUser": isSuperUser,
"isRequestFromMySide": isRequestFromMySide,
"PatientImageData": patientImageData,
};
}

@ -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<AppState>().setProfileImageData = profileImageData;
} else {
profileImageData = null;
GetIt.instance<AppState>().setProfileImageData = null;
@ -286,6 +288,16 @@ class ProfileSettingsViewModel extends ChangeNotifier {
profileImageData = imageData;
// Store in AppState for global access
GetIt.instance<AppState>().setProfileImageData = imageData;
// Update the family files cache with the new profile image
try {
final medicalFileViewModel = GetIt.instance.get<MedicalFileViewModel>();
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);
},

@ -1210,7 +1210,8 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
handleAppointmentNextAction(widget.patientAppointmentHistoryResponseModel.nextAction);
},
backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction),
borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.01),
borderColor:
AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01),
textColor: widget.patientAppointmentHistoryResponseModel.nextAction == 15 ? AppColors.textColor : Colors.white,
fontSize: 16.f,
fontWeight: FontWeight.w600,

@ -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<AppointmentCard> createState() => _AppointmentCardState();
}
class _AppointmentCardState extends State<AppointmentCard> {
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<AppState>();
@ -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<void> 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);
}
}
}

@ -150,7 +150,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();

@ -71,7 +71,7 @@ class LoginScreenState extends State<LoginScreen> {
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(

@ -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<AppState>().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<AppState>().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<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
fontWeight: FontWeight.w600,
fontSize: 16.f,
color: AppColors.textColor,

@ -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),

@ -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<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
color: tests!.checkIfGraphShouldBeDisplayed()
? context.read<LabViewModel>().getColor(
tests?.calculatedResultFlag ?? "",
@ -77,7 +79,7 @@ class LabOrderResultItem extends StatelessWidget {
style: TextStyle(
fontSize: 12.f,
fontWeight: FontWeight.w600,
fontFamily: 'Poppins',
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
color: AppColors.greyTextColor,
),
softWrap: true,

@ -286,7 +286,7 @@ class _LabResultDetailsState extends State<LabResultDetails> {
style: TextStyle(
fontSize: 12.f,
fontWeight: FontWeight.w600,
fontFamily: 'Poppins',
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
color: AppColors.greyTextColor,
),
softWrap: true,
@ -364,7 +364,7 @@ class _LabResultDetailsState extends State<LabResultDetails> {
value,
style: TextStyle(
fontWeight: FontWeight.w600,
fontFamily: 'Poppins',
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
fontSize: 8.f,
color: AppColors.textColor,
),
@ -376,7 +376,7 @@ class _LabResultDetailsState extends State<LabResultDetails> {
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<AppState>().isArabic() ? 'CairoArabic' : 'Poppins', fontWeight: FontWeight.w600, color: AppColors.labelTextColor),
),
);
}

@ -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<AppState>().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<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
color: AppColors.textColor
),
)

@ -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<MedicalFileAppointmentCard> createState() => _MedicalFileAppointmentCardState();
}
class _MedicalFileAppointmentCardState extends State<MedicalFileAppointmentCard> {
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<AppState>();
@ -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<void> 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) {});
}
}
}

@ -398,14 +398,18 @@ class _MedicalReportsPageState extends State<MedicalReportsPage> {
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();

@ -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<FamilyCards> {
AppState appState = getIt<AppState>();
late InsuranceViewModel insuranceViewModel;
late ProfileSettingsViewModel profileSettingsViewModel;
File? _selectedImage;
bool _isUploadingImage = false;
@override
void initState() {
@ -67,6 +75,321 @@ class _FamilyCardsState extends State<FamilyCards> {
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<bool> _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<bool> _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<FamilyCards> {
widget.profileViewList!.addAll(widget.profiles);
widget.profileViewList!.removeWhere((element) => element.responseId == appState.getAuthenticatedUser()?.patientId);
insuranceViewModel = Provider.of<InsuranceViewModel>(context, listen: false);
profileSettingsViewModel = Provider.of<ProfileSettingsViewModel>(context, listen: false);
DialogService dialogService = getIt.get<DialogService>();
if (widget.isRequestDesign) {
return Column(
@ -192,17 +516,61 @@ class _FamilyCardsState extends State<FamilyCards> {
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<Color>(
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<FamilyCards> {
}
},
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<FamilyCards> {
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),

@ -422,7 +422,7 @@ class ProfileSettingsState extends State<ProfileSettings> {
}
}
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<FamilyCardWidget> createState() => _FamilyCardWidgetState();
}
class _FamilyCardWidgetState extends State<FamilyCardWidget> {
@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<ProfileSettingsViewModel>();
final appState = getIt.get<AppState>();
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<AppState>();
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<ProfileSettingsViewModel>(
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);

@ -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,

@ -28,34 +28,100 @@ class ProfilePictureWidget extends StatefulWidget {
class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
final AppState _appState = getIt.get<AppState>();
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<ProfileSettingsViewModel>();
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<ProfileSettingsViewModel>();
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<ProfilePictureWidget> {
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<ProfilePictureWidget> {
}
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<ProfilePictureWidget> {
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<ProfilePictureWidget> {
);
}
// 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<ProfilePictureWidget> {
),
);
} 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<ProfilePictureWidget> {
}
}
// 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<ProfilePictureWidget> {
@override
Widget build(BuildContext context) {
// Check for user change on every build (handles navigation scenarios)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_checkAndUpdateUserImage();
}
});
return Consumer<ProfileSettingsViewModel>(
builder: (context, profileVm, _) {
return Center(

@ -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<RadiologyOrdersPage> {
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),

@ -125,7 +125,7 @@ class _RiskFactorsScreenState extends State<RiskFactorsScreen> {
fontSize: 13.f,
fontWeight: FontWeight.w600,
color: AppColors.greyInfoTextColor,
fontFamily: isArabic ? 'GESSTwo' : 'Poppins'
fontFamily: isArabic ? 'CairoArabic' : 'Poppins'
),
children: [
TextSpan(

@ -468,14 +468,14 @@ class _TriagePageState extends State<TriagePage> {
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<TriagePage> {
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<TriagePage> {
color: AppColors.textColor,
fontWeight: FontWeight.w600,
fontSize: 13.f,
fontFamily: isArabic ? 'GESSTwo' : 'Poppins',
fontFamily: isArabic ? 'CairoArabic' : 'Poppins',
),
),
],

@ -196,7 +196,7 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
style: TextStyle(
fontSize: 12.f,
fontWeight: FontWeight.w600,
fontFamily: 'Poppins',
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
color: AppColors.greyTextColor,
),
softWrap: true,
@ -335,7 +335,6 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
Widget _buildHistoryGraph(List<DataPoint> history, {List<DataPoint>? 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<AppState>().isArabic();
return CustomGraph(
@ -344,44 +343,43 @@ class _VitalSignDetailsPageState extends State<VitalSignDetailsPage> {
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<VitalSignDetailsPage> {
final List<HorizontalRangeAnnotation> 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<VitalSignDetailsPage> {
value,
style: TextStyle(
fontWeight: FontWeight.w600,
fontFamily: 'Poppins',
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
fontSize: 8.f,
color: AppColors.textColor,
),

@ -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,

@ -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;

Loading…
Cancel
Save