diff --git a/assets/images/svg/bfc.svg b/assets/images/svg/bfc.svg new file mode 100644 index 00000000..a6d2ef3a --- /dev/null +++ b/assets/images/svg/bfc.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/bmi1.svg b/assets/images/svg/bmi1.svg new file mode 100644 index 00000000..c957dc8e --- /dev/null +++ b/assets/images/svg/bmi1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/bmr1.svg b/assets/images/svg/bmr1.svg new file mode 100644 index 00000000..7e403a81 --- /dev/null +++ b/assets/images/svg/bmr1.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/cloudy_weather.svg b/assets/images/svg/cloudy_weather.svg new file mode 100644 index 00000000..bc7d54bf --- /dev/null +++ b/assets/images/svg/cloudy_weather.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/images/svg/cloudyweather.svg b/assets/images/svg/cloudyweather.svg new file mode 100644 index 00000000..6f8ebb47 --- /dev/null +++ b/assets/images/svg/cloudyweather.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/svg/ddc.svg b/assets/images/svg/ddc.svg new file mode 100644 index 00000000..f671baf8 --- /dev/null +++ b/assets/images/svg/ddc.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/evening.svg b/assets/images/svg/evening.svg new file mode 100644 index 00000000..7932d78d --- /dev/null +++ b/assets/images/svg/evening.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/images/svg/ibwc.svg b/assets/images/svg/ibwc.svg new file mode 100644 index 00000000..acfba187 --- /dev/null +++ b/assets/images/svg/ibwc.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/morning.svg b/assets/images/svg/morning.svg new file mode 100644 index 00000000..0f0d9904 --- /dev/null +++ b/assets/images/svg/morning.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/images/svg/op.svg b/assets/images/svg/op.svg new file mode 100644 index 00000000..ff400de8 --- /dev/null +++ b/assets/images/svg/op.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/images/svg/whitecalender.svg b/assets/images/svg/whitecalender.svg new file mode 100644 index 00000000..ff400de8 --- /dev/null +++ b/assets/images/svg/whitecalender.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 76481987..9dc024b6 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1186,6 +1186,14 @@ "carbsProteinFat": "الكربوهيدرات\nالبروتين والدهون", "ovulationPeriod": "فترة\nالإباضة", "deliveryDueDate": "تاريخ الولادة\nالمتوقع", + "bmiCalculatorDesc": "احسب مؤشر كتلة جسمك لفهم نطاق وزنك الصحي.", + "caloriesCalculatorDesc": "اكتشف عدد السعرات الحرارية التي تحتاجها يومياً للحفاظ على الوزن أو فقدانه.", + "bmrCalculatorDesc": "حدد معدل الأيض الأساسي ومتطلباتك اليومية من الطاقة.", + "idealBodyWeightDesc": "اكتشف وزنك المثالي بناءً على الطول والعمر وبنية الجسم.", + "bodyFatCalculatorDesc": "قدّر نسبة دهون جسمك لتتبع أهداف اللياقة والصحة.", + "carbsProteinFatDesc": "وازن بين العناصر الغذائية مع النسب الصحيحة من الكربوهيدرات والبروتين والدهون.", + "ovulationPeriodDesc": "تتبع فترة خصوبتك وخطط للحمل بدقة.", + "deliveryDueDateDesc": "احسب تاريخ ولادتك المتوقع وراقب مراحل الحمل.", "low": "منخفض", "preDiabetic": "ما قبل السكري", "high": "مرتفع", diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 163eb616..311d320c 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1169,14 +1169,22 @@ "monitorCholesterolLevels": "Monitor cholesterol levels, assess heart health risks, and take proactive steps for well-being.", "triglyceridesFatBlood": "Triglycerides Fat Blood", "understandTriglyceridesImpact": "Understand triglycerides' impact on heart health with personalized insights and expert recommendations.", - "bmiCalculator": "BMI\nCalculator", - "caloriesCalculator": "Calories\nCalculator", - "bmrCalculator": "BMR\nCalculator", - "idealBodyWeight": "Ideal Body\nWeight", - "bodyFatCalculator": "Body Fat\nCalculator", - "carbsProteinFat": "Carbs\nProtein & Fat", - "ovulationPeriod": "Ovulation\nPeriod", - "deliveryDueDate": "Delivery\nDue Date", + "bmiCalculator": "BMI Calculator", + "bmiCalculatorDesc": "Calculate your body mass index to understand your healthy weight range.", + "caloriesCalculator": "Calories Calculator", + "caloriesCalculatorDesc": "Find out how many calories you need daily to maintain or lose weight.", + "bmrCalculator": "BMR Calculator", + "bmrCalculatorDesc": "Determine your basal metabolic rate and daily energy requirements.", + "idealBodyWeight": "Ideal Body Weight", + "idealBodyWeightDesc": "Discover your ideal weight based on height, age, and body structure.", + "bodyFatCalculator": "Body Fat Calculator", + "bodyFatCalculatorDesc": "Estimate your body fat percentage to track fitness and health goals.", + "carbsProteinFat": "Carbs Protein & Fat", + "carbsProteinFatDesc": "Balance your macros with the right carbs, protein, and fat ratios.", + "ovulationPeriod": "Ovulation Period", + "ovulationPeriodDesc": "Track your fertile window and plan for conception with accuracy.", + "deliveryDueDate": "Delivery Due Date", + "deliveryDueDateDesc": "Calculate your expected delivery date and monitor pregnancy milestones.", "low": "Low", "preDiabetic": "Pre-diabetic", "high": "High", diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index ef1cd36a..819691be 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -4,7 +4,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.preProd; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 743505b9..aab9e25d 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -363,6 +363,14 @@ class AppAssets { static const String files = '$svgBasePath/files.svg'; static const String online_checkin_icon = '$svgBasePath/online_checkin_icon.svg'; + //Health Calculators + static const String bodyfatcalc = '$svgBasePath/bfc.svg'; + static const String duedatecalc = '$svgBasePath/ddc.svg'; + static const String idealbodyweightcacl = '$svgBasePath/ibwc.svg'; + static const String ovluationperiod = '$svgBasePath/op.svg'; + static const String bmicalc = '$svgBasePath/bmi1.svg'; + static const String bmrcalc = '$svgBasePath/bmr1.svg'; + // PNGS // static const String hmgLogo = '$pngBasePath/hmg_logo.png'; static const String liveCareService = '$pngBasePath/livecare_service.png'; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 3b7253db..f451f0d5 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -64,6 +64,8 @@ import 'package:hmg_patient_app_new/features/todo_section/todo_section_repo.dart import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_repo.dart'; import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/features/weather/weather_repo.dart'; +import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart'; import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_view_model.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; @@ -164,6 +166,7 @@ class AppDependencies { getIt.registerLazySingleton(() => SymptomsCheckerRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => BloodDonationRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => WaterMonitorRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => WeatherRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => MyInvoicesRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => HealthTrackersRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => MonthlyReportRepoImp(loggerService: getIt(), apiClient: getIt())); @@ -299,6 +302,8 @@ class AppDependencies { getIt.registerLazySingleton(() => WaterMonitorViewModel(waterMonitorRepo: getIt(), errorHandlerService: getIt())); + getIt.registerLazySingleton(() => WeatherMonitorViewModel(weatherRepo: getIt(), errorHandlerService: getIt(), locationUtils: getIt())); + //commenting this because its already define there was on run time error because of this. getIt.registerLazySingleton(() => MyInvoicesViewModel(myInvoicesRepo: getIt(), errorHandlerService: getIt(), navServices: getIt())); diff --git a/lib/core/utils/calender_utils_new.dart b/lib/core/utils/calender_utils_new.dart index 6f043e26..dfb400be 100644 --- a/lib/core/utils/calender_utils_new.dart +++ b/lib/core/utils/calender_utils_new.dart @@ -52,13 +52,7 @@ class CalenderUtilsNew { }); } - FutureOr createOrUpdateEvent( - {required String title, - required String description, - required String location, - DateTime? scheduleDateTime, - String? eventId, - int? reminderMinutes}) async { + FutureOr createOrUpdateEvent({required String title, required String description, required String location, DateTime? scheduleDateTime, String? eventId, int? reminderMinutes}) async { if (writableCalender.isEmpty) { await getCalenders(); } @@ -93,19 +87,19 @@ class CalenderUtilsNew { // return eventId.isNotEmpty; } - FutureOr createMultipleEvents( - {required int reminderMinutes, - int? frequencyNumber, - required int days, - required String orderDate, - required String itemDescriptionN, - required String route, - Function(String)? onFailure, - String? prescriptionNumber, - DateTime? scheduleDateTime, - }) async { + FutureOr createMultipleEvents({ + required int reminderMinutes, + int? frequencyNumber, + required int days, + required String orderDate, + required String itemDescriptionN, + required String route, + Function(String)? onFailure, + String? prescriptionNumber, + DateTime? scheduleDateTime, + }) async { DateTime currentDay = DateTime.now(); - DateTime actualDate = scheduleDateTime??DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day, 8, 0); + DateTime actualDate = scheduleDateTime ?? DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day, 8, 0); print("the frequency is $frequencyNumber"); frequencyNumber ??= 2; //Some time frequency number is null so by default will be 2 int interval = calculateIntervalAsPerFrequency(frequencyNumber); diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 5db5b289..7a71b345 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -322,8 +322,10 @@ extension EmailValidator on String { Widget toText32({bool isEnglishOnly = false, FontWeight? weight, Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, + style: TextStyle( - height: 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.f, letterSpacing: -1, fontFamily: getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins', fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal), + + height: getIt.get().getLanguageCode() == "ar" ? 1.2 : 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.f, letterSpacing: -1, fontFamily: getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins', fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal,), ); Widget toText44({Color? color, bool isBold = false, bool isEnglishOnly = false,}) => Text( diff --git a/lib/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index 05f82208..8b7ce7c3 100644 --- a/lib/features/medical_file/medical_file_view_model.dart +++ b/lib/features/medical_file/medical_file_view_model.dart @@ -41,6 +41,7 @@ class MedicalFileViewModel extends ChangeNotifier { List patientSickLeavesViewList = []; bool isSickLeavesSortByClinic = true; + // bool isSickLeavesDataNeedsReloading = true; List patientAllergiesList = []; @@ -165,13 +166,13 @@ class MedicalFileViewModel extends ChangeNotifier { setIsPatientSickLeaveListLoading(bool val) { // if (val && isSickLeavesDataNeedsReloading) { - patientSickLeaveList.clear(); - patientSickLeavesByClinic.clear(); - patientSickLeavesByHospital.clear(); - patientSickLeavesViewList.clear(); - patientSickLeavePDFBase64 = ""; - isSickLeavesSortByClinic = true; - isPatientSickLeaveListLoading = val; + patientSickLeaveList.clear(); + patientSickLeavesByClinic.clear(); + patientSickLeavesByHospital.clear(); + patientSickLeavesViewList.clear(); + patientSickLeavePDFBase64 = ""; + isSickLeavesSortByClinic = true; + isPatientSickLeaveListLoading = val; // } notifyListeners(); } @@ -456,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 @@ -499,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) { @@ -774,4 +777,41 @@ class MedicalFileViewModel extends ChangeNotifier { }, ); } + + /// Update family member's profile image in cached family files + void updateFamilyMemberProfileImage(int patientID, String imageData) { + try { + bool updated = false; + + // Update in patientFamilyFiles list + for (var i = 0; i < patientFamilyFiles.length; i++) { + if (patientFamilyFiles[i].patientId == patientID || + patientFamilyFiles[i].responseId == patientID) { + patientFamilyFiles[i].patientImageData = imageData; + updated = true; + print("✅ Updated profile image for family member in patientFamilyFiles (index: $i, patientID: $patientID)"); + break; + } + } + + // Update in pendingFamilyFiles list if exists + for (var i = 0; i < pendingFamilyFiles.length; i++) { + if (pendingFamilyFiles[i].patientId == patientID || + pendingFamilyFiles[i].responseId == patientID) { + pendingFamilyFiles[i].patientImageData = imageData; + print("✅ Updated profile image for pending family member (index: $i, patientID: $patientID)"); + break; + } + } + + if (updated) { + // Notify listeners to update UI + notifyListeners(); + } else { + print("⚠️ Family member not found in cache for patientID: $patientID"); + } + } catch (e) { + print("❌ Error updating family member profile image in cache: $e"); + } + } } diff --git a/lib/features/medical_file/models/family_file_response_model.dart b/lib/features/medical_file/models/family_file_response_model.dart index f4aa557d..59aa2bd2 100644 --- a/lib/features/medical_file/models/family_file_response_model.dart +++ b/lib/features/medical_file/models/family_file_response_model.dart @@ -25,6 +25,7 @@ class FamilyFileResponseModelLists { String? statusDescription; bool? isSuperUser = false; bool? isRequestFromMySide; + String? patientImageData; FamilyFileResponseModelLists( {this.id, @@ -50,7 +51,8 @@ class FamilyFileResponseModelLists { this.patientName, this.statusDescription, this.isSuperUser, - this.isRequestFromMySide}); + this.isRequestFromMySide, + this.patientImageData}); factory FamilyFileResponseModelLists.fromRawJson(String str) => FamilyFileResponseModelLists.fromJson(json.decode(str)); @@ -81,6 +83,7 @@ class FamilyFileResponseModelLists { statusDescription: json["StatusDescription"], isSuperUser: json["isSuperUser"] ?? false, isRequestFromMySide: json["isRequestFromMySide"] ?? false, + patientImageData: json["PatientImageData"], ); Map toJson() => { @@ -108,5 +111,6 @@ class FamilyFileResponseModelLists { "StatusDescription": statusDescription, "isSuperUser": isSuperUser, "isRequestFromMySide": isRequestFromMySide, + "PatientImageData": patientImageData, }; } diff --git a/lib/features/my_appointments/models/reminder_type.dart b/lib/features/my_appointments/models/reminder_type.dart new file mode 100644 index 00000000..f13f20a0 --- /dev/null +++ b/lib/features/my_appointments/models/reminder_type.dart @@ -0,0 +1,50 @@ +/// Enum to define different types of reminders +enum ReminderType { + /// Reminder for appointment schedule + appointment, + + /// Reminder for payment due + payment, + + /// Reminder for check-in + checkIn, +} + +extension ReminderTypeExtension on ReminderType { + /// Get display name for the reminder type + String get displayName { + switch (this) { + case ReminderType.appointment: + return 'Appointment Reminder'; + case ReminderType.payment: + return 'Payment Reminder'; + case ReminderType.checkIn: + return 'Check-In Reminder'; + } + } + + /// Get description for the reminder type + String get description { + switch (this) { + case ReminderType.appointment: + return 'Appointment scheduled reminder'; + case ReminderType.payment: + return 'Payment due reminder'; + case ReminderType.checkIn: + return 'Check-in reminder'; + } + } + + /// Get event ID prefix for calendar events + String get eventIdPrefix { + switch (this) { + case ReminderType.appointment: + return 'APPT'; + case ReminderType.payment: + return 'PAY'; + case ReminderType.checkIn: + return 'CHKIN'; + } + } +} + diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index 73f8d331..942a51e6 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -2,11 +2,13 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/utils/calender_utils_new.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_favorite_doctors_list.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/appointemnet_filters.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/reminder_type.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/appointment_rated_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ask_doctor_request_type_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart'; @@ -19,6 +21,7 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; @@ -1109,4 +1112,409 @@ class MyAppointmentsViewModel extends ChangeNotifier { }, ); } + + /// Creates a reminder for the appointment + /// Shows a bottom sheet to select reminder time and creates calendar event + /// [reminderType] specifies the type of reminder (appointment, payment, or check-in) + /// For payment and check-in reminders, shows only 1, 2, 3 hours before appointment + /// Automatically removes previous reminders when creating a new one + Future createReminder({ + required BuildContext context, + required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, + ReminderType reminderType = ReminderType.appointment, + Function(bool)? onSuccess, + Function(String)? onError, + }) async { + try { + CalenderUtilsNew calender = CalenderUtilsNew.instance; + DateTime endDate = DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate); + + // Generate title and description based on reminder type + String title; + String description; + String eventIdPrefix = reminderType.eventIdPrefix; + + switch (reminderType) { + case ReminderType.appointment: + title = "Appointment with ${patientAppointmentHistoryResponseModel.doctorNameObj}"; + description = "${patientAppointmentHistoryResponseModel.doctorNameObj} will be having an appointment on ${patientAppointmentHistoryResponseModel.appointmentDate}"; + break; + case ReminderType.payment: + title = "Payment Due for Appointment #${patientAppointmentHistoryResponseModel.appointmentNo}"; + description = "Payment reminder for appointment with ${patientAppointmentHistoryResponseModel.doctorNameObj} at ${patientAppointmentHistoryResponseModel.projectName}"; + break; + case ReminderType.checkIn: + title = "Check-In Reminder for Appointment #${patientAppointmentHistoryResponseModel.appointmentNo}"; + description = "Don't forget to check in for your appointment with ${patientAppointmentHistoryResponseModel.doctorNameObj}"; + break; + } + + // For payment and check-in, remove existing reminders first + if (reminderType == ReminderType.payment || reminderType == ReminderType.checkIn) { + // Remove all existing reminder types + await _removeAllRemindersForAppointment(patientAppointmentHistoryResponseModel); + } + + // For payment and check-in reminders, use custom hour-based options + if (reminderType == ReminderType.payment || reminderType == ReminderType.checkIn) { + _showHourBasedReminderSheet( + context: context, + patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, + reminderType: reminderType, + title: title, + description: description, + eventIdPrefix: eventIdPrefix, + endDate: endDate, + onSuccess: onSuccess, + onError: onError, + ); + } else { + // For appointment reminders, use the standard reminder sheet with all options + BottomSheetUtils().showReminderBottomSheet( + context, + endDate, + patientAppointmentHistoryResponseModel.doctorNameObj ?? "", + "${patientAppointmentHistoryResponseModel.appointmentNo}", + "", + "", + title: title, + description: description, + onSuccess: () { + setAppointmentReminder(true, patientAppointmentHistoryResponseModel); + if (onSuccess != null) { + onSuccess(true); + } + }, + isMultiAllowed: true, + onMultiDateSuccess: (int selectedIndex) async { + String eventTitle; + String eventDescription; + + switch (reminderType) { + case ReminderType.appointment: + eventTitle = "Appointment Reminder with ${patientAppointmentHistoryResponseModel.doctorNameObj} on ${DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate)}, Appointment #${patientAppointmentHistoryResponseModel.appointmentNo}"; + eventDescription = "Appointment Reminder with ${patientAppointmentHistoryResponseModel.doctorNameObj} in ${patientAppointmentHistoryResponseModel.projectName}"; + break; + case ReminderType.payment: + eventTitle = "Payment Due Reminder for Appointment #${patientAppointmentHistoryResponseModel.appointmentNo} with ${patientAppointmentHistoryResponseModel.doctorNameObj}"; + eventDescription = "Payment reminder for ${patientAppointmentHistoryResponseModel.doctorNameObj} at ${patientAppointmentHistoryResponseModel.projectName}"; + break; + case ReminderType.checkIn: + eventTitle = "Check-In Reminder for Appointment #${patientAppointmentHistoryResponseModel.appointmentNo} with ${patientAppointmentHistoryResponseModel.doctorNameObj}"; + eventDescription = "Check-in reminder for appointment with ${patientAppointmentHistoryResponseModel.doctorNameObj} at ${patientAppointmentHistoryResponseModel.projectName}"; + break; + } + + bool isEventAddedOrRemoved = await calender.createOrUpdateEvent( + title: eventTitle, + description: eventDescription, + scheduleDateTime: DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), + eventId: "${eventIdPrefix}_${patientAppointmentHistoryResponseModel.appointmentNo}", + location: patientAppointmentHistoryResponseModel.projectName ?? '', + reminderMinutes: selectedIndex, + ); + + setAppointmentReminder(isEventAddedOrRemoved, patientAppointmentHistoryResponseModel); + + if (onSuccess != null) { + onSuccess(isEventAddedOrRemoved); + } + }, + ); + } + } catch (e) { + if (onError != null) { + onError(e.toString()); + } + } + } + + /// Removes all reminder types for an appointment (internal helper) + Future _removeAllRemindersForAppointment(PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel) async { + CalenderUtilsNew calender = CalenderUtilsNew.instance; + + // Remove all possible reminder types + for (var type in ReminderType.values) { + try { + await calender.checkAndRemove( + id: "${type.eventIdPrefix}_${patientAppointmentHistoryResponseModel.appointmentNo}", + ); + } catch (e) { + // Ignore errors if reminder doesn't exist + debugPrint("No existing ${type.displayName} to remove"); + } + } + } + + /// Shows hour-based reminder selection for payment and check-in reminders + void _showHourBasedReminderSheet({ + required BuildContext context, + required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, + required ReminderType reminderType, + required String title, + required String description, + required String eventIdPrefix, + required DateTime endDate, + Function(bool)? onSuccess, + Function(String)? onError, + }) { + showCommonBottomSheetWithoutHeight( + context, + title: "Select Reminder Time", + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Text( + "When would you like to be reminded?", + style: TextStyle( + fontSize: 14.f, + color: AppColors.greyTextColor, + ), + textAlign: TextAlign.center, + ), + ), + SizedBox(height: 24.h), + _buildHourOption( + context: context, + hours: 1, + icon: Icons.alarm, + patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, + reminderType: reminderType, + title: title, + description: description, + eventIdPrefix: eventIdPrefix, + endDate: endDate, + onSuccess: onSuccess, + onError: onError, + ), + SizedBox(height: 12.h), + _buildHourOption( + context: context, + hours: 2, + icon: Icons.alarm, + patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, + reminderType: reminderType, + title: title, + description: description, + eventIdPrefix: eventIdPrefix, + endDate: endDate, + onSuccess: onSuccess, + onError: onError, + ), + SizedBox(height: 12.h), + _buildHourOption( + context: context, + hours: 3, + icon: Icons.alarm, + patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, + reminderType: reminderType, + title: title, + description: description, + eventIdPrefix: eventIdPrefix, + endDate: endDate, + onSuccess: onSuccess, + onError: onError, + ), + ], + ), + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + /// Builds an hour option button for payment/check-in reminders + Widget _buildHourOption({ + required BuildContext context, + required int hours, + required IconData icon, + required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, + required ReminderType reminderType, + required String title, + required String description, + required String eventIdPrefix, + required DateTime endDate, + Function(bool)? onSuccess, + Function(String)? onError, + }) { + return GestureDetector( + onTap: () async { + Navigator.of(context).pop(); // Close the selection sheet + + // Create reminder with selected hours (converted to minutes) + await _createReminderWithHours( + hours: hours, + patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, + reminderType: reminderType, + title: title, + description: description, + eventIdPrefix: eventIdPrefix, + endDate: endDate, + onSuccess: onSuccess, + onError: onError, + ); + }, + child: Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + border: Border.all(color: AppColors.borderOnlyColor), + borderRadius: BorderRadius.circular(12.r), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: AppColors.primaryRedColor.withAlpha(26), + borderRadius: BorderRadius.circular(8.r), + ), + child: Icon( + icon, + color: AppColors.primaryRedColor, + size: 24.h, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Text( + "$hours ${hours == 1 ? 'Hour' : 'Hours'} Before Appointment", + style: TextStyle( + fontSize: 16.f, + fontWeight: FontWeight.w600, + color: AppColors.textColor, + ), + ), + ), + Icon( + Icons.arrow_forward_ios, + size: 16.h, + color: AppColors.greyTextColor, + ), + ], + ), + ), + ); + } + + /// Creates reminder with specific hours before appointment + Future _createReminderWithHours({ + required int hours, + required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, + required ReminderType reminderType, + required String title, + required String description, + required String eventIdPrefix, + required DateTime endDate, + Function(bool)? onSuccess, + Function(String)? onError, + }) async { + try { + CalenderUtilsNew calender = CalenderUtilsNew.instance; + + // Convert hours to minutes + int reminderMinutes = hours * 60; + + String eventTitle; + String eventDescription; + + switch (reminderType) { + case ReminderType.appointment: + eventTitle = "Appointment Reminder with ${patientAppointmentHistoryResponseModel.doctorNameObj} on ${DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate)}, Appointment #${patientAppointmentHistoryResponseModel.appointmentNo}"; + eventDescription = "Appointment Reminder with ${patientAppointmentHistoryResponseModel.doctorNameObj} in ${patientAppointmentHistoryResponseModel.projectName}"; + break; + case ReminderType.payment: + eventTitle = "Payment Due - $hours ${hours == 1 ? 'hour' : 'hours'} before Appointment #${patientAppointmentHistoryResponseModel.appointmentNo}"; + eventDescription = "Payment reminder for ${patientAppointmentHistoryResponseModel.doctorNameObj} at ${patientAppointmentHistoryResponseModel.projectName}"; + break; + case ReminderType.checkIn: + eventTitle = "Check-In Now - $hours ${hours == 1 ? 'hour' : 'hours'} before Appointment #${patientAppointmentHistoryResponseModel.appointmentNo}"; + eventDescription = "Check-in reminder for appointment with ${patientAppointmentHistoryResponseModel.doctorNameObj} at ${patientAppointmentHistoryResponseModel.projectName}"; + break; + } + + bool isEventAddedOrRemoved = await calender.createOrUpdateEvent( + title: eventTitle, + description: eventDescription, + scheduleDateTime: DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), + eventId: "${eventIdPrefix}_${patientAppointmentHistoryResponseModel.appointmentNo}", + location: patientAppointmentHistoryResponseModel.projectName ?? '', + reminderMinutes: reminderMinutes, + ); + + setAppointmentReminder(isEventAddedOrRemoved, patientAppointmentHistoryResponseModel); + + if (onSuccess != null) { + onSuccess(isEventAddedOrRemoved); + } + } catch (e) { + if (onError != null) { + onError(e.toString()); + } + } + } + + /// Deletes the reminder for the appointment + /// Removes the calendar event associated with the appointment + /// [reminderType] specifies the type of reminder to delete (appointment, payment, or check-in) + Future deleteReminder({ + required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, + ReminderType reminderType = ReminderType.appointment, + Function(bool)? onSuccess, + Function(String)? onError, + }) async { + try { + CalenderUtilsNew calender = CalenderUtilsNew.instance; + String eventIdPrefix = reminderType.eventIdPrefix; + + bool isEventRemoved = await calender.checkAndRemove( + id: "${eventIdPrefix}_${patientAppointmentHistoryResponseModel.appointmentNo}", + ); + + // Update the reminder status (inverted because we're removing) + setAppointmentReminder(!isEventRemoved, patientAppointmentHistoryResponseModel); + + if (onSuccess != null) { + onSuccess(isEventRemoved); + } + } catch (e) { + if (onError != null) { + onError(e.toString()); + } + } + } + + /// Toggle reminder for an appointment + /// Creates or deletes reminder based on the current state + /// [reminderType] specifies the type of reminder (appointment, payment, or check-in) + Future toggleReminder({ + required BuildContext context, + required bool shouldCreateReminder, + required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, + required ReminderType reminderType , + Function(bool)? onSuccess, + Function(String)? onError, + }) async { + if (shouldCreateReminder) { + await createReminder( + context: context, + patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, + reminderType: reminderType, + onSuccess: onSuccess, + onError: onError, + ); + } else { + await deleteReminder( + patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel, + reminderType: reminderType, + onSuccess: onSuccess, + onError: onError, + ); + } + } } diff --git a/lib/features/profile_settings/profile_settings_view_model.dart b/lib/features/profile_settings/profile_settings_view_model.dart index 77960569..b208494a 100644 --- a/lib/features/profile_settings/profile_settings_view_model.dart +++ b/lib/features/profile_settings/profile_settings_view_model.dart @@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/models/get_patient_info_response_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_repo.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; @@ -219,9 +220,10 @@ class ProfileSettingsViewModel extends ChangeNotifier { if (response.data != null && response.data['Patient_GetProfileImageDataList'] != null) { var imageList = response.data['Patient_GetProfileImageDataList']; if (imageList is List && imageList.isNotEmpty) { - profileImageData = imageList[0]['PatientsProfileImageData']; + profileImageData = imageList[0]['ImageData']; // Store in AppState for global access GetIt.instance().setProfileImageData = profileImageData; + } else { profileImageData = null; GetIt.instance().setProfileImageData = null; @@ -286,6 +288,16 @@ class ProfileSettingsViewModel extends ChangeNotifier { profileImageData = imageData; // Store in AppState for global access GetIt.instance().setProfileImageData = imageData; + + // Update the family files cache with the new profile image + try { + final medicalFileViewModel = GetIt.instance.get(); + medicalFileViewModel.updateFamilyMemberProfileImage(patientID, imageData); + print("✅ Updated profile image in family files cache for patient: $patientID"); + } catch (e) { + print("⚠️ Could not update family files cache: $e"); + } + notifyListeners(); onSuccess?.call(response.data); }, diff --git a/lib/features/weather/models/waether_cities_model.dart b/lib/features/weather/models/waether_cities_model.dart new file mode 100644 index 00000000..6a434bff --- /dev/null +++ b/lib/features/weather/models/waether_cities_model.dart @@ -0,0 +1,257 @@ +import 'dart:convert'; + +class GetCityInfoList { + CategoriesNames? categoriesNames; + String? category; + String? categoryValue; + int? cityId; + String? cityName; + String? cityNameN; + ColorName? colorName; + String? createdOn; + String? id; + int? iniceId; + bool? isOrderEmpty; + bool? isValuesReversed; + bool? language; + double? latitude; + double? longitude; + String? name; + int? orderNum; + double? temperature; + String? value; + + GetCityInfoList({ + this.categoriesNames, + this.category, + this.categoryValue, + this.cityId, + this.cityName, + this.cityNameN, + this.colorName, + this.createdOn, + this.id, + this.iniceId, + this.isOrderEmpty, + this.isValuesReversed, + this.language, + this.latitude, + this.longitude, + this.name, + this.orderNum, + this.temperature, + this.value, + }); + + factory GetCityInfoList.fromRawJson(String str) => GetCityInfoList.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory GetCityInfoList.fromJson(Map json) { + ColorName? parseColorName(dynamic value) { + if (value == null) return null; + try { + return colorNameValues.map[value.toString()]; + } catch (e) { + print("Unknown color name: $value"); + return null; + } + } + + CategoriesNames? parseCategoriesNames(dynamic value) { + if (value == null) return null; + try { + return CategoriesNames.fromJson(value as Map); + } catch (e) { + print("Error parsing CategoriesNames: $e"); + return null; + } + } + + return GetCityInfoList( + categoriesNames: parseCategoriesNames(json["CategoriesNames"]), + category: json["Category"], + categoryValue: json["CategoryValue"], + cityId: json["CityID"], + cityName: json["CityName"], + cityNameN: json["CityNameN"], + colorName: parseColorName(json["ColorName"]), + createdOn: json["CreatedOn"], + id: json["ID"], + iniceId: json["IniceID"], + isOrderEmpty: json["IsOrderEmpty"], + isValuesReversed: json["IsValuesReversed"], + language: json["Language"], + latitude: json["Latitude"]?.toDouble(), + longitude: json["Longitude"]?.toDouble(), + name: json["Name"], + orderNum: json["OrderNum"], + temperature: json["Temperature"]?.toDouble(), + value: json["Value"], + ); + } + + Map toJson() => { + "CategoriesNames": categoriesNames?.toJson(), + "Category": category, + "CategoryValue": categoryValue, + "CityID": cityId, + "CityName": cityName, + "CityNameN": cityNameN, + "ColorName": colorNameValues.reverse[colorName], + "CreatedOn": createdOn, + "ID": id, + "IniceID": iniceId, + "IsOrderEmpty": isOrderEmpty, + "IsValuesReversed": isValuesReversed, + "Language": language, + "Latitude": latitude, + "Longitude": longitude, + "Name": name, + "OrderNum": orderNum, + "Temperature": temperature, + "Value": value, + }; +} + +class CategoriesNames { + Category1? category1; + Category2? category2; + Category3? category3; + Category4? category4; + Category5? category5; + + CategoriesNames({ + this.category1, + this.category2, + this.category3, + this.category4, + this.category5, + }); + + factory CategoriesNames.fromRawJson(String str) => CategoriesNames.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory CategoriesNames.fromJson(Map json) { + T? parseEnum(dynamic value, EnumValues enumValues, String enumName) { + if (value == null) return null; + try { + return enumValues.map[value.toString()]; + } catch (e) { + print("Unknown $enumName value: $value"); + return null; + } + } + + return CategoriesNames( + category1: parseEnum(json["Category1"], category1Values, "Category1"), + category2: parseEnum(json["Category2"], category2Values, "Category2"), + category3: parseEnum(json["Category3"], category3Values, "Category3"), + category4: parseEnum(json["Category4"], category4Values, "Category4"), + category5: parseEnum(json["Category5"], category5Values, "Category5"), + ); + } + + Map toJson() => { + "Category1": category1Values.reverse[category1], + "Category2": category2Values.reverse[category2], + "Category3": category3Values.reverse[category3], + "Category4": category4Values.reverse[category4], + "Category5": category5Values.reverse[category5], + }; +} + +enum Category1 { + AT_EXTREME_RISK, + CATEGORY1_EXTREME, + EXTREME, + POOR +} + +final category1Values = EnumValues({ + "At Extreme Risk": Category1.AT_EXTREME_RISK, + "Extreme\u000d\n": Category1.CATEGORY1_EXTREME, + "Extreme": Category1.EXTREME, + "Poor": Category1.POOR +}); + +enum Category2 { + AT_HIGH_RISK, + CATEGORY2_VERY_HIGH, + FAIR, + VERY_HIGH +} + +final category2Values = EnumValues({ + "At High Risk": Category2.AT_HIGH_RISK, + "Very High\u000d\n": Category2.CATEGORY2_VERY_HIGH, + "Fair": Category2.FAIR, + "Very High": Category2.VERY_HIGH +}); + +enum Category3 { + AT_RISK, + CATEGORY3_HIGH, + GOOD, + HIGH +} + +final category3Values = EnumValues({ + "At Risk": Category3.AT_RISK, + "High\u000d\n": Category3.CATEGORY3_HIGH, + "Good": Category3.GOOD, + "High": Category3.HIGH +}); + +enum Category4 { + CATEGORY4_MODERATE, + MODERATE, + NEUTRAL, + VERY_GOOD +} + +final category4Values = EnumValues({ + "Moderate\u000d\n": Category4.CATEGORY4_MODERATE, + "Moderate": Category4.MODERATE, + "Neutral": Category4.NEUTRAL, + "Very Good": Category4.VERY_GOOD +}); + +enum Category5 { + BENEFICIAL, + CATEGORY5_LOW, + EXCELLENT, + LOW +} + +final category5Values = EnumValues({ + "Beneficial": Category5.BENEFICIAL, + "Low\u000d\n": Category5.CATEGORY5_LOW, + "Excellent": Category5.EXCELLENT, + "Low": Category5.LOW +}); + +enum ColorName { + GREEN, + ORANGE, + RED +} + +final colorNameValues = EnumValues({ + "green": ColorName.GREEN, + "orange": ColorName.ORANGE, + "red": ColorName.RED +}); + +class EnumValues { + Map map; + late Map reverseMap; + + EnumValues(this.map); + + Map get reverse { + reverseMap = map.map((k, v) => MapEntry(v, k)); + return reverseMap; + } +} diff --git a/lib/features/weather/weather_repo.dart b/lib/features/weather/weather_repo.dart new file mode 100644 index 00000000..26bf0a32 --- /dev/null +++ b/lib/features/weather/weather_repo.dart @@ -0,0 +1,72 @@ +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/core/location_util.dart'; +import 'package:hmg_patient_app_new/features/weather/models/waether_cities_model.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +abstract class WeatherRepo { + Future>>> getCityInfo(); +} + +class WeatherRepoImp implements WeatherRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + WeatherRepoImp({required this.loggerService, required this.apiClient}); + + @override + Future>>> getCityInfo() async { + Map request = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + WEATHER_INDICATOR, + body: request, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + loggerService.logInfo("Weather API Response: $response"); + final list = response['GetCityInfo_List']; + + if (list == null || list.isEmpty) { + loggerService.logInfo("Weather API returned empty or null list"); + // Return empty list if no city info + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: [], + ); + return; + } + + loggerService.logInfo("Parsing ${list.length} city info items"); + final cityInfoList = list.map((item) => GetCityInfoList.fromJson(item as Map)).toList().cast(); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: cityInfoList, + ); + } catch (e) { + loggerService.logError("Error parsing weather data: $e"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/weather/weather_view_model.dart b/lib/features/weather/weather_view_model.dart new file mode 100644 index 00000000..0e3a3285 --- /dev/null +++ b/lib/features/weather/weather_view_model.dart @@ -0,0 +1,55 @@ +import 'package:flutter/cupertino.dart'; +import 'package:hmg_patient_app_new/core/location_util.dart'; +import 'package:hmg_patient_app_new/features/weather/models/waether_cities_model.dart'; +import 'package:hmg_patient_app_new/features/weather/weather_repo.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +class WeatherMonitorViewModel extends ChangeNotifier { + WeatherRepo weatherRepo; + ErrorHandlerService errorHandlerService; + LocationUtils locationUtils; + + WeatherMonitorViewModel({required this.weatherRepo, required this.errorHandlerService, required this.locationUtils}); + + final List _cityInfoList = []; + bool isLoading = false; + + List get cityInfoList => _cityInfoList; + + Future fetchCityInfoList({Function(dynamic)? onSuccess, Function(String)? onError}) async { + isLoading = true; + _cityInfoList.clear(); + + locationUtils.getLocation( + isShowConfirmDialog: true, + onSuccess: (position) async { + final result = await weatherRepo.getCityInfo(); + result.fold( + (failure) async { + isLoading = false; + notifyListeners(); + await errorHandlerService.handleError(failure: failure); + if (onError != null) { + onError(failure.toString()); + } + }, + (apiResponse) { + isLoading = false; + if (apiResponse.messageStatus == 2) { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? "Unknown error"); + } + } else if (apiResponse.messageStatus == 1) { + final cities = apiResponse.data ?? []; + _cityInfoList.addAll(cities); + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + }); + } +} diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 12abda09..98098186 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -2893,14 +2893,14 @@ static const Map _en_US = { "monitorCholesterolLevels": "Monitor cholesterol levels, assess heart health risks, and take proactive steps for well-being.", "triglyceridesFatBlood": "Triglycerides Fat Blood", "understandTriglyceridesImpact": "Understand triglycerides' impact on heart health with personalized insights and expert recommendations.", - "bmiCalculator": "BMI\nCalculator", - "caloriesCalculator": "Calories\nCalculator", - "bmrCalculator": "BMR\nCalculator", - "idealBodyWeight": "Ideal Body\nWeight", - "bodyFatCalculator": "Body Fat\nCalculator", - "carbsProteinFat": "Carbs\nProtein & Fat", - "ovulationPeriod": "Ovulation\nPeriod", - "deliveryDueDate": "Delivery\nDue Date", + "bmiCalculator": "BMI Calculator", + "caloriesCalculator": "Calories Calculator", + "bmrCalculator": "BMR Calculator", + "idealBodyWeight": "Ideal Body Weight", + "bodyFatCalculator": "Body Fat Calculator", + "carbsProteinFat": "Carbs Protein & Fat", + "ovulationPeriod": "Ovulation Period", + "deliveryDueDate": "Delivery Due Date", "low": "Low", "preDiabetic": "Pre-diabetic", "high": "High", diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index d872f0c9..75b5e390 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1177,13 +1177,21 @@ abstract class LocaleKeys { static const triglyceridesFatBlood = 'triglyceridesFatBlood'; static const understandTriglyceridesImpact = 'understandTriglyceridesImpact'; static const bmiCalculator = 'bmiCalculator'; + static const bmiCalculatorDesc = 'bmiCalculatorDesc'; static const caloriesCalculator = 'caloriesCalculator'; + static const caloriesCalculatorDesc = 'caloriesCalculatorDesc'; static const bmrCalculator = 'bmrCalculator'; + static const bmrCalculatorDesc = 'bmrCalculatorDesc'; static const idealBodyWeight = 'idealBodyWeight'; + static const idealBodyWeightDesc = 'idealBodyWeightDesc'; static const bodyFatCalculator = 'bodyFatCalculator'; + static const bodyFatCalculatorDesc = 'bodyFatCalculatorDesc'; static const carbsProteinFat = 'carbsProteinFat'; + static const carbsProteinFatDesc = 'carbsProteinFatDesc'; static const ovulationPeriod = 'ovulationPeriod'; + static const ovulationPeriodDesc = 'ovulationPeriodDesc'; static const deliveryDueDate = 'deliveryDueDate'; + static const deliveryDueDateDesc = 'deliveryDueDateDesc'; static const low = 'low'; static const preDiabetic = 'preDiabetic'; static const high = 'high'; diff --git a/lib/main.dart b/lib/main.dart index e9b599c9..6f086c34 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:flutter/services.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/features/weather/weather_view_model.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/features/ask_doctor/ask_doctor_view_model.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; @@ -242,6 +243,9 @@ void main() async { ), ChangeNotifierProvider( create: (_) => getIt.get(), + ) , + ChangeNotifierProvider( + create: (_) => getIt.get(), ) ], child: MyApp()), ), // Wrap your app diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index 5344fead..ff656c72 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:ui' as ui; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -33,13 +34,11 @@ import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointmen import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_doctor_card.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_rating_widget.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/ask_doctor_request_type_select.dart'; -import 'package:hmg_patient_app_new/presentation/ask_doctor/ask_doctor_page.dart'; import 'package:hmg_patient_app_new/presentation/ask_doctor/doctor_response_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/bell_switch.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart'; -import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_detail_page.dart'; -import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -47,6 +46,7 @@ import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:lottie/lottie.dart'; import 'package:map_launcher/map_launcher.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; @@ -96,18 +96,54 @@ class _AppointmentDetailsPageState extends State { } else { isAppointmentWithin4Hours = true; } + + // Check if reminder exists in calendar and update the flag + await _checkAndUpdateReminderStatus(); } }); // Initialize countdown timer for payment - if (widget.patientAppointmentHistoryResponseModel.nextAction == 15 || - widget.patientAppointmentHistoryResponseModel.nextAction == 20) { + if (widget.patientAppointmentHistoryResponseModel.nextAction == 15 || widget.patientAppointmentHistoryResponseModel.nextAction == 20) { _startCountdownTimer(); } super.initState(); } + /// Checks if a reminder exists in the calendar for this appointment + /// and updates the hasReminder flag accordingly + Future _checkAndUpdateReminderStatus() async { + try { + CalenderUtilsNew calender = CalenderUtilsNew.instance; + + // Check if reminder exists for this appointment + bool reminderExists = await calender.checkIfEventExist( + widget.patientAppointmentHistoryResponseModel.appointmentNo.toString() ?? "", + ); + + // Update the flag if it's different from current state + if (mounted && widget.patientAppointmentHistoryResponseModel.hasReminder != reminderExists) { + setState(() { + widget.patientAppointmentHistoryResponseModel.hasReminder = reminderExists; + }); + + // Also update in view model to keep data consistent + myAppointmentsViewModel.setAppointmentReminder(reminderExists, widget.patientAppointmentHistoryResponseModel); + } + + debugPrint("Reminder status for appointment ${widget.patientAppointmentHistoryResponseModel.appointmentNo}: $reminderExists"); + setState(() {}); + } catch (e) { + debugPrint("Error checking reminder status: $e"); + // If there's an error, default to false + if (mounted) { + setState(() { + widget.patientAppointmentHistoryResponseModel.hasReminder = false; + }); + } + } + } + void _startCountdownTimer() { final appointmentDate = DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate); _updateTimeRemaining(); @@ -307,11 +343,13 @@ class _AppointmentDetailsPageState extends State { }, onError: (err) { LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, + showCommonBottomSheetWithoutHeight(context, child: Utils.getErrorWidget(loadingText: err.toString()), callBackFunc: () {}, - title: "", isCloseButtonVisible: true, isDismissible: false, isFullScreen: false); + title: "", + isCloseButtonVisible: true, + isDismissible: false, + isFullScreen: false); }); }), callBackFunc: () {}, @@ -326,7 +364,58 @@ class _AppointmentDetailsPageState extends State { onRescheduleTap: () async { openDoctorScheduleCalendar(); }, + timerWidget: Column( + children: [ + SizedBox( + height: 16.w, + ), + Divider( + height: 1, + color: AppColors.dividerColor, + ), + SizedBox( + height: 16.w, + ), + Directionality( + textDirection: ui.TextDirection.ltr, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inDays.toString().padLeft(2, '0') : '00', + LocaleKeys.days.tr(context: context), + ), + ), + _buildTimeSeparator(), + Expanded( + child: _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inHours.remainder(24).toString().padLeft(2, '0') : '00', + LocaleKeys.hours.tr(context: context), + ), + ), + _buildTimeSeparator(), + Expanded( + child: _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inMinutes.remainder(60).toString().padLeft(2, '0') : '00', + LocaleKeys.minutes.tr(context: context), + ), + ), + _buildTimeSeparator(), + Expanded( + child: _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inSeconds.remainder(60).toString().padLeft(2, '0') : '00', + LocaleKeys.seconds.tr(context: context), + ), + ), + ], + ), + ), + ], + ), ), + SizedBox(height: 16.h), !AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? Column( @@ -470,22 +559,24 @@ class _AppointmentDetailsPageState extends State { ], ), SizedBox(height: 8.h), - bookAppointmentsViewModel.appointmentNearestGateResponseModel != null ? Wrap( - direction: Axis.horizontal, - spacing: 8.w, - runSpacing: 8.h, - children: [ - AppCustomChipWidget( - labelText: bookAppointmentsVM.isAppointmentNearestGateLoading - ? "Floor: Ground Floor" + bookAppointmentsViewModel.appointmentNearestGateResponseModel != null + ? Wrap( + direction: Axis.horizontal, + spacing: 8.w, + runSpacing: 8.h, + children: [ + AppCustomChipWidget( + labelText: bookAppointmentsVM.isAppointmentNearestGateLoading + ? "Floor: Ground Floor" : "${LocaleKeys.floor.tr(context: context)}: ${getIt.get().isArabic() ? bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocationN : bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocation}", ).toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading), - AppCustomChipWidget( - labelText: + AppCustomChipWidget( + labelText: "${LocaleKeys.nearestGate.tr(context: context)}: ${getIt.get().isArabic() ? bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumberN : bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumber}") .toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading), - ], - ) : SizedBox.shrink(), + ], + ) + : SizedBox.shrink(), ], ), ), @@ -511,22 +602,27 @@ class _AppointmentDetailsPageState extends State { ], ), const Spacer(), - Switch( - activeThumbColor: AppColors.successColor, - // activeTrackColor: AppColors.successColor.withValues(alpha: .15), - value: widget.patientAppointmentHistoryResponseModel.hasReminder!, + BellAnimatedSwitch( + initialValue: widget.patientAppointmentHistoryResponseModel.hasReminder ?? false, + activeColor: AppColors.successColor.withOpacity(0.2), + inactiveColor: AppColors.lightGrayBGColor, + activeCircleColor: AppColors.successColor, + inactiveCircleColor: AppColors.greyTextColor, + activeIconColor: AppColors.bgGreenColor, + inactiveIconColor: AppColors.greyTextColor, + activeIcon: Utils.buildSvgWithAssets(icon: AppAssets.bell, iconColor: AppColors.whiteColor, width: 15.w, height: 15.h), + inactiveIcon: Utils.buildSvgWithAssets(icon: AppAssets.bell, iconColor: AppColors.whiteColor, width: 15.w, height: 15.h), onChanged: (newValue) async { - CalenderUtilsNew calender = CalenderUtilsNew.instance; + CalenderUtilsNew calender = CalenderUtilsNew.instance; bool isEventAddedOrRemoved = false; - if(newValue == true){ + if (newValue == true) { DateTime startDate = DateTime.now(); - DateTime endDate = DateUtil.convertStringToDate(widget - .patientAppointmentHistoryResponseModel.appointmentDate); + DateTime endDate = DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate); BottomSheetUtils().showReminderBottomSheet( context, endDate, - widget.patientAppointmentHistoryResponseModel.doctorNameObj??"", - "${widget.patientAppointmentHistoryResponseModel.appointmentNo}"??"", + widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "", + "${widget.patientAppointmentHistoryResponseModel.appointmentNo}" ?? "", "", "", title: "Appointment with ${widget.patientAppointmentHistoryResponseModel.doctorNameObj}", @@ -544,27 +640,80 @@ class _AppointmentDetailsPageState extends State { "Appointment Reminder with ${widget.patientAppointmentHistoryResponseModel.doctorNameObj} on ${DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate)}, Appointment #${widget.patientAppointmentHistoryResponseModel.appointmentNo}", description: "Appointment Reminder with ${widget.patientAppointmentHistoryResponseModel.doctorNameObj} in ${widget.patientAppointmentHistoryResponseModel.projectName}", - scheduleDateTime: DateUtil.convertStringToDate(widget - .patientAppointmentHistoryResponseModel.appointmentDate), - eventId: "${widget.patientAppointmentHistoryResponseModel.appointmentNo}", - location: '', - reminderMinutes: selectedIndex - ); + scheduleDateTime: DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), + eventId: "${widget.patientAppointmentHistoryResponseModel.appointmentNo}", + location: '', + reminderMinutes: selectedIndex); setState(() { myAppointmentsViewModel.setAppointmentReminder(isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel); }); }, ); - }else { - isEventAddedOrRemoved = await calender.checkAndRemove( id:"${widget.patientAppointmentHistoryResponseModel.appointmentNo}", ); + } else { + isEventAddedOrRemoved = await calender.checkAndRemove( + id: "${widget.patientAppointmentHistoryResponseModel.appointmentNo}", + ); setState(() { myAppointmentsViewModel.setAppointmentReminder(!isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel); }); } - - }, - ), + ) + + // Switch( + // activeThumbColor: AppColors.successColor, + // // activeTrackColor: AppColors.successColor.withValues(alpha: .15), + // value: widget.patientAppointmentHistoryResponseModel.hasReminder!, + // onChanged: (newValue) async { + // CalenderUtilsNew calender = CalenderUtilsNew.instance; + // bool isEventAddedOrRemoved = false; + // if(newValue == true){ + // DateTime startDate = DateTime.now(); + // DateTime endDate = DateUtil.convertStringToDate(widget + // .patientAppointmentHistoryResponseModel.appointmentDate); + // BottomSheetUtils().showReminderBottomSheet( + // context, + // endDate, + // widget.patientAppointmentHistoryResponseModel.doctorNameObj??"", + // "${widget.patientAppointmentHistoryResponseModel.appointmentNo}"??"", + // "", + // "", + // title: "Appointment with ${widget.patientAppointmentHistoryResponseModel.doctorNameObj}", + // description: + // "${widget.patientAppointmentHistoryResponseModel.doctorNameObj} will be having an appointment on ${widget.patientAppointmentHistoryResponseModel.appointmentDate}", + // onSuccess: () { + // setState(() { + // myAppointmentsViewModel.setAppointmentReminder(newValue, widget.patientAppointmentHistoryResponseModel); + // }); + // }, + // isMultiAllowed: true, + // onMultiDateSuccess: (int selectedIndex) async { + // isEventAddedOrRemoved = await calender.createOrUpdateEvent( + // title: + // "Appointment Reminder with ${widget.patientAppointmentHistoryResponseModel.doctorNameObj} on ${DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate)}, Appointment #${widget.patientAppointmentHistoryResponseModel.appointmentNo}", + // description: + // "Appointment Reminder with ${widget.patientAppointmentHistoryResponseModel.doctorNameObj} in ${widget.patientAppointmentHistoryResponseModel.projectName}", + // scheduleDateTime: DateUtil.convertStringToDate(widget + // .patientAppointmentHistoryResponseModel.appointmentDate), + // eventId: "${widget.patientAppointmentHistoryResponseModel.appointmentNo}", + // location: '', + // reminderMinutes: selectedIndex + // ); + // setState(() { + // myAppointmentsViewModel.setAppointmentReminder(isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel); + // }); + // }, + // ); + // }else { + // isEventAddedOrRemoved = await calender.checkAndRemove( id:"${widget.patientAppointmentHistoryResponseModel.appointmentNo}", ); + // setState(() { + // myAppointmentsViewModel.setAppointmentReminder(!isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel); + // }); + // } + // + // + // }, + // ), ], ).paddingSymmetrical(16.h, 16.h), ), @@ -582,106 +731,116 @@ class _AppointmentDetailsPageState extends State { ), physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, - shrinkWrap: true, - children: [ - MedicalFileCard( - label: LocaleKeys.labResults.tr(context: context), - textColor: AppColors.blackColor, - backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.lab_result_icon, - isLargeText: true, - iconSize: 36.w, - ).onPress(() async { - LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingLabResults.tr(context: context)); - await labViewModel.getLabResultsByAppointmentNo( - appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo, - projectID: widget.patientAppointmentHistoryResponseModel.projectID, - clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, - doctorID: widget.patientAppointmentHistoryResponseModel.doctorID, - doctorName: widget.patientAppointmentHistoryResponseModel.doctorNameObj!, - clinicName: widget.patientAppointmentHistoryResponseModel.clinicName!, - projectName: widget.patientAppointmentHistoryResponseModel.projectName!, - appointmentDate: widget.patientAppointmentHistoryResponseModel.appointmentDate!, - onSuccess: (val) { - LoaderBottomSheet.hideLoader(); - }, - onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }, - ); - }), - MedicalFileCard( - label: "${LocaleKeys.radiology.tr(context: context)} ${LocaleKeys.radiologySubtitle.tr(context: context)}", - textColor: AppColors.blackColor, - backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.radiology_icon, - isLargeText: true, - iconSize: 36.w, - ).onPress(() async { - LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingRadiologyResults.tr(context: context)); - await radiologyViewModel.getPatientRadiologyOrdersByAppointment( - appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo, - projectID: widget.patientAppointmentHistoryResponseModel.projectID, - onSuccess: (val) { - LoaderBottomSheet.hideLoader(); - }, - onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }, - ); - }), - MedicalFileCard( - label: LocaleKeys.prescriptions.tr(context: context), - textColor: AppColors.blackColor, - backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.prescription_item_icon, - isLargeText: true, - iconSize: 36.w, - ).onPress(() async { - LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingAppointmentPrescriptions.tr(context: context)); - await prescriptionsViewModel.getPrescriptionDetails( - getPrescriptionRequestModel(), - onSuccess: (val) { - LoaderBottomSheet.hideLoader(); - if (val.data.isNotEmpty) { - PatientPrescriptionsResponseModel patientPrescriptionsResponseModel = PatientPrescriptionsResponseModel( - doctorImageURL: widget.patientAppointmentHistoryResponseModel.doctorImageURL, - doctorName: widget.patientAppointmentHistoryResponseModel.doctorNameObj, - appointmentDate: widget.patientAppointmentHistoryResponseModel.appointmentDate, - clinicDescription: widget.patientAppointmentHistoryResponseModel.clinicName, - decimalDoctorRate: widget.patientAppointmentHistoryResponseModel.decimalDoctorRate, - name: widget.patientAppointmentHistoryResponseModel.projectName, - isHomeMedicineDeliverySupported: val.data.first.isHomeMedicineDeliverySupported ?? false, - projectID: widget.patientAppointmentHistoryResponseModel.projectID, - clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, - doctorID: widget.patientAppointmentHistoryResponseModel.doctorID, - setupID: widget.patientAppointmentHistoryResponseModel.setupID, - isInOutPatient: widget.patientAppointmentHistoryResponseModel.isInOutPatient, - appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo, - episodeID: widget.patientAppointmentHistoryResponseModel.episodeID, - dischargeNo: 0, + shrinkWrap: true, + children: [ + MedicalFileCard( + label: LocaleKeys.labResults.tr(context: context), + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.lab_result_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() async { + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingLabResults.tr(context: context)); + await labViewModel.getLabResultsByAppointmentNo( + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo, + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, + doctorID: widget.patientAppointmentHistoryResponseModel.doctorID, + doctorName: widget.patientAppointmentHistoryResponseModel.doctorNameObj!, + clinicName: widget.patientAppointmentHistoryResponseModel.clinicName!, + projectName: widget.patientAppointmentHistoryResponseModel.projectName!, + appointmentDate: widget.patientAppointmentHistoryResponseModel.appointmentDate!, + onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, ); - Navigator.of(context).push( - CustomPageRoute( - page: PrescriptionDetailPage( - isFromAppointments: true, prescriptionsResponseModel: patientPrescriptionsResponseModel), - ), + }, + ); + }), + MedicalFileCard( + label: "${LocaleKeys.radiology.tr(context: context)} ${LocaleKeys.radiologySubtitle.tr(context: context)}", + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.radiology_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() async { + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingRadiologyResults.tr(context: context)); + await radiologyViewModel.getPatientRadiologyOrdersByAppointment( + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo, + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, ); - } else { + }, + ); + }), + MedicalFileCard( + label: LocaleKeys.prescriptions.tr(context: context), + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.prescription_item_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() async { + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingAppointmentPrescriptions.tr(context: context)); + await prescriptionsViewModel.getPrescriptionDetails( + getPrescriptionRequestModel(), + onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + if (val.data.isNotEmpty) { + PatientPrescriptionsResponseModel patientPrescriptionsResponseModel = PatientPrescriptionsResponseModel( + doctorImageURL: widget.patientAppointmentHistoryResponseModel.doctorImageURL, + doctorName: widget.patientAppointmentHistoryResponseModel.doctorNameObj, + appointmentDate: widget.patientAppointmentHistoryResponseModel.appointmentDate, + clinicDescription: widget.patientAppointmentHistoryResponseModel.clinicName, + decimalDoctorRate: widget.patientAppointmentHistoryResponseModel.decimalDoctorRate, + name: widget.patientAppointmentHistoryResponseModel.projectName, + isHomeMedicineDeliverySupported: val.data.first.isHomeMedicineDeliverySupported ?? false, + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, + doctorID: widget.patientAppointmentHistoryResponseModel.doctorID, + setupID: widget.patientAppointmentHistoryResponseModel.setupID, + isInOutPatient: widget.patientAppointmentHistoryResponseModel.isInOutPatient, + appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo, + episodeID: widget.patientAppointmentHistoryResponseModel.episodeID, + dischargeNo: 0, + ); + Navigator.of(context).push( + CustomPageRoute( + page: PrescriptionDetailPage(isFromAppointments: true, prescriptionsResponseModel: patientPrescriptionsResponseModel), + ), + ); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: LocaleKeys.noPrescriptionsForAppointment.tr(context: context)), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight( context, child: Utils.getErrorWidget(loadingText: LocaleKeys.noPrescriptionsForAppointment.tr(context: context)), @@ -689,40 +848,29 @@ class _AppointmentDetailsPageState extends State { isFullScreen: false, isCloseButtonVisible: true, ); - } - }, - onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: LocaleKeys.noPrescriptionsForAppointment.tr(context: context)), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }, - ); - // Navigator.of(context).push( - // CustomPageRoute( - // page: VaccineListPage(), - // ), - // ); - }), - MedicalFileCard( - label: LocaleKeys.doctorResponses.tr(context: context), - textColor: AppColors.blackColor, - backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.ask_doctor_medical_file_icon, - isLargeText: true, - iconSize: 36.w, - ).onPress(() { - getIt.get().initAskDoctorViewModel(); - getIt.get().getDoctorResponses(); - Navigator.of(context).push( - CustomPageRoute( - page: DoctorResponsePage(), - ), - ); + }, + ); + // Navigator.of(context).push( + // CustomPageRoute( + // page: VaccineListPage(), + // ), + // ); + }), + MedicalFileCard( + label: LocaleKeys.doctorResponses.tr(context: context), + textColor: AppColors.blackColor, + backgroundColor: AppColors.whiteColor, + svgIcon: AppAssets.ask_doctor_medical_file_icon, + isLargeText: true, + iconSize: 36.w, + ).onPress(() { + getIt.get().initAskDoctorViewModel(); + getIt.get().getDoctorResponses(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorResponsePage(), + ), + ); }), myAppointmentsVM.isAppointmentRatedResponseLoading ? MedicalFileCard( @@ -993,8 +1141,7 @@ class _AppointmentDetailsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.patientAppointmentHistoryResponseModel.nextAction == 15 || - widget.patientAppointmentHistoryResponseModel.nextAction == 20) + if (widget.patientAppointmentHistoryResponseModel.nextAction == 15 || widget.patientAppointmentHistoryResponseModel.nextAction == 20) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1002,10 +1149,7 @@ class _AppointmentDetailsPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.amountBeforeTax.tr(context: context).toText18(isBold: true), - Utils.getPaymentAmountWithSymbol( - widget.patientAppointmentHistoryResponseModel.patientShare!.toString().toText16(isBold: true), - AppColors.blackColor, - 13, + Utils.getPaymentAmountWithSymbol(widget.patientAppointmentHistoryResponseModel.patientShare!.toString().toText16(isBold: true), AppColors.blackColor, 13, isSaudiCurrency: true), ], ), @@ -1013,9 +1157,7 @@ class _AppointmentDetailsPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - child: LocaleKeys.upcomingPaymentNow - .tr(context: context).toText12(isBold: true, color: AppColors.greyTextColor)), + Expanded(child: LocaleKeys.upcomingPaymentNow.tr(context: context).toText12(isBold: true, color: AppColors.greyTextColor)), "VAT 15%(${widget.patientAppointmentHistoryResponseModel.patientTaxAmount})".toText14(isBold: true, color: AppColors.greyTextColor, letterSpacing: -0.64), ], ), @@ -1030,10 +1172,7 @@ class _AppointmentDetailsPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Utils.getPaymentAmountWithSymbol( - widget.patientAppointmentHistoryResponseModel.patientShareWithTax!.toString().toText24(isBold: true), - AppColors.blackColor, - 17, + Utils.getPaymentAmountWithSymbol(widget.patientAppointmentHistoryResponseModel.patientShareWithTax!.toString().toText24(isBold: true), AppColors.blackColor, 17, isSaudiCurrency: true), ], ), @@ -1057,11 +1196,11 @@ class _AppointmentDetailsPageState extends State { borderColor: AppColors.primaryRedColor, textColor: Colors.white, fontSize: 16.f, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w600, borderRadius: 12.r, - padding: EdgeInsets.symmetric(horizontal: 10.w), - height: 50.h, - icon: AppAssets.add_icon, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 50.h, + icon: AppAssets.add_icon, iconColor: Colors.white, iconSize: 18.h, ).paddingSymmetrical(16.h, 24.h) @@ -1072,7 +1211,7 @@ class _AppointmentDetailsPageState extends State { }, backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction), borderColor: - AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.01), + AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), textColor: widget.patientAppointmentHistoryResponseModel.nextAction == 15 ? AppColors.textColor : Colors.white, fontSize: 16.f, fontWeight: FontWeight.w600, @@ -1233,8 +1372,7 @@ class _AppointmentDetailsPageState extends State { myAppointmentsViewModel.initAppointmentsViewModel(); myAppointmentsViewModel.getPatientAppointments(true, false); Navigator.of(context).pop(); - }, title: "", height: ResponsiveExtension.screenHeight * 0.3, isAutoDismiss: true, isCloseButtonVisible: true, isDismissible: false, isFullScreen: false, - isSuccessDialog: true); + }, title: "", height: ResponsiveExtension.screenHeight * 0.3, isAutoDismiss: true, isCloseButtonVisible: true, isDismissible: false, isFullScreen: false, isSuccessDialog: true); }); // LoaderBottomSheet.hideLoader(); case 15: @@ -1254,43 +1392,45 @@ class _AppointmentDetailsPageState extends State { 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), - ), - ], - ), + Lottie.asset(AppAnimations.warningAnimation, + repeat: false, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), + SizedBox(height: 12,), + LocaleKeys.upcomingPaymentPending.tr(context: context).toText14(color: AppColors.textColor, isCenter: true), SizedBox(height: 24.h), + // // Countdown Timer - DD : HH : MM : SS format with labels - Always LTR + // Directionality( + // textDirection: ui.TextDirection.ltr, + // child: 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), diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index d551c444..8923e9fa 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -17,7 +18,6 @@ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_vie import 'package:hmg_patient_app_new/features/my_appointments/utils/appointment_type.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/appointment_details_page.dart'; -import 'package:hmg_patient_app_new/presentation/appointments/widgets/ask_doctor_request_type_select.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/eye_measurement_details_page.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -27,8 +27,9 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'dart:ui' as ui; +import 'package:hmg_patient_app_new/presentation/appointments/appointment_payment_page.dart'; -class AppointmentCard extends StatelessWidget { +class AppointmentCard extends StatefulWidget { final PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel; final MyAppointmentsViewModel myAppointmentsViewModel; final bool isLoading; @@ -56,6 +57,97 @@ class AppointmentCard extends StatelessWidget { this.contactUsViewModel, this.isForRate = false}); + @override + State createState() => _AppointmentCardState(); +} + +class _AppointmentCardState extends State { + Timer? _countdownTimer; + Duration? _timeRemaining; + Function(void Function())? _modalSetState; + + @override + void initState() { + super.initState(); + // Start countdown timer if appointment requires payment + if (widget.patientAppointmentHistoryResponseModel.nextAction == 15) { + _startCountdownTimer(); + } + } + + @override + void dispose() { + _countdownTimer?.cancel(); + super.dispose(); + } + + void _startCountdownTimer() { + final appointmentDate = DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate!); + final expiryDate = appointmentDate.subtract(const Duration(hours: 4)); + + setState(() { + _timeRemaining = expiryDate.difference(DateTime.now()); + }); + + _countdownTimer?.cancel(); + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + final newTimeRemaining = expiryDate.difference(DateTime.now()); + + if (newTimeRemaining.isNegative) { + timer.cancel(); + setState(() { + _timeRemaining = Duration.zero; + }); + _modalSetState?.call(() { + _timeRemaining = Duration.zero; + }); + } else { + setState(() { + _timeRemaining = newTimeRemaining; + }); + _modalSetState?.call(() { + _timeRemaining = newTimeRemaining; + }); + } + }); + } + + // Helper method to build each time unit (number + label) + Widget _buildTimeUnit(String value, String label) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + alignment: Alignment.center, + child: value.toText32( + isBold: true, + color: AppColors.blackColor, + isEnglishOnly: true, + ), + ), + SizedBox(height: 4.h), + Container( + alignment: Alignment.center, + child: label.toText12( + color: AppColors.greyTextColor, + ), + ), + ], + ); + } + + // Helper method to build time separator (:) + Widget _buildTimeSeparator() { + return Padding( + padding: EdgeInsets.only(bottom: 16.h, left: 6.w, right: 6.w), + child: ':'.toText32( + isBold: true, + color: AppColors.blackColor, + ), + ); + } + @override Widget build(BuildContext context) { final appState = getIt.get(); @@ -66,11 +158,11 @@ class AppointmentCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - isForRate ? SizedBox() : _buildHeader(context, appState), + widget.isForRate ? SizedBox() : _buildHeader(context, appState), SizedBox(height: 16.h), _buildDoctorRow(context), SizedBox(height: 16.h), - isForRate ? SizedBox() : _buildActionArea(context, appState), + widget.isForRate ? SizedBox() : _buildActionArea(context, appState), ], ), ), @@ -87,7 +179,7 @@ class AppointmentCard extends StatelessWidget { } Widget _buildChips(BuildContext context, AppState appState) { - final isLiveCare = !isLoading && patientAppointmentHistoryResponseModel.isLiveCareAppointment!; + final isLiveCare = !widget.isLoading && widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment!; return Wrap( alignment: WrapAlignment.start, @@ -96,25 +188,25 @@ class AppointmentCard extends StatelessWidget { runSpacing: 6.h, children: [ AppCustomChipWidget( - icon: isLoading ? AppAssets.walkin_appointment_icon : (isLiveCare ? AppAssets.small_livecare_icon : AppAssets.walkin_appointment_icon), - iconColor: isLoading ? AppColors.textColor : (isLiveCare ? Colors.white : AppColors.textColor), - labelText: isLoading ? LocaleKeys.walkin.tr(context: context) : (isLiveCare ? LocaleKeys.livecare.tr(context: context) : LocaleKeys.walkin.tr(context: context)), - backgroundColor: isLoading ? AppColors.greyColor : (isLiveCare ? AppColors.successColor : AppColors.greyColor), - textColor: isLoading ? AppColors.textColor : (isLiveCare ? Colors.white : AppColors.textColor), - ).toShimmer2(isShow: isLoading), + icon: widget.isLoading ? AppAssets.walkin_appointment_icon : (isLiveCare ? AppAssets.small_livecare_icon : AppAssets.walkin_appointment_icon), + iconColor: widget.isLoading ? AppColors.textColor : (isLiveCare ? Colors.white : AppColors.textColor), + labelText: widget.isLoading ? LocaleKeys.walkin.tr(context: context) : (isLiveCare ? LocaleKeys.livecare.tr(context: context) : LocaleKeys.walkin.tr(context: context)), + backgroundColor: widget.isLoading ? AppColors.greyColor : (isLiveCare ? AppColors.successColor : AppColors.greyColor), + textColor: widget.isLoading ? AppColors.textColor : (isLiveCare ? Colors.white : AppColors.textColor), + ).toShimmer2(isShow: widget.isLoading), AppCustomChipWidget( labelText: - isLoading ? 'OutPatient' : (appState.isArabic() ? patientAppointmentHistoryResponseModel.isInOutPatientDescriptionN! : patientAppointmentHistoryResponseModel.isInOutPatientDescription!), + widget.isLoading ? 'OutPatient' : (appState.isArabic() ? widget.patientAppointmentHistoryResponseModel.isInOutPatientDescriptionN! : widget.patientAppointmentHistoryResponseModel.isInOutPatientDescription!), backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.1), textColor: AppColors.warningColorYellow, - ).toShimmer2(isShow: isLoading), + ).toShimmer2(isShow: widget.isLoading), AppCustomChipWidget( - labelText: isLoading ? 'Booked' : AppointmentType.getAppointmentStatusType(patientAppointmentHistoryResponseModel.patientStatusType!), + labelText: widget.isLoading ? 'Booked' : AppointmentType.getAppointmentStatusType(widget.patientAppointmentHistoryResponseModel.patientStatusType!), backgroundColor: AppColors.successColor.withValues(alpha: 0.1), textColor: AppColors.successColor, - ).toShimmer2(isShow: isLoading), + ).toShimmer2(isShow: widget.isLoading), ], - ).toShimmer2(isShow: isLoading); + ).toShimmer2(isShow: widget.isLoading); } Widget _buildDoctorRow(BuildContext context) { @@ -125,11 +217,11 @@ class AppointmentCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Image.network( - isLoading ? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png' : patientAppointmentHistoryResponseModel.doctorImageURL!, + widget.isLoading ? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png' : widget.patientAppointmentHistoryResponseModel.doctorImageURL!, width: 63.h, height: 63.h, fit: BoxFit.cover, - ).circle(100.r).toShimmer2(isShow: isLoading), + ).circle(100.r).toShimmer2(isShow: widget.isLoading), Transform.translate( offset: Offset(0.0, -20.h), child: Container( @@ -149,11 +241,11 @@ class AppointmentCard extends StatelessWidget { Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h, iconColor: AppColors.ratingColorYellow), SizedBox(height: 2.h), (isFoldable || isTablet) - ? "${patientAppointmentHistoryResponseModel.decimalDoctorRate}".toText9(isBold: true, color: AppColors.textColor, isEnglishOnly: true) - : "${patientAppointmentHistoryResponseModel.decimalDoctorRate ?? "0.0"}".toText11(isBold: true, color: AppColors.textColor, isEnglishOnly: true), + ? "${widget.patientAppointmentHistoryResponseModel.decimalDoctorRate}".toText9(isBold: true, color: AppColors.textColor, isEnglishOnly: true) + : "${widget.patientAppointmentHistoryResponseModel.decimalDoctorRate ?? "0.0"}".toText11(isBold: true, color: AppColors.textColor, isEnglishOnly: true), ], ), - ).circle(100).toShimmer2(isShow: isLoading), + ).circle(100).toShimmer2(isShow: widget.isLoading), ), ], ), @@ -164,20 +256,20 @@ class AppointmentCard extends StatelessWidget { children: [ Row( children: [ - (isLoading ? 'Dr' : "${patientAppointmentHistoryResponseModel.doctorTitle}").toText16(isBold: true, maxlines: 1), - (isLoading ? 'John Doe' : " ${patientAppointmentHistoryResponseModel.doctorNameObj!.truncate(20)}") - .toText16(isBold: true, maxlines: 1, isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")), + (widget.isLoading ? 'Dr' : "${widget.patientAppointmentHistoryResponseModel.doctorTitle}").toText16(isBold: true, maxlines: 1), + (widget.isLoading ? 'John Doe' : " ${widget.patientAppointmentHistoryResponseModel.doctorNameObj!.truncate(20)}") + .toText16(isBold: true, maxlines: 1, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")), SizedBox(width: 12.w), - (patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null && patientAppointmentHistoryResponseModel.doctorNationalityFlagURL!.isNotEmpty) + (widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null && widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL!.isNotEmpty) ? Image.network( - patientAppointmentHistoryResponseModel.doctorNationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png", + widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png", width: 20.h, height: 15.h, fit: BoxFit.cover, ) : SizedBox.shrink(), ], - ).toShimmer2(isShow: isLoading), + ).toShimmer2(isShow: widget.isLoading), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, @@ -185,29 +277,29 @@ class AppointmentCard extends StatelessWidget { runSpacing: 4.h, children: [ AppCustomChipWidget( - labelText: isLoading + labelText: widget.isLoading ? 'Cardiology' - : (patientAppointmentHistoryResponseModel.clinicName!.length > 20 - ? '${patientAppointmentHistoryResponseModel.clinicName!.substring(0, 20)}...' - : patientAppointmentHistoryResponseModel.clinicName!), - ).toShimmer2(isShow: isLoading), + : (widget.patientAppointmentHistoryResponseModel.clinicName!.length > 20 + ? '${widget.patientAppointmentHistoryResponseModel.clinicName!.substring(0, 20)}...' + : widget.patientAppointmentHistoryResponseModel.clinicName!), + ).toShimmer2(isShow: widget.isLoading), AppCustomChipWidget( - labelText: isLoading + labelText: widget.isLoading ? 'Olaya' - : (patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital").length > 15 - ? '${(patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital").substring(0, 15)}...' - : patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital") - .toShimmer2(isShow: isLoading), + : (widget.patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital").length > 15 + ? '${(widget.patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital").substring(0, 15)}...' + : widget.patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital") + .toShimmer2(isShow: widget.isLoading), Directionality( textDirection: ui.TextDirection.ltr, child: AppCustomChipWidget( labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), icon: AppAssets.appointment_calendar_icon, - richText: isLoading - ? 'Cardiology'.toText10().toShimmer2(isShow: isLoading) - : "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)}" + richText: widget.isLoading + ? 'Cardiology'.toText10().toShimmer2(isShow: widget.isLoading) + : "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false)}" .toText10(isEnglishOnly: true, isBold: true), - ).toShimmer2(isShow: isLoading), + ).toShimmer2(isShow: widget.isLoading), ), // AppCustomChipWidget( @@ -240,10 +332,10 @@ class AppointmentCard extends StatelessWidget { } Widget _buildActionArea(BuildContext context, AppState appState) { - if ((((patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) || - (patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) || - !Utils.isClinicAllowedForRebook(patientAppointmentHistoryResponseModel.clinicID ?? 0))) && - AppointmentType.isArrived(patientAppointmentHistoryResponseModel)) { + if ((((widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) || + (widget.patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) || + !Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID ?? 0))) && + AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) { // if (((patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) && // DateTime.now().difference(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate)).inDays <= 15)) { // return Row( @@ -325,25 +417,25 @@ class AppointmentCard extends StatelessWidget { // ); // } else { return CustomButton( - text: isFromMedicalReport ? LocaleKeys.selectAppointment.tr(context: context) : LocaleKeys.viewDetails.tr(context: context), + text: widget.isFromMedicalReport ? LocaleKeys.selectAppointment.tr(context: context) : LocaleKeys.viewDetails.tr(context: context), onPressed: () { - if (isFromMedicalReport) { - if (isForFeedback) { - contactUsViewModel!.setPatientFeedbackSelectedAppointment(patientAppointmentHistoryResponseModel); + if (widget.isFromMedicalReport) { + if (widget.isForFeedback) { + widget.contactUsViewModel!.setPatientFeedbackSelectedAppointment(widget.patientAppointmentHistoryResponseModel); } else { - medicalFileViewModel!.setSelectedMedicalReportAppointment(patientAppointmentHistoryResponseModel); + widget.medicalFileViewModel!.setSelectedMedicalReportAppointment(widget.patientAppointmentHistoryResponseModel); } Navigator.pop(context, false); } else { Navigator.of(context) .push( CustomPageRoute( - page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), ), ) .then((_) { - myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); + widget.myAppointmentsViewModel.initAppointmentsViewModel(); + widget.myAppointmentsViewModel.getPatientAppointments(true, false); }); } }, @@ -356,23 +448,23 @@ class AppointmentCard extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 10.w), // height: isTablet || isFoldable ? 46.h : 40.h, height: 40.h, - icon: isFromMedicalReport ? AppAssets.checkmark_icon : null, + icon: widget.isFromMedicalReport ? AppAssets.checkmark_icon : null, iconColor: AppColors.primaryRedColor, iconSize: 16.h, ); // } } else { - if (isFromMedicalReport) { - if (isForEyeMeasurements) { + if (widget.isFromMedicalReport) { + if (widget.isForEyeMeasurements) { return SizedBox.shrink(); } else { return CustomButton( text: LocaleKeys.selectAppointment.tr(context: context), onPressed: () { - if (isForFeedback) { - contactUsViewModel!.setPatientFeedbackSelectedAppointment(patientAppointmentHistoryResponseModel); + if (widget.isForFeedback) { + widget.contactUsViewModel!.setPatientFeedbackSelectedAppointment(widget.patientAppointmentHistoryResponseModel); } else { - medicalFileViewModel!.setSelectedMedicalReportAppointment(patientAppointmentHistoryResponseModel); + widget.medicalFileViewModel!.setSelectedMedicalReportAppointment(widget.patientAppointmentHistoryResponseModel); } Navigator.pop(context, false); }, @@ -391,33 +483,33 @@ class AppointmentCard extends StatelessWidget { ); } } - return (patientAppointmentHistoryResponseModel.isActiveDoctor ?? true) - ? (AppointmentType.isArrived(patientAppointmentHistoryResponseModel) && - patientAppointmentHistoryResponseModel.isClinicReBookingAllowed == false) + return (widget.patientAppointmentHistoryResponseModel.isActiveDoctor ?? true) + ? (AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) && + widget.patientAppointmentHistoryResponseModel.isClinicReBookingAllowed == false) ? // Show only View Details button without arrow when rebooking not allowed _getArrivedButton(context) : Row( children: [ Expanded( flex: 6, - child: (AppointmentType.isArrived(patientAppointmentHistoryResponseModel) + child: (AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? _getArrivedButton(context) : CustomButton( - text: AppointmentType.getNextActionText(patientAppointmentHistoryResponseModel.nextAction), - onPressed: () => _goToDetails(context), - backgroundColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.15), - borderColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), - textColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), + text: AppointmentType.getNextActionText(widget.patientAppointmentHistoryResponseModel.nextAction), + onPressed: () => handleAppointmentNextAction(widget.patientAppointmentHistoryResponseModel.nextAction, context), + backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.15), + borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), + textColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction), fontSize: (isFoldable || isTablet) ? 12.f : 14.f, fontWeight: FontWeight.w600, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), height: 40.h, - icon: AppointmentType.getNextActionIcon(patientAppointmentHistoryResponseModel.nextAction), - iconColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), + icon: AppointmentType.getNextActionIcon(widget.patientAppointmentHistoryResponseModel.nextAction), + iconColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction), iconSize: 15.h, )) - .toShimmer2(isShow: isLoading), + .toShimmer2(isShow: widget.isLoading), ), SizedBox(width: 8.h), Expanded( @@ -442,7 +534,7 @@ class AppointmentCard extends StatelessWidget { fit: BoxFit.contain, ), ), - ).toShimmer2(isShow: isLoading).onPress(() { + ).toShimmer2(isShow: widget.isLoading).onPress(() { _goToDetails(context); }), ), @@ -454,12 +546,12 @@ class AppointmentCard extends StatelessWidget { Navigator.of(context) .push( CustomPageRoute( - page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), ), ) .then((_) { - myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); + widget.myAppointmentsViewModel.initAppointmentsViewModel(); + widget.myAppointmentsViewModel.getPatientAppointments(true, false); }); }, backgroundColor: AppColors.secondaryLightRedColor, @@ -471,7 +563,7 @@ class AppointmentCard extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 10.w), // height: isTablet || isFoldable ? 46.h : 40.h, height: 40.h, - icon: isFromMedicalReport ? AppAssets.checkmark_icon : null, + icon: widget.isFromMedicalReport ? AppAssets.checkmark_icon : null, iconColor: AppColors.primaryRedColor, iconSize: 16.h, ); @@ -479,7 +571,7 @@ class AppointmentCard extends StatelessWidget { } Widget _getArrivedButton(BuildContext context) { - if (patientAppointmentHistoryResponseModel.isClinicReBookingAllowed == false) { + if (widget.patientAppointmentHistoryResponseModel.isClinicReBookingAllowed == false) { return CustomButton( text: LocaleKeys.viewDetails.tr(context: context), onPressed: () => _goToDetails(context), @@ -518,21 +610,21 @@ class AppointmentCard extends StatelessWidget { } void _goToDetails(BuildContext context) { - if (isFromMedicalReport) return; - if (isForEyeMeasurements) { + if (widget.isFromMedicalReport) return; + if (widget.isForEyeMeasurements) { Navigator.of(context).push( CustomPageRoute( - page: EyeMeasurementDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), + page: EyeMeasurementDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), ), ); } else { - if (!AppointmentType.isArrived(patientAppointmentHistoryResponseModel)) { - bookAppointmentsViewModel.getAppointmentNearestGate(projectID: patientAppointmentHistoryResponseModel.projectID, clinicID: patientAppointmentHistoryResponseModel.clinicID); + if (!AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) { + widget.bookAppointmentsViewModel.getAppointmentNearestGate(projectID: widget.patientAppointmentHistoryResponseModel.projectID, clinicID: widget.patientAppointmentHistoryResponseModel.clinicID); } Navigator.of(context) .push( CustomPageRoute( - page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), ), ) .then((_) { @@ -544,22 +636,22 @@ class AppointmentCard extends StatelessWidget { void openDoctorScheduleCalendar(BuildContext context) async { final doctor = DoctorsListResponseModel( - clinicID: patientAppointmentHistoryResponseModel.clinicID, - projectID: patientAppointmentHistoryResponseModel.projectID, - doctorID: patientAppointmentHistoryResponseModel.doctorID, - doctorImageURL: patientAppointmentHistoryResponseModel.doctorImageURL, - doctorTitle: patientAppointmentHistoryResponseModel.doctorTitle, - name: patientAppointmentHistoryResponseModel.doctorNameObj, + clinicID: widget.patientAppointmentHistoryResponseModel.clinicID, + projectID: widget.patientAppointmentHistoryResponseModel.projectID, + doctorID: widget.patientAppointmentHistoryResponseModel.doctorID, + doctorImageURL: widget.patientAppointmentHistoryResponseModel.doctorImageURL, + doctorTitle: widget.patientAppointmentHistoryResponseModel.doctorTitle, + name: widget.patientAppointmentHistoryResponseModel.doctorNameObj, nationalityFlagURL: '', speciality: [], - clinicName: patientAppointmentHistoryResponseModel.clinicName, - projectName: patientAppointmentHistoryResponseModel.projectName, + clinicName: widget.patientAppointmentHistoryResponseModel.clinicName, + projectName: widget.patientAppointmentHistoryResponseModel.projectName, ); - bookAppointmentsViewModel.setSelectedDoctor(doctor); + widget.bookAppointmentsViewModel.setSelectedDoctor(doctor); LoaderBottomSheet.showLoader(); - await bookAppointmentsViewModel.getDoctorFreeSlots( + await widget.bookAppointmentsViewModel.getDoctorFreeSlots( isBookingForLiveCare: false, onSuccess: (respData) async { LoaderBottomSheet.hideLoader(); @@ -584,4 +676,121 @@ class AppointmentCard extends StatelessWidget { }, ); } + + Future handleAppointmentNextAction(nextAction, BuildContext context) async { + switch (nextAction) { + case 0: + // No action needed + _goToDetails(context); + break; + case 10: + // Confirm appointment - go to details + _goToDetails(context); + break; + case 15: + // Pending payment - show waiting modal + // Ensure timer is running before showing modal + if (_countdownTimer == null || !_countdownTimer!.isActive) { + _startCountdownTimer(); + } + + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.notice.tr(), + child: StatefulBuilder( + builder: (context, setModalState) { + // Store the modal setState callback + _modalSetState = setModalState; + + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Message text + LocaleKeys.upcomingPaymentPending.tr(context: context).toText14( + color: AppColors.textColor, + isCenter: true, + ), + SizedBox(height: 24.h), + // Countdown Timer - DD : HH : MM : SS format with labels + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Days + _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inDays.toString().padLeft(2, '0') : '00', + LocaleKeys.days.tr(context: context), + ), + _buildTimeSeparator(), + // Hours + _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inHours.remainder(24).toString().padLeft(2, '0') : '00', + LocaleKeys.hours.tr(context: context), + ), + _buildTimeSeparator(), + // Minutes + _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inMinutes.remainder(60).toString().padLeft(2, '0') : '00', + LocaleKeys.minutes.tr(context: context), + ), + _buildTimeSeparator(), + // Seconds + _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inSeconds.remainder(60).toString().padLeft(2, '0') : '00', + LocaleKeys.seconds.tr(context: context), + ), + ], + ), + SizedBox(height: 24.h), + // Green Acknowledge button with checkmark icon + CustomButton( + text: LocaleKeys.acknowledged.tr(context: context), + onPressed: () { + _modalSetState = null; // Clear callback when closing + Navigator.of(context).pop(); + }, + backgroundColor: AppColors.successColor, + borderColor: AppColors.successColor, + textColor: Colors.white, + fontSize: 16.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + height: 50.h, + icon: AppAssets.checkmark_icon, + iconColor: Colors.white, + ), + ], + ); + }, + ), + callBackFunc: () { + _modalSetState = null; // Clear callback when closing + }, + isFullScreen: false, + isCloseButtonVisible: true, + ); + break; + case 20: + // Pay now - navigate to payment page + widget.myAppointmentsViewModel.setIsPatientAppointmentShareLoading(true); + Navigator.of(context).push( + CustomPageRoute( + page: AppointmentPaymentPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + ), + ); + break; + case 50: + // Confirm livecare - go to details + _goToDetails(context); + break; + case 90: + // Check-in - go to details + _goToDetails(context); + break; + default: + // Default - go to details + _goToDetails(context); + } + } } + diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index 4b5df07b..593746cb 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -24,7 +24,13 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; class AppointmentDoctorCard extends StatelessWidget { const AppointmentDoctorCard( - {super.key, required this.patientAppointmentHistoryResponseModel, required this.onRescheduleTap, required this.onCancelTap, required this.onAskDoctorTap, this.renderWidgetForERDisplay = false}); + {super.key, + required this.patientAppointmentHistoryResponseModel, + required this.onRescheduleTap, + required this.onCancelTap, + required this.onAskDoctorTap, + this.renderWidgetForERDisplay = false, + this.timerWidget}); final PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel; final VoidCallback onRescheduleTap; @@ -32,6 +38,8 @@ class AppointmentDoctorCard extends StatelessWidget { final VoidCallback onAskDoctorTap; final bool renderWidgetForERDisplay; + final Widget? timerWidget; + @override Widget build(BuildContext context) { return Container( @@ -142,43 +150,43 @@ 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(); - selectedDoctor.doctorID = patientAppointmentHistoryResponseModel.doctorID; - selectedDoctor.doctorImageURL = patientAppointmentHistoryResponseModel.doctorImageURL; - selectedDoctor.name = patientAppointmentHistoryResponseModel.doctorNameObj; - selectedDoctor.doctorTitle = patientAppointmentHistoryResponseModel.doctorTitle; - selectedDoctor.nationalityFlagURL = ""; - selectedDoctor.speciality = patientAppointmentHistoryResponseModel.doctorSpeciality; - selectedDoctor.clinicName = patientAppointmentHistoryResponseModel.clinicName; - selectedDoctor.projectName = patientAppointmentHistoryResponseModel.projectName; - selectedDoctor.clinicID = patientAppointmentHistoryResponseModel.clinicID; - selectedDoctor.projectID = patientAppointmentHistoryResponseModel.projectID; + selectedDoctor.doctorID = patientAppointmentHistoryResponseModel.doctorID; + selectedDoctor.doctorImageURL = patientAppointmentHistoryResponseModel.doctorImageURL; + selectedDoctor.name = patientAppointmentHistoryResponseModel.doctorNameObj; + selectedDoctor.doctorTitle = patientAppointmentHistoryResponseModel.doctorTitle; + selectedDoctor.nationalityFlagURL = ""; + selectedDoctor.speciality = patientAppointmentHistoryResponseModel.doctorSpeciality; + selectedDoctor.clinicName = patientAppointmentHistoryResponseModel.clinicName; + selectedDoctor.projectName = patientAppointmentHistoryResponseModel.projectName; + selectedDoctor.clinicID = patientAppointmentHistoryResponseModel.clinicID; + selectedDoctor.projectID = patientAppointmentHistoryResponseModel.projectID; - getIt.get().setSelectedDoctor(selectedDoctor); - LoaderBottomSheet.showLoader(); - await getIt.get().getDoctorProfile(onSuccess: (dynamic respData) { - LoaderBottomSheet.hideLoader(); - Navigator.of(context).push( - CustomPageRoute( - page: DoctorProfilePage( - isDoctorAllowedToBook: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false), - ), - ), - ); - }, onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); - }) + getIt.get().setSelectedDoctor(selectedDoctor); + LoaderBottomSheet.showLoader(); + await getIt.get().getDoctorProfile(onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage( + isDoctorAllowedToBook: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false), + ), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }) ], ), SizedBox(height: 8.h), @@ -188,6 +196,8 @@ class AppointmentDoctorCard extends StatelessWidget { AppointmentType.isArrived(patientAppointmentHistoryResponseModel), ), ), + if (timerWidget != null) + timerWidget ?? SizedBox() ], ), ), @@ -215,26 +225,26 @@ class AppointmentDoctorCard extends StatelessWidget { iconColor: AppColors.primaryRedColor, ) : - // !patientAppointmentHistoryResponseModel.isLiveCareAppointment! - // ? CustomButton( - // text: LocaleKeys.rebookSameDoctor.tr(), - // onPressed: () { - // onRescheduleTap(); - // }, - // backgroundColor: AppColors.greyColor, - // borderColor: AppColors.greyColor, - // textColor: AppColors.blackColor, - // fontSize: 12.f, - // isBold: true, - // borderRadius: 12.r, - // padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0), - // height: 40.h, - // icon: AppAssets.rebook_appointment_icon, - // iconColor: AppColors.blackColor, - // iconSize: 14.h, - // ) - // : - SizedBox.shrink(); + // !patientAppointmentHistoryResponseModel.isLiveCareAppointment! + // ? CustomButton( + // text: LocaleKeys.rebookSameDoctor.tr(), + // onPressed: () { + // onRescheduleTap(); + // }, + // backgroundColor: AppColors.greyColor, + // borderColor: AppColors.greyColor, + // textColor: AppColors.blackColor, + // fontSize: 12.f, + // isBold: true, + // borderRadius: 12.r, + // padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0), + // height: 40.h, + // icon: AppAssets.rebook_appointment_icon, + // iconColor: AppColors.blackColor, + // iconSize: 14.h, + // ) + // : + SizedBox.shrink(); } else { return patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? CustomButton( diff --git a/lib/presentation/authentication/login.dart b/lib/presentation/authentication/login.dart index d559ebbc..b2979235 100644 --- a/lib/presentation/authentication/login.dart +++ b/lib/presentation/authentication/login.dart @@ -71,7 +71,7 @@ class LoginScreenState extends State { children: [ Utils.showLottie(context: context, assetPath: AppAnimations.login, width: 200.h, height: 200.h, repeat: true, fit: BoxFit.cover), SizedBox(height: 130.h), // Adjusted to sizer unit - LocaleKeys.welcomeToDrSulaiman.tr(context: context).toText32(isBold: true, color: AppColors.textColor), + LocaleKeys.welcomeToDrSulaiman.tr(context: context).toText32(isBold: true, color: AppColors.textColor ), SizedBox(height: 32.h), Localizations.override(context: context, locale: Locale('en', 'US'), child: Container()), // Force English locale for this widget TextInputWidget( diff --git a/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart b/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart index 02dd9b97..c09fbfef 100644 --- a/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart +++ b/lib/presentation/book_appointment/doctor_filter/doctors_filter.dart @@ -1,6 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -59,7 +61,7 @@ class DoctorsFilters extends StatelessWidget{ Text( LocaleKeys.filters.tr(), style:TextStyle( - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', fontWeight: FontWeight.w600, fontSize: 27.f, color: AppColors.textColor, @@ -69,7 +71,7 @@ class DoctorsFilters extends StatelessWidget{ Text( LocaleKeys.clearAllFilters.tr(), style:TextStyle( - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', fontWeight: FontWeight.w600, fontSize: 14.f, color: AppColors.errorColor @@ -199,7 +201,7 @@ class DoctorsFilters extends StatelessWidget{ Text( title, style:TextStyle( - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', fontWeight: FontWeight.w600, fontSize: 16.f, color: AppColors.textColor, diff --git a/lib/presentation/book_appointment/review_appointment_page.dart b/lib/presentation/book_appointment/review_appointment_page.dart index 689e6cf3..86702dbb 100644 --- a/lib/presentation/book_appointment/review_appointment_page.dart +++ b/lib/presentation/book_appointment/review_appointment_page.dart @@ -90,12 +90,14 @@ class _ReviewAppointmentPageState extends State { .toString() .toText16(isBold: true, maxlines: 1), SizedBox(width: 12.w), - (bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL != null && bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL!.isNotEmpty )? Image.network( - bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png", - width: 20.h, - height: 15.h, - fit: BoxFit.cover, - ) : SizedBox.shrink(), + (bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL != null && bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL!.isNotEmpty) + ? Image.network( + bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png", + width: 20.h, + height: 15.h, + fit: BoxFit.cover, + ) + : SizedBox.shrink(), ], ), SizedBox(height: 2.h), @@ -162,7 +164,9 @@ class _ReviewAppointmentPageState extends State { runSpacing: 4.h, children: [ AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} ${LocaleKeys.yearsOld.tr(context: context)}"), - AppCustomChipWidget(labelText: "${LocaleKeys.gender.tr(context: context)}: ${appState.getAuthenticatedUser()?.gender == 1 ? LocaleKeys.malE.tr(context: context) : LocaleKeys.femaleGender.tr(context: context)}"), + AppCustomChipWidget( + labelText: + "${LocaleKeys.gender.tr(context: context)}: ${appState.getAuthenticatedUser()?.gender == 1 ? LocaleKeys.malE.tr(context: context) : LocaleKeys.femaleGender.tr(context: context)}"), ], ), ], @@ -191,19 +195,19 @@ class _ReviewAppointmentPageState extends State { bookAppointmentsViewModel.appointmentNearestGateResponseModel != null ? Wrap( direction: Axis.horizontal, - spacing: 8.w, - runSpacing: 8.h, - children: [ - AppCustomChipWidget( - labelText: bookAppointmentsVM.isAppointmentNearestGateLoading - ? "Floor: Ground Floor" + spacing: 8.w, + runSpacing: 8.h, + children: [ + AppCustomChipWidget( + labelText: bookAppointmentsVM.isAppointmentNearestGateLoading + ? "Floor: Ground Floor" : "${LocaleKeys.floor.tr(context: context)} ${getIt.get().isArabic() ? bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocationN : bookAppointmentsViewModel.appointmentNearestGateResponseModel!.clinicLocation}", ).toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading), - AppCustomChipWidget( - labelText: + AppCustomChipWidget( + labelText: "${LocaleKeys.nearestGate.tr(context: context)} ${getIt.get().isArabic() ? bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumberN : bookAppointmentsVM.appointmentNearestGateResponseModel!.nearestGateNumber}") .toShimmer2(isShow: bookAppointmentsVM.isAppointmentNearestGateLoading), - ], + ], ) : SizedBox.shrink(), ], @@ -317,8 +321,8 @@ class _ReviewAppointmentPageState extends State { void initiateBookAppointment() async { // LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: false, loadingText: bookAppointmentsViewModel.isPatientRescheduleAppointment ? LocaleKeys.reschedulingAppo.tr(context: context) : LocaleKeys.bookingYourAppointment.tr(context: context)); - // LoaderBottomSheet.showLoader(loadingText: bookAppointmentsViewModel.isPatientRescheduleAppointment ? LocaleKeys.reschedulingAppo.tr(context: context) : LocaleKeys.bookingYourAppointment.tr(context: context)); - LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context)); + LoaderBottomSheet.showLoader( + loadingText: bookAppointmentsViewModel.isPatientRescheduleAppointment ? LocaleKeys.reschedulingAppo.tr(context: context) : LocaleKeys.bookingYourAppointment.tr(context: context)); myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); if (bookAppointmentsViewModel.isLiveCareSchedule) { @@ -329,23 +333,16 @@ class _ReviewAppointmentPageState extends State { LoaderBottomSheet.hideLoader(); await Future.delayed(Duration(milliseconds: 50)).then((value) async { // LoaderBottomSheet.showLoader(loadingText: LocaleKeys.appointmentSuccess.tr()); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()), - callBackFunc: () { - bookAppointmentsViewModel.setIsPatientRescheduleAppointment(false); - bookAppointmentsViewModel.setIsLiveCareSchedule(false); - Navigator.pushAndRemoveUntil( - context, - CustomPageRoute( - page: LandingNavigation(), - ), - (r) => false); - }, - isFullScreen: false, - isCloseButtonVisible: false, - isAutoDismiss: true - ); + showCommonBottomSheetWithoutHeight(context, child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()), callBackFunc: () { + bookAppointmentsViewModel.setIsPatientRescheduleAppointment(false); + bookAppointmentsViewModel.setIsLiveCareSchedule(false); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }, isFullScreen: false, isCloseButtonVisible: false, isAutoDismiss: true); }); }); } else { @@ -368,23 +365,11 @@ class _ReviewAppointmentPageState extends State { }, onSuccess: (apiResp) async { LoaderBottomSheet.hideLoader(); await Future.delayed(Duration(milliseconds: 50)).then((value) async { - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()).paddingSymmetrical(0.h, 24.h), - callBackFunc: () { - bookAppointmentsViewModel.setIsLiveCareSchedule(false); - bookAppointmentsViewModel.setIsPatientRescheduleAppointment(false); - Navigator.pushAndRemoveUntil( - context, - CustomPageRoute( - page: LandingNavigation(), - ), - (r) => false); - }, - isFullScreen: false, - isCloseButtonVisible: false, - isAutoDismiss: true - ); + showCommonBottomSheetWithoutHeight(context, child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()).paddingSymmetrical(0.h, 24.h), callBackFunc: () { + bookAppointmentsViewModel.setIsLiveCareSchedule(false); + bookAppointmentsViewModel.setIsPatientRescheduleAppointment(false); + Navigator.pushAndRemoveUntil(context, CustomPageRoute(page: LandingNavigation()), (r) => false); + }, isFullScreen: false, isCloseButtonVisible: false, isAutoDismiss: true); }); }); } diff --git a/lib/presentation/book_appointment/widgets/appointment_success_reminder_bottom_sheet.dart b/lib/presentation/book_appointment/widgets/appointment_success_reminder_bottom_sheet.dart new file mode 100644 index 00000000..ec3afc40 --- /dev/null +++ b/lib/presentation/book_appointment/widgets/appointment_success_reminder_bottom_sheet.dart @@ -0,0 +1,441 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/reminder_type.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; + +enum ReminderBottomSheetStep { + success, + reminderConfirmation, + reminderTypeSelection, + creatingReminder, + reminderSuccess, + reminderError, +} + +class AppointmentSuccessReminderBottomSheet extends StatefulWidget { + final PatientAppointmentHistoryResponseModel appointmentModel; + final MyAppointmentsViewModel myAppointmentsViewModel; + final VoidCallback onComplete; + + const AppointmentSuccessReminderBottomSheet({ + Key? key, + required this.appointmentModel, + required this.myAppointmentsViewModel, + required this.onComplete, + }) : super(key: key); + + @override + State createState() => _AppointmentSuccessReminderBottomSheetState(); +} + +class _AppointmentSuccessReminderBottomSheetState extends State { + ReminderBottomSheetStep currentStep = ReminderBottomSheetStep.success; + ReminderType? selectedReminderType; + String errorMessage = ''; + bool isProcessing = false; + + @override + void initState() { + super.initState(); + // Auto-progress to reminder confirmation after showing success + Future.delayed(Duration(milliseconds: 2000), () { + if (mounted) { + setState(() { + currentStep = ReminderBottomSheetStep.reminderConfirmation; + }); + } + }); + } + + void _handleNoReminder() { + Navigator.of(context).pop(); + widget.onComplete(); + } + + void _handleYesReminder() { + setState(() { + currentStep = ReminderBottomSheetStep.reminderTypeSelection; + }); + } + + void _selectReminderType(ReminderType type) { + setState(() { + selectedReminderType = type; + }); + } + + Future _createReminder() async { + if (selectedReminderType == null) return; + + setState(() { + currentStep = ReminderBottomSheetStep.creatingReminder; + isProcessing = true; + }); + + await widget.myAppointmentsViewModel.toggleReminder( + context: context, + shouldCreateReminder: true, + reminderType: selectedReminderType!, + patientAppointmentHistoryResponseModel: widget.appointmentModel, + onSuccess: (result) { + if (mounted) { + setState(() { + currentStep = ReminderBottomSheetStep.reminderSuccess; + isProcessing = false; + }); + // Auto-close after showing success + Future.delayed(Duration(milliseconds: 2000), () { + if (mounted) { + Navigator.of(context).pop(); + widget.onComplete(); + } + }); + } + }, + onError: (error) { + if (mounted) { + setState(() { + currentStep = ReminderBottomSheetStep.reminderError; + errorMessage = error; + isProcessing = false; + }); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.vertical(top: Radius.circular(24.r)), + ), + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 24.h), + child: AnimatedSwitcher( + duration: Duration(milliseconds: 300), + child: _buildStepContent(), + ), + ); + } + + Widget _buildStepContent() { + switch (currentStep) { + case ReminderBottomSheetStep.success: + return _buildSuccessStep(); + case ReminderBottomSheetStep.reminderConfirmation: + return _buildReminderConfirmationStep(); + case ReminderBottomSheetStep.reminderTypeSelection: + return _buildReminderTypeSelectionStep(); + case ReminderBottomSheetStep.creatingReminder: + return _buildCreatingReminderStep(); + case ReminderBottomSheetStep.reminderSuccess: + return _buildReminderSuccessStep(); + case ReminderBottomSheetStep.reminderError: + return _buildReminderErrorStep(); + } + } + + Widget _buildSuccessStep() { + return Column( + key: ValueKey('success'), + mainAxisSize: MainAxisSize.min, + children: [ + Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr(context: context)), + SizedBox(height: 16.h), + Center( + child: "Your appointment has been booked successfully!".toText14( + color: AppColors.greyTextColor, + ), + ), + ], + ); + } + + Widget _buildReminderConfirmationStep() { + return Column( + key: ValueKey('confirmation'), + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.notifications_active_outlined, + size: 56.h, + color: AppColors.primaryRedColor, + ), + SizedBox(height: 16.h), + Center(child: "Add Reminder?".toText20(isBold: true)), + SizedBox(height: 12.h), + Center( + child: "Would you like to add a reminder for this appointment?".toText14( + color: AppColors.greyTextColor, + ), + ), + SizedBox(height: 24.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: LocaleKeys.cancel.tr(context: context), + onPressed: _handleNoReminder, + backgroundColor: AppColors.whiteColor, + borderColor: AppColors.greyTextColor, + textColor: AppColors.greyTextColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12, + height: 48.h, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: CustomButton( + text: LocaleKeys.confirm.tr(context: context), + onPressed: _handleYesReminder, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: Colors.white, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12, + height: 48.h, + ), + ), + ], + ), + ], + ); + } + + Widget _buildReminderTypeSelectionStep() { + return Column( + key: ValueKey('type_selection'), + mainAxisSize: MainAxisSize.min, + children: [ + Center(child: "Select Reminder Type".toText20(isBold: true)), + SizedBox(height: 8.h), + Center( + child: "Choose what type of reminder you want to set".toText14( + color: AppColors.greyTextColor, + ), + ), + SizedBox(height: 24.h), + _buildReminderTypeCard( + type: ReminderType.appointment, + icon: Icons.event_outlined, + title: "Appointment Reminder", + description: "Get reminded about your upcoming appointment", + ), + SizedBox(height: 12.h), + _buildReminderTypeCard( + type: ReminderType.payment, + icon: Icons.payment_outlined, + title: "Payment Reminder", + description: "Get reminded about payment for this appointment", + ), + SizedBox(height: 12.h), + _buildReminderTypeCard( + type: ReminderType.checkIn, + icon: Icons.check_circle_outline, + title: "Check-In Reminder", + description: "Get reminded to check in for your appointment", + ), + SizedBox(height: 24.h), + Row( + children: [ + Expanded( + child: CustomButton( + text: LocaleKeys.cancel.tr(context: context), + onPressed: () { + setState(() { + currentStep = ReminderBottomSheetStep.reminderConfirmation; + selectedReminderType = null; + }); + }, + backgroundColor: AppColors.whiteColor, + borderColor: AppColors.greyTextColor, + textColor: AppColors.greyTextColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12, + height: 48.h, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: CustomButton( + text: "Continue", + onPressed: selectedReminderType != null ? _createReminder : null, + backgroundColor: selectedReminderType != null ? AppColors.primaryRedColor : AppColors.greyTextColor.withAlpha(76), + borderColor: selectedReminderType != null ? AppColors.primaryRedColor : AppColors.greyTextColor.withAlpha(76), + textColor: Colors.white, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12, + height: 48.h, + ), + ), + ], + ), + ], + ); + } + + Widget _buildReminderTypeCard({ + required ReminderType type, + required IconData icon, + required String title, + required String description, + }) { + final isSelected = selectedReminderType == type; + return GestureDetector( + onTap: () => _selectReminderType(type), + child: Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: isSelected ? AppColors.primaryRedColor.withAlpha(26) : AppColors.scaffoldBgColor, + border: Border.all( + color: isSelected ? AppColors.primaryRedColor : AppColors.borderOnlyColor, + width: isSelected ? 2 : 1, + ), + borderRadius: BorderRadius.circular(12.r), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: isSelected ? AppColors.primaryRedColor : AppColors.greyTextColor.withAlpha(26), + borderRadius: BorderRadius.circular(8.r), + ), + child: Icon( + icon, + color: isSelected ? Colors.white : AppColors.greyTextColor, + size: 24.h, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.toText16( + isBold: true, + color: isSelected ? AppColors.primaryRedColor : AppColors.textColor, + ), + SizedBox(height: 4.h), + description.toText12( + color: AppColors.greyTextColor, + ), + ], + ), + ), + if (isSelected) + Icon( + Icons.check_circle, + color: AppColors.primaryRedColor, + size: 24.h, + ), + ], + ), + ), + ); + } + + Widget _buildCreatingReminderStep() { + return Column( + key: ValueKey('creating'), + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(color: AppColors.primaryRedColor), + SizedBox(height: 24.h), + Center( + child: "Creating your reminder...".toText16( + isBold: true, + ), + ), + SizedBox(height: 8.h), + Center( + child: "Please wait while we set up your reminder".toText14( + color: AppColors.greyTextColor, + ), + ), + ], + ); + } + + Widget _buildReminderSuccessStep() { + return Column( + key: ValueKey('reminder_success'), + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.check_circle, + size: 64.h, + color: AppColors.successColor, + ), + SizedBox(height: 16.h), + Center( + child: LocaleKeys.reminderSuccess.tr(context: context).toText20( + isBold: true, + ), + ), + SizedBox(height: 8.h), + Center( + child: "Your ${selectedReminderType?.displayName ?? 'reminder'} has been set successfully!".toText14( + color: AppColors.greyTextColor, + ), + ), + ], + ); + } + + Widget _buildReminderErrorStep() { + return Column( + key: ValueKey('error'), + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.error_outline, + size: 64.h, + color: AppColors.primaryRedColor, + ), + SizedBox(height: 16.h), + Center( + child: "Error".toText20( + isBold: true, + color: AppColors.primaryRedColor, + ), + ), + SizedBox(height: 8.h), + Center( + child: errorMessage.toText14( + color: AppColors.greyTextColor, + ), + ), + SizedBox(height: 24.h), + CustomButton( + text: "Close", + onPressed: () { + Navigator.of(context).pop(); + widget.onComplete(); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: Colors.white, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12, + height: 48.h, + ), + ], + ); + } +} + diff --git a/lib/presentation/book_appointment/widgets/bell_switch.dart b/lib/presentation/book_appointment/widgets/bell_switch.dart new file mode 100644 index 00000000..498ea6aa --- /dev/null +++ b/lib/presentation/book_appointment/widgets/bell_switch.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; + +class BellAnimatedSwitch extends StatefulWidget { + final bool initialValue; + final ValueChanged? onChanged; + final Color? activeColor; + final Color? inactiveColor; + final Color? activeIconColor; + final Color? inactiveIconColor; + final Color? activeCircleColor; + final Color? inactiveCircleColor; + final Widget? activeIcon; + final Widget? inactiveIcon; + final double width; + final double height; + final Duration animationDuration; + + const BellAnimatedSwitch({ + super.key, + this.initialValue = false, + this.onChanged, + this.activeColor, + this.inactiveColor, + this.activeIconColor, + this.inactiveIconColor, + this.activeCircleColor, + this.inactiveCircleColor, + this.activeIcon, + this.inactiveIcon, + this.width = 70, + this.height = 40, + this.animationDuration = const Duration(milliseconds: 300), + }); + + @override + State createState() => _BellAnimatedSwitchState(); +} + +class _BellAnimatedSwitchState extends State with SingleTickerProviderStateMixin { + late bool _isActive; + late AnimationController _animationController; + late Animation _circleAlignment; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _isActive = widget.initialValue; + _animationController = AnimationController( + duration: widget.animationDuration, + vsync: this, + ); + + // Animation for the circle position (left/right) + _circleAlignment = AlignmentTween( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + + // Scale animation for tap feedback + _scaleAnimation = Tween(begin: 1.0, end: 0.9).animate( + CurvedAnimation( + parent: _animationController, + curve: const Interval(0.0, 0.5, curve: Curves.easeOut), + ), + ); + + if (_isActive) { + _animationController.value = 1.0; + } + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + void _toggle() { + setState(() { + _isActive = !_isActive; + if (_isActive) { + _animationController.forward(); + } else { + _animationController.reverse(); + } + widget.onChanged?.call(_isActive); + }); + } + + Color _getBackgroundColor() { + if (_isActive) { + return widget.activeColor ?? Colors.green.shade400; + } + return widget.inactiveColor ?? Colors.grey.shade300; + } + + Color _getCircleColor() { + if (_isActive) { + return widget.activeCircleColor ?? Colors.white; + } + return widget.inactiveCircleColor ?? Colors.white; + } + + Widget _getIcon() { + if (_isActive) { + return widget.activeIcon ?? Icon( + Icons.check, + size: 18, + color: widget.activeIconColor ?? Colors.green.shade400, + ); + } + return widget.inactiveIcon ?? Icon( + Icons.close, + size: 18, + color: widget.inactiveIconColor ?? Colors.grey.shade600, + ); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: _toggle, + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Container( + width: widget.width, + height: widget.height, + decoration: BoxDecoration( + color: _getBackgroundColor(), + borderRadius: BorderRadius.circular(widget.height / 2), + // boxShadow: [ + // BoxShadow( + // color: Colors.black.withAlpha(26), + // blurRadius: 4, + // offset: const Offset(0, 2), + // ), + // ], + ), + child: Stack( + children: [ + // Animated circle with icon inside + Align( + alignment: _circleAlignment.value, + child: Container( + margin: const EdgeInsets.all(4), + width: widget.height - 8, + height: widget.height - 8, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _getCircleColor(), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(51), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: Center( + child: _getIcon(), + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/presentation/contact_us/feedback_page.dart b/lib/presentation/contact_us/feedback_page.dart index cc70f28e..8ca6619c 100644 --- a/lib/presentation/contact_us/feedback_page.dart +++ b/lib/presentation/contact_us/feedback_page.dart @@ -475,12 +475,12 @@ class FeedbackPage extends StatelessWidget { ), SizedBox(height: 8.h), "${contactUsViewModel.cocItemsList[index].cOCTitle}" - .toText16(weight: FontWeight.bold, fontFamily: Utils.isArabicText(contactUsViewModel.cocItemsList[index].cOCTitle ?? "") ? "GESSTwo" : "Poppins"), + .toText16(weight: FontWeight.bold, fontFamily: Utils.isArabicText(contactUsViewModel.cocItemsList[index].cOCTitle ?? "") ? "CairoArabic" : "Poppins"), SizedBox(height: 8.h), "${contactUsViewModel.cocItemsList[index].detail}".toText14( weight: FontWeight.w600, color: AppColors.textColorLight, - fontFamily: Utils.isArabicText(contactUsViewModel.cocItemsList[index].detail ?? "") ? "GESSTwo" : "Poppins"), + fontFamily: Utils.isArabicText(contactUsViewModel.cocItemsList[index].detail ?? "") ? "CairoArabic" : "Poppins"), ], ), ).paddingOnly(bottom: 16.h), diff --git a/lib/presentation/health_calculators_and_converts/health_calculators_page.dart b/lib/presentation/health_calculators_and_converts/health_calculators_page.dart index bba6f705..877a5d9b 100644 --- a/lib/presentation/health_calculators_and_converts/health_calculators_page.dart +++ b/lib/presentation/health_calculators_and_converts/health_calculators_page.dart @@ -14,6 +14,7 @@ import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/expandable_list_widget.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; class HealthCalculatorsPage extends StatefulWidget { @@ -37,73 +38,89 @@ class _HealthCalculatorsPageState extends State { return CollapsingListView( title: widget.type == HealthCalConEnum.calculator ? LocaleKeys.healthCalculators.tr(context: context) : LocaleKeys.healthConverters.tr(), child: widget.type == HealthCalConEnum.calculator - ? Column( - children: [ - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r), - child: Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.general_health, height: 40.h, width: 40.w, fit: BoxFit.none, applyThemeColor: false), - SizedBox(width: 12.w), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - LocaleKeys.generalHealth.tr().toText16(isBold: true), - LocaleKeys.relatedToBMICalories.tr().toText12(isBold: true, color: Color(0xFF8F9AA3)) - ], - ), - ), - SizedBox( - width: 12.w, - ), - Transform.flip( - flipX: getIt.get().isArabic(), - child: Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor)), - ], - ).paddingAll(16.w)) - .onPress(() { - dialogService.showFamilyBottomSheetWithoutHWithChild( - label: LocaleKeys.selectCalculator.tr(), - message: "", - child: showCalculatorsItems(type: HealthCalculatorEnum.general), - onOkPressed: () {}, - ); - }), - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r), - child: Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.women_health, height: 40.h, width: 40.w, fit: BoxFit.none, applyThemeColor: false), - SizedBox(width: 12.w), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - LocaleKeys.womensHealth.tr().toText16(isBold: true), - LocaleKeys.relatedToPeriodsOvulation.tr().toText12(isBold: true, color: Color(0xFF8F9AA3)) - ], - ), - ), - SizedBox(width: 12.w), - Transform.flip( - flipX: getIt.get().isArabic(), - child: Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor)), - ], - ).paddingAll(16.w)) - .onPress(() { - dialogService.showFamilyBottomSheetWithoutHWithChild( - label: LocaleKeys.selectCalculator.tr(), - message: "", - child: showCalculatorsItems(type: HealthCalculatorEnum.women), - onOkPressed: () {}, - ); - }), + ? CustomExpandableList( + expansionMode: ExpansionMode.exactlyOne, + dividerColor: AppColors.dividerColor, + itemPadding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 14.h), + items: [ + ExpandableListItem( + title: LocaleKeys.womensHealth.tr(context: context).toText18(isBold: true), + expandedBackgroundColor: Colors.transparent, + initiallyExpanded: true, + children: [showCalculatorsItems(type: HealthCalculatorEnum.women)], + ), + ExpandableListItem( + title: LocaleKeys.generalHealth.tr(context: context).toText18(isBold: true), + expandedBackgroundColor: Colors.transparent, + initiallyExpanded: true, + children: [showCalculatorsItems(type: HealthCalculatorEnum.general)], + ), ], - ).paddingSymmetrical(20.w, 24.h) + theme: ExpandableListTheme.custom( + defaultTrailingIcon: Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, height: 22.h, width: 22.w, iconColor: AppColors.textColor), + ), + ).paddingSymmetrical(16.w, 0.0) + // ? Column( + // children: [ + // Container( + // decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r), + // child: Row( + // children: [ + // Utils.buildSvgWithAssets(icon: AppAssets.general_health, height: 40.h, width: 40.w, fit: BoxFit.none, applyThemeColor: false), + // SizedBox(width: 12.w), + // Flexible( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // mainAxisAlignment: MainAxisAlignment.center, + // children: [LocaleKeys.generalHealth.tr().toText16(isBold: true), LocaleKeys.relatedToBMICalories.tr().toText12(isBold: true, color: Color(0xFF8F9AA3))], + // ), + // ), + // SizedBox( + // width: 12.w, + // ), + // Transform.flip( + // flipX: getIt.get().isArabic(), + // child: Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor)), + // ], + // ).paddingAll(16.w)) + // .onPress(() { + // dialogService.showFamilyBottomSheetWithoutHWithChild( + // label: LocaleKeys.selectCalculator.tr(), + // message: "", + // child: showCalculatorsItems(type: HealthCalculatorEnum.general), + // onOkPressed: () {}, + // ); + // }), + // SizedBox(height: 16.h), + // Container( + // decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r), + // child: Row( + // children: [ + // Utils.buildSvgWithAssets(icon: AppAssets.women_health, height: 40.h, width: 40.w, fit: BoxFit.none, applyThemeColor: false), + // SizedBox(width: 12.w), + // Flexible( + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // mainAxisAlignment: MainAxisAlignment.center, + // children: [LocaleKeys.womensHealth.tr().toText16(isBold: true), LocaleKeys.relatedToPeriodsOvulation.tr().toText12(isBold: true, color: Color(0xFF8F9AA3))], + // ), + // ), + // SizedBox(width: 12.w), + // Transform.flip( + // flipX: getIt.get().isArabic(), + // child: Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor)), + // ], + // ).paddingAll(16.w)) + // .onPress(() { + // dialogService.showFamilyBottomSheetWithoutHWithChild( + // label: LocaleKeys.selectCalculator.tr(), + // message: "", + // child: showCalculatorsItems(type: HealthCalculatorEnum.women), + // onOkPressed: () {}, + // ); + // }), + // ], + // ).paddingSymmetrical(20.w, 24.h) : Column( children: [ Container( @@ -151,11 +168,7 @@ class _HealthCalculatorsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, - children: [ - LocaleKeys.bloodCholesterol.tr().toText16(isBold: true), - LocaleKeys.monitorCholesterolLevels.tr() - .toText12(isBold: true, color: Color(0xFF8F9AA3)) - ], + children: [LocaleKeys.bloodCholesterol.tr().toText16(isBold: true), LocaleKeys.monitorCholesterolLevels.tr().toText12(isBold: true, color: Color(0xFF8F9AA3))], ), ), SizedBox(width: 12.w), @@ -183,8 +196,7 @@ class _HealthCalculatorsPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ LocaleKeys.triglyceridesFatBlood.tr().toText16(isBold: true), - LocaleKeys.understandTriglyceridesImpact.tr() - .toText12(isBold: true, color: Color(0xFF8F9AA3)) + LocaleKeys.understandTriglyceridesImpact.tr().toText12(isBold: true, color: Color(0xFF8F9AA3)) ], ), ), @@ -204,49 +216,104 @@ class _HealthCalculatorsPageState extends State { } Widget showCalculatorsItems({required HealthCalculatorEnum type}) { - return GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, // 4 icons per row - crossAxisSpacing: 16.w, - mainAxisSpacing: 16.w, - childAspectRatio: 0.85), - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: type == HealthCalculatorEnum.general ? generalHealthServices.length : womenHealthServices.length, - padding: EdgeInsets.zero, - itemBuilder: (BuildContext context, int index) { - return HealthCard( - icon: type == HealthCalculatorEnum.general ? generalHealthServices[index].icon : womenHealthServices[index].icon, - labelText: type == HealthCalculatorEnum.general ? generalHealthServices[index].title : womenHealthServices[index].title, - onTap: () { - Navigator.pop(context); - Navigator.of(context).push( - CustomPageRoute( - page: HealthCalculatorDetailedPage( - calculatorType: type == HealthCalculatorEnum.general ? generalHealthServices[index].type : womenHealthServices[index].type, - clinicID: type == HealthCalculatorEnum.general ? generalHealthServices[index].clinicID : womenHealthServices[index].clinicID, - calculationID: type == HealthCalculatorEnum.general ? generalHealthServices[index].calculationID : womenHealthServices[index].calculationID, + return ListView.separated( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: type == HealthCalculatorEnum.general ? generalHealthServices.length : womenHealthServices.length, + padding: EdgeInsets.zero, + itemBuilder: (BuildContext context, int index) { + return HealthCard( + icon: type == HealthCalculatorEnum.general ? generalHealthServices[index].icon : womenHealthServices[index].icon, + labelText: type == HealthCalculatorEnum.general ? generalHealthServices[index].title : womenHealthServices[index].title, + subtitle: type == HealthCalculatorEnum.general ? generalHealthServices[index].subTitle ?? "" : womenHealthServices[index].subTitle ?? "", + iconColor: type == HealthCalculatorEnum.general ? generalHealthServices[index].iconColor : womenHealthServices[index].iconColor, + iconBgColor: type == HealthCalculatorEnum.general ? generalHealthServices[index].bgColor : womenHealthServices[index].bgColor, + onTap: () { + Navigator.of(context).push( + CustomPageRoute( + page: HealthCalculatorDetailedPage( + calculatorType: type == HealthCalculatorEnum.general ? generalHealthServices[index].type : womenHealthServices[index].type, + clinicID: type == HealthCalculatorEnum.general ? generalHealthServices[index].clinicID : womenHealthServices[index].clinicID, + calculationID: type == HealthCalculatorEnum.general ? generalHealthServices[index].calculationID : womenHealthServices[index].calculationID, + ), ), - ), - ); - }, - ); - }, - ); + ); + }, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(height: 16.h); + }); } final List generalHealthServices = [ - HealthComponentModel(title: LocaleKeys.bmiCalculator.tr(), icon: AppAssets.bmi, type: HealthCalculatorsTypeEnum.bmi, clinicID: 108, calculationID: null), - HealthComponentModel(title: LocaleKeys.caloriesCalculator.tr(), icon: AppAssets.calories, type: HealthCalculatorsTypeEnum.calories, clinicID: null, calculationID: 2), - HealthComponentModel(title: LocaleKeys.bmrCalculator.tr(), icon: AppAssets.bmr, type: HealthCalculatorsTypeEnum.bmr, clinicID: null, calculationID: 3), - HealthComponentModel(title: LocaleKeys.idealBodyWeight.tr(), icon: AppAssets.ibw, type: HealthCalculatorsTypeEnum.idealBodyWeight, clinicID: null, calculationID: 4), - HealthComponentModel(title: LocaleKeys.bodyFatCalculator.tr(), icon: AppAssets.ibw, type: HealthCalculatorsTypeEnum.bodyFat, clinicID: null, calculationID: 5), - HealthComponentModel(title: LocaleKeys.carbsProteinFat.tr(), icon: AppAssets.ibw, type: HealthCalculatorsTypeEnum.crabsProteinFat, clinicID: null, calculationID: 11), + HealthComponentModel( + title: LocaleKeys.bmiCalculator.tr(), + subTitle: LocaleKeys.bmiCalculatorDesc.tr(), + icon: AppAssets.bmicalc, + type: HealthCalculatorsTypeEnum.bmi, + clinicID: 108, + bgColor: AppColors.bmiCalculatorColor, + calculationID: null), + HealthComponentModel( + title: LocaleKeys.caloriesCalculator.tr(), + subTitle: LocaleKeys.caloriesCalculatorDesc.tr(), + icon: AppAssets.bmicalc, + type: HealthCalculatorsTypeEnum.calories, + clinicID: null, + bgColor: AppColors.caloriesCalculatorColor, + calculationID: 2), + HealthComponentModel( + title: LocaleKeys.bmrCalculator.tr(), + subTitle: LocaleKeys.bmrCalculatorDesc.tr(), + icon: AppAssets.bmrcalc, + type: HealthCalculatorsTypeEnum.bmr, + clinicID: null, + bgColor: AppColors.bmrCalculatorColor, + calculationID: 3), + HealthComponentModel( + title: LocaleKeys.idealBodyWeight.tr(), + subTitle: LocaleKeys.idealBodyWeightDesc.tr(), + icon: AppAssets.idealbodyweightcacl, + type: HealthCalculatorsTypeEnum.idealBodyWeight, + bgColor: AppColors.idealBodyWeightColor, + clinicID: null, + calculationID: 4), + HealthComponentModel( + title: LocaleKeys.bodyFatCalculator.tr(), + subTitle: LocaleKeys.bodyFatCalculatorDesc.tr(), + icon: AppAssets.bodyfatcalc, + type: HealthCalculatorsTypeEnum.bodyFat, + clinicID: null, + bgColor: AppColors.bodyFatCalculatorColor, + calculationID: 5), + HealthComponentModel( + title: LocaleKeys.carbsProteinFat.tr(), + subTitle: LocaleKeys.carbsProteinFatDesc.tr(), + icon: AppAssets.bmicalc, + type: HealthCalculatorsTypeEnum.crabsProteinFat, + clinicID: null, + bgColor: AppColors.carbsProteinFatColor, + calculationID: 11), ]; final List womenHealthServices = [ - HealthComponentModel(title: LocaleKeys.ovulationPeriod.tr(), icon: AppAssets.locate_me, type: HealthCalculatorsTypeEnum.ovulation, clinicID: null, calculationID: 6 ), - HealthComponentModel(title: LocaleKeys.deliveryDueDate.tr(), icon: AppAssets.activeCheck, type: HealthCalculatorsTypeEnum.deliveryDueDate, clinicID: null, calculationID: 6), + HealthComponentModel( + title: LocaleKeys.ovulationPeriod.tr(), + subTitle: LocaleKeys.ovulationPeriodDesc.tr(), + icon: AppAssets.ovluationperiod, + type: HealthCalculatorsTypeEnum.ovulation, + clinicID: null, + calculationID: 6, + bgColor: AppColors.ovulationPeriodColor), + HealthComponentModel( + title: LocaleKeys.deliveryDueDate.tr(), + subTitle: LocaleKeys.deliveryDueDateDesc.tr(), + icon: AppAssets.duedatecalc, + type: HealthCalculatorsTypeEnum.deliveryDueDate, + clinicID: null, + bgColor: AppColors.deliveryDueDateColor, + calculationID: 6), ]; } diff --git a/lib/presentation/health_calculators_and_converts/widgets/health_card.dart b/lib/presentation/health_calculators_and_converts/widgets/health_card.dart index 21482e7c..e6c3d4d4 100644 --- a/lib/presentation/health_calculators_and_converts/widgets/health_card.dart +++ b/lib/presentation/health_calculators_and_converts/widgets/health_card.dart @@ -1,4 +1,7 @@ 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'; @@ -9,41 +12,55 @@ class HealthCard extends StatelessWidget { const HealthCard({ super.key, required this.icon, + this.iconColor, + this.iconBgColor, required this.labelText, + required this.subtitle, this.onTap, }); final String icon; + final Color? iconColor; + final Color? iconBgColor; final String labelText; + final String subtitle; final VoidCallback? onTap; @override Widget build(BuildContext context) { + AppState appState = getIt.get(); return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.r, - hasShadow: false, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.r, hasShadow: false), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Utils.buildSvgWithAssets( - icon: icon, + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: iconBgColor ?? Colors.transparent, borderRadius: 10.r), + height: 40.w, width: 40.w, - height: 40.h, - fit: BoxFit.cover, - ).toShimmer2(isShow: false, radius: 12.r), - SizedBox(height: 14.h), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Flexible( - child: labelText.toText12( - isBold: true, - maxLine: 2, - )), - ], + child: Utils.buildSvgWithAssets(icon: icon, fit: BoxFit.none, height: 22.w, width: 22.w, iconColor: iconColor), + ), + SizedBox(width: 12.h), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + labelText.toText16(weight: FontWeight.w600), + ], + ), + SizedBox(height: 4.h), + subtitle.toText12(maxLine: 2, color: AppColors.greyTextColor), + ], + ), + ), + SizedBox(width: 2.h), + Transform.flip( + flipX: appState.getLanguageCode() == "ar", + child: Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.cover, iconColor: AppColors.textColor), ), ], ).paddingAll(16.w).onPress(() { diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index 770a5cbe..c92d47e5 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -6,6 +6,7 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/location_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -19,6 +20,7 @@ import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/hmg_s import 'package:hmg_patient_app_new/features/hospital/hospital_selection_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/blood_donation/blood_donation_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; @@ -47,47 +49,46 @@ import '../../core/dependencies.dart' show getIt; import '../../features/qr_parking/qr_parking_view_model.dart'; import '../emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart'; -class ServicesPage extends StatelessWidget { +class ServicesPage extends StatefulWidget { bool showBackIcon; ServicesPage({super.key, this.showBackIcon = true}); + @override + State createState() => _ServicesPageState(); +} + +class _ServicesPageState extends State { late BloodDonationViewModel bloodDonationViewModel; + late MedicalFileViewModel medicalFileViewModel; + late WeatherMonitorViewModel weatherVM; + late final List hmgServices = [ HmgServicesComponentModel(11, LocaleKeys.emergencyServices.tr(), "", AppAssets.emergency_services_icon, bgColor: AppColors.primaryRedColor, true, route: null, onTap: () async { // if (getIt.get().isAuthenticated) { - // getIt.get().flushData(); - // getIt.get().getTransportationOrders( - // showLoader: false, - // ); - // getIt.get().getRRTOrders( - // showLoader: false, - // ); - Navigator.of(getIt.get().navigatorKey.currentContext!).push( - CustomPageRoute( - page: EmergencyServicesPage(), - settings: const RouteSettings(name: '/EmergencyServicesPage'), - ), - ); + // getIt.get().flushData(); + // getIt.get().getTransportationOrders( + // showLoader: false, + // ); + // getIt.get().getRRTOrders( + // showLoader: false, + // ); + Navigator.of(getIt.get().navigatorKey.currentContext!).push( + CustomPageRoute( + page: EmergencyServicesPage(), + settings: const RouteSettings(name: '/EmergencyServicesPage'), + ), + ); // } else { // await getIt.get().onLoginPressed(); // } }), - HmgServicesComponentModel( - 11, - LocaleKeys.bookAppointment.tr(), - "", - AppAssets.appointment_calendar_icon, - bgColor: AppColors.bookAppointment, - true, - route: null, - onTap: () { - getIt.get().onTabChanged(0); - Navigator.of(getIt().navigatorKey.currentContext!).push(CustomPageRoute(page: BookAppointmentPage())); - } - ), + HmgServicesComponentModel(11, LocaleKeys.bookAppointment.tr(), "", AppAssets.appointment_calendar_icon, bgColor: AppColors.bookAppointment, true, route: null, onTap: () { + getIt.get().onTabChanged(0); + Navigator.of(getIt().navigatorKey.currentContext!).push(CustomPageRoute(page: BookAppointmentPage())); + }), HmgServicesComponentModel(5, LocaleKeys.completeCheckup.tr(), "", AppAssets.comprehensiveCheckup, bgColor: AppColors.bgGreenColor, true, route: null, onTap: () async { if (getIt.get().isAuthenticated) { getIt.get().pushPageRoute(AppRoutes.comprehensiveCheckupPage); @@ -164,33 +165,31 @@ class ServicesPage extends StatelessWidget { } }), HmgServicesComponentModel( - 3, - LocaleKeys.bloodDonation.tr(context: getIt.get().navigatorKey.currentContext!), - "".needTranslation, - AppAssets.blood_donation_icon, - bgColor: AppColors.bloodDonationCardColor, - true, - route: null, - onTap: () { - if (getIt.get().isAuthenticated) { - Navigator.of(getIt().navigatorKey.currentContext!).push( - CustomPageRoute( - page: BloodDonationPage(), - ), - ); - } else { - Navigator.of(getIt().navigatorKey.currentContext!).push( - CustomPageRoute( - page: ServiceInfoPage( - serviceName: LocaleKeys.bloodDonation.tr(), - serviceHeader: LocaleKeys.bloodDonationServiceHeader.tr(), - serviceDescription: LocaleKeys.bloodDonationServiceDescription.tr(), - serviceImage: AppAssets.bloodDonationService), - ), - ); - } + 3, + LocaleKeys.bloodDonation.tr(context: getIt.get().navigatorKey.currentContext!), + "".needTranslation, + AppAssets.blood_donation_icon, + bgColor: AppColors.bloodDonationCardColor, + true, + route: null, onTap: () { + if (getIt.get().isAuthenticated) { + Navigator.of(getIt().navigatorKey.currentContext!).push( + CustomPageRoute( + page: BloodDonationPage(), + ), + ); + } else { + Navigator.of(getIt().navigatorKey.currentContext!).push( + CustomPageRoute( + page: ServiceInfoPage( + serviceName: LocaleKeys.bloodDonation.tr(), + serviceHeader: LocaleKeys.bloodDonationServiceHeader.tr(), + serviceDescription: LocaleKeys.bloodDonationServiceDescription.tr(), + serviceImage: AppAssets.bloodDonationService), + ), + ); } - ), + }), HmgServicesComponentModel( 113, LocaleKeys.servicePriceList.tr(context: getIt.get().navigatorKey.currentContext!), @@ -375,21 +374,100 @@ class ServicesPage extends StatelessWidget { ), ]; + @override + void initState() { + // TODO: implement initState + super.initState(); + weatherVM = getIt(); + weatherVM.fetchCityInfoList(); + } + + + @override Widget build(BuildContext context) { bloodDonationViewModel = Provider.of(context); medicalFileViewModel = Provider.of(context); - hmgServices.removeWhere((element) => Utils.havePrivilege(element.action) == false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( title: LocaleKeys.exploreServices.tr(), - isLeading: showBackIcon, + isLeading: widget.showBackIcon, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + children: [ + Flexible( + child: Container( + padding: EdgeInsets.all(16.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false), + child: Column( + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: "assets/images/svg/cloudyweather.svg", width: 64.w, height: 64.w).onPress(() { + // locationUtils?.getCurrentLocation(); + print(getIt.get().userLat); + print(getIt.get().userLong); + }), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "August 19, 2025 - Tuesday, 11:25 ".toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), + SizedBox( + height: 4.w, + ), + "Cloudy 38oC".toText16(isBold: true, weight: FontWeight.w600), + SizedBox( + height: 4.w, + ), + "Health tips based on current weather".toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500) + ], + ), + ], + ), + SizedBox(height: 16.w), + Container( + padding: EdgeInsets.all(16.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.lightGrayBGColor, + borderRadius: 10.r, + hasShadow: false, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: "assets/images/svg/morning.svg", width: 24.w, height: 24.h), + SizedBox(width: 4.w), + DateTime.now().toLocal().toString().substring(11, 16).toText14(isBold: true), + ], + ), + SizedBox(width: 25.w), + DateTime.now().toLocal().toString().substring(11, 16).toText14(isBold: true), + SizedBox(width: 25.w), + Row( + children: [ + Utils.buildSvgWithAssets(icon: "assets/images/svg/evening.svg", width: 24.w, height: 24.h), + SizedBox(width: 4.w), + DateTime.now().toLocal().toString().substring(11, 16).toText14(isBold: true), + ], + ), + ], + ).expanded, + ).paddingSymmetrical(0.w, 8.w) + ], + ), + ), + ) + ], + ).paddingSymmetrical(24.w, 0.w), SizedBox(height: 16.h), LocaleKeys.medicalAndCareServices.tr().toText18(isBold: true).paddingSymmetrical(24.w, 0), SizedBox(height: 16.h), @@ -440,173 +518,176 @@ class ServicesPage extends StatelessWidget { ), ), SizedBox(height: 24.h), - getIt.get().isAuthenticated ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.personalServices.tr().toText18(isBold: true).paddingSymmetrical(24.w, 0), - SizedBox(height: 16.h), - Row( - children: [ - Expanded( - child: Container( - height: 183.h, - width: 183.h, - padding: EdgeInsets.all(16.w), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.r, - hasShadow: false, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - spacing: 8.w, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.wallet, width: 40.w, height: 40.h, applyThemeColor: false), - LocaleKeys.habibWallet.tr().toText14(isBold: true, maxlines: 2).expanded, - Utils.buildSvgWithAssets(icon: getIt.get().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward), - ], - ), - Spacer(), - getIt.get().isAuthenticated - ? Consumer(builder: (context, habibWalletVM, child) { - return Utils.getPaymentAmountWithSymbol2(num.parse(NumberFormat.decimalPattern().format(habibWalletVM.habibWalletAmount)), isExpanded: false, letterSpacing: -1) - .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.r, width: 80.w, height: 24.h); - }) - : LocaleKeys.loginToViewWalletBalance.tr().toText12(isBold: true, maxLine: 2), - Spacer(), - getIt.get().isAuthenticated - ? CustomButton( - height: 40.h, - icon: AppAssets.recharge_icon, - iconSize: 24.w, - iconColor: AppColors.infoColor, - textColor: AppColors.infoColor, - text: LocaleKeys.recharge.tr(), - borderWidth: 0.w, - isBold: true, - borderColor: Colors.transparent, - backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08), - padding: EdgeInsets.all(8.w), - fontSize: 14.f, - onPressed: () { - Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage())); - }, - ) - : SizedBox.shrink(), - ], - ).onPress(() async { - if (getIt.get().isAuthenticated) { - Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage())); - } else { - await getIt.get().onLoginPressed(); - } - }), - ), - ), - SizedBox(width: 16.w), - Expanded( - child: Container( - height: 183.h, - width: 183.h, - padding: EdgeInsets.all(16.w), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.r, - hasShadow: false, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - spacing: 8.w, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.services_medical_file_icon, width: 40.w, height: 40.h, applyThemeColor: false), - LocaleKeys.medicalFile.tr().toText16(isBold: true, maxlines: 2).expanded, - Utils.buildSvgWithAssets(icon: getIt.get().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward), - ], + getIt.get().isAuthenticated + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.personalServices.tr().toText18(isBold: true).paddingSymmetrical(24.w, 0), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: Container( + height: 183.h, + width: 183.h, + padding: EdgeInsets.all(16.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 8.w, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.wallet, width: 40.w, height: 40.h, applyThemeColor: false), + LocaleKeys.habibWallet.tr().toText14(isBold: true, maxlines: 2).expanded, + Utils.buildSvgWithAssets(icon: getIt.get().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward), + ], + ), + Spacer(), + getIt.get().isAuthenticated + ? Consumer(builder: (context, habibWalletVM, child) { + return Utils.getPaymentAmountWithSymbol2(num.parse(NumberFormat.decimalPattern().format(habibWalletVM.habibWalletAmount)), + isExpanded: false, letterSpacing: -1) + .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.r, width: 80.w, height: 24.h); + }) + : LocaleKeys.loginToViewWalletBalance.tr().toText12(isBold: true, maxLine: 2), + Spacer(), + getIt.get().isAuthenticated + ? CustomButton( + height: 40.h, + icon: AppAssets.recharge_icon, + iconSize: 24.w, + iconColor: AppColors.infoColor, + textColor: AppColors.infoColor, + text: LocaleKeys.recharge.tr(), + borderWidth: 0.w, + isBold: true, + borderColor: Colors.transparent, + backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08), + padding: EdgeInsets.all(8.w), + fontSize: 14.f, + onPressed: () { + Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage())); + }, + ) + : SizedBox.shrink(), + ], + ).onPress(() async { + if (getIt.get().isAuthenticated) { + Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage())); + } else { + await getIt.get().onLoginPressed(); + } + }), ), - Spacer(), - getIt.get().isAuthenticated - ? Wrap( - spacing: -12.h, - // runSpacing: 0.h, - children: [ - Utils.buildImgWithAssets( - icon: AppAssets.babyGirlImg, - height: 32.h, - width: 32.w, - border: 1, - fit: BoxFit.contain, - borderRadius: 50.r, - ), - Utils.buildImgWithAssets( - icon: AppAssets.femaleImg, - height: 32.h, - width: 32.w, - border: 1, - borderRadius: 50.r, - fit: BoxFit.contain, - ), - Utils.buildImgWithAssets( - icon: AppAssets.maleImg, - height: 32.h, - width: 32.w, - border: 1, - borderRadius: 50.r, - fit: BoxFit.contain, - ), - ], - ) - : LocaleKeys.loginToViewMedicalFile.tr().toText12(isBold: true, maxLine: 2), - Spacer(), - getIt.get().isAuthenticated - ? CustomButton( - height: 40.h, - icon: AppAssets.add_icon, - iconSize: 24.h, - iconColor: AppColors.primaryRedColor, - textColor: AppColors.primaryRedColor, - text: LocaleKeys.addMember.tr(), - borderWidth: 0.w, - isBold: true, - borderColor: Colors.transparent, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.08), - padding: EdgeInsets.all(8.w), - fontSize: 14.f, - onPressed: () { - DialogService dialogService = getIt.get(); - medicalFileViewModel.clearAuthValues(); - dialogService.showAddFamilyFileSheet( - label: LocaleKeys.addFamilyMember.tr(), - message: LocaleKeys.pleaseFillBelowFieldToAddNewFamilyMember.tr(), - onVerificationPress: () { - medicalFileViewModel.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms); - }); - }, - ) - : SizedBox.shrink(), - ], - ).onPress(() async { - if (getIt.get().isAuthenticated) { - Navigator.of(context).push( - CustomPageRoute( - page: MedicalFilePage(), + ), + SizedBox(width: 16.w), + Expanded( + child: Container( + height: 183.h, + width: 183.h, + padding: EdgeInsets.all(16.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, ), - ); - } else { - await getIt.get().onLoginPressed(); - } - }), - ), - ), - ], - ).paddingSymmetrical(24.w, 0), - ], - ) : SizedBox(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 8.w, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.services_medical_file_icon, width: 40.w, height: 40.h, applyThemeColor: false), + LocaleKeys.medicalFile.tr().toText16(isBold: true, maxlines: 2).expanded, + Utils.buildSvgWithAssets(icon: getIt.get().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward), + ], + ), + Spacer(), + getIt.get().isAuthenticated + ? Wrap( + spacing: -12.h, + // runSpacing: 0.h, + children: [ + Utils.buildImgWithAssets( + icon: AppAssets.babyGirlImg, + height: 32.h, + width: 32.w, + border: 1, + fit: BoxFit.contain, + borderRadius: 50.r, + ), + Utils.buildImgWithAssets( + icon: AppAssets.femaleImg, + height: 32.h, + width: 32.w, + border: 1, + borderRadius: 50.r, + fit: BoxFit.contain, + ), + Utils.buildImgWithAssets( + icon: AppAssets.maleImg, + height: 32.h, + width: 32.w, + border: 1, + borderRadius: 50.r, + fit: BoxFit.contain, + ), + ], + ) + : LocaleKeys.loginToViewMedicalFile.tr().toText12(isBold: true, maxLine: 2), + Spacer(), + getIt.get().isAuthenticated + ? CustomButton( + height: 40.h, + icon: AppAssets.add_icon, + iconSize: 24.h, + iconColor: AppColors.primaryRedColor, + textColor: AppColors.primaryRedColor, + text: LocaleKeys.addMember.tr(), + borderWidth: 0.w, + isBold: true, + borderColor: Colors.transparent, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.08), + padding: EdgeInsets.all(8.w), + fontSize: 14.f, + onPressed: () { + DialogService dialogService = getIt.get(); + medicalFileViewModel.clearAuthValues(); + dialogService.showAddFamilyFileSheet( + label: LocaleKeys.addFamilyMember.tr(), + message: LocaleKeys.pleaseFillBelowFieldToAddNewFamilyMember.tr(), + onVerificationPress: () { + medicalFileViewModel.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms); + }); + }, + ) + : SizedBox.shrink(), + ], + ).onPress(() async { + if (getIt.get().isAuthenticated) { + Navigator.of(context).push( + CustomPageRoute( + page: MedicalFilePage(), + ), + ); + } else { + await getIt.get().onLoginPressed(); + } + }), + ), + ), + ], + ).paddingSymmetrical(24.w, 0), + ], + ) + : SizedBox(), SizedBox(height: 24.h), LocaleKeys.healthTools.tr().toText18(isBold: true).paddingSymmetrical(24.w, 0), SizedBox(height: 16.h), diff --git a/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart b/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart index 52dba2dd..ad2ad77f 100644 --- a/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart +++ b/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart @@ -2,6 +2,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; @@ -58,7 +60,7 @@ class LabOrderResultItem extends StatelessWidget { style: TextStyle( fontSize: 24.f, fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', color: tests!.checkIfGraphShouldBeDisplayed() ? context.read().getColor( tests?.calculatedResultFlag ?? "", @@ -77,7 +79,7 @@ class LabOrderResultItem extends StatelessWidget { style: TextStyle( fontSize: 12.f, fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', color: AppColors.greyTextColor, ), softWrap: true, diff --git a/lib/presentation/lab/lab_results/lab_result_details.dart b/lib/presentation/lab/lab_results/lab_result_details.dart index fb5fd583..9fbb49a8 100644 --- a/lib/presentation/lab/lab_results/lab_result_details.dart +++ b/lib/presentation/lab/lab_results/lab_result_details.dart @@ -286,7 +286,7 @@ class _LabResultDetailsState extends State { style: TextStyle( fontSize: 12.f, fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', color: AppColors.greyTextColor, ), softWrap: true, @@ -364,7 +364,7 @@ class _LabResultDetailsState extends State { value, style: TextStyle( fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', fontSize: 8.f, color: AppColors.textColor, ), @@ -376,7 +376,7 @@ class _LabResultDetailsState extends State { padding: const EdgeInsets.only(top: 8.0), child: Text( label, - style: TextStyle(fontSize: 8.f, fontFamily: 'Poppins', fontWeight: FontWeight.w600, color: AppColors.labelTextColor), + style: TextStyle(fontSize: 8.f, fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', fontWeight: FontWeight.w600, color: AppColors.labelTextColor), ), ); } diff --git a/lib/presentation/lab/lab_results/lab_result_list_item.dart b/lib/presentation/lab/lab_results/lab_result_list_item.dart index 908d129d..78369463 100644 --- a/lib/presentation/lab/lab_results/lab_result_list_item.dart +++ b/lib/presentation/lab/lab_results/lab_result_list_item.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart' ; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -25,7 +27,7 @@ class LabHistoryItem extends StatelessWidget{ style: TextStyle( fontSize: 14.f, fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', color: AppColors.labelTextColor ), ), @@ -34,7 +36,7 @@ class LabHistoryItem extends StatelessWidget{ style: TextStyle( fontSize: 18.f, fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', color: AppColors.textColor ), ) diff --git a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart index 13ea39dd..2e769609 100644 --- a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart +++ b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -13,14 +14,16 @@ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_vie import 'package:hmg_patient_app_new/features/my_appointments/utils/appointment_type.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/appointment_details_page.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/appointment_payment_page.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'dart:ui' as ui; -class MedicalFileAppointmentCard extends StatelessWidget { +class MedicalFileAppointmentCard extends StatefulWidget { final PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel; final MyAppointmentsViewModel myAppointmentsViewModel; final Function onRescheduleTap; @@ -34,6 +37,97 @@ class MedicalFileAppointmentCard extends StatelessWidget { required this.onAskDoctorTap, }); + @override + State createState() => _MedicalFileAppointmentCardState(); +} + +class _MedicalFileAppointmentCardState extends State { + Timer? _countdownTimer; + Duration? _timeRemaining; + Function(void Function())? _modalSetState; + + @override + void initState() { + super.initState(); + // Start countdown timer if appointment requires payment + if (widget.patientAppointmentHistoryResponseModel.nextAction == 15) { + _startCountdownTimer(); + } + } + + @override + void dispose() { + _countdownTimer?.cancel(); + super.dispose(); + } + + void _startCountdownTimer() { + final appointmentDate = DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate!); + final expiryDate = appointmentDate.subtract(const Duration(hours: 4)); + + setState(() { + _timeRemaining = expiryDate.difference(DateTime.now()); + }); + + _countdownTimer?.cancel(); + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + final newTimeRemaining = expiryDate.difference(DateTime.now()); + + if (newTimeRemaining.isNegative) { + timer.cancel(); + setState(() { + _timeRemaining = Duration.zero; + }); + _modalSetState?.call(() { + _timeRemaining = Duration.zero; + }); + } else { + setState(() { + _timeRemaining = newTimeRemaining; + }); + _modalSetState?.call(() { + _timeRemaining = newTimeRemaining; + }); + } + }); + } + + // Helper method to build each time unit (number + label) + Widget _buildTimeUnit(String value, String label) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + alignment: Alignment.center, + child: value.toText32( + isBold: true, + color: AppColors.blackColor, + isEnglishOnly: true, + ), + ), + SizedBox(height: 4.h), + Container( + alignment: Alignment.center, + child: label.toText12( + color: AppColors.greyTextColor, + ), + ), + ], + ); + } + + // Helper method to build time separator (:) + Widget _buildTimeSeparator() { + return Padding( + padding: EdgeInsets.only(bottom: 16.h, left: 6.w, right: 6.w), + child: ':'.toText32( + isBold: true, + color: AppColors.blackColor, + ), + ); + } + @override Widget build(BuildContext context) { AppState appState = getIt.get(); @@ -43,17 +137,17 @@ class MedicalFileAppointmentCard extends StatelessWidget { AppCustomChipWidget( richText: Directionality( textDirection: ui.TextDirection.ltr, - child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false) - .toText12(color: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor, isBold: true, isEnglishOnly: true) + child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false) + .toText12(color: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor, isBold: true, isEnglishOnly: true) .paddingSymmetrical(8.w, 0), ), - icon: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) ? AppAssets.appointment_calendar_icon : AppAssets.alarm_clock_icon, - iconColor: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor, + icon: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? AppAssets.appointment_calendar_icon : AppAssets.alarm_clock_icon, + iconColor: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor, iconSize: 16.w, - backgroundColor: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) ? AppColors.greyColor : AppColors.secondaryLightRedColor, - textColor: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor, + backgroundColor: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? AppColors.greyColor : AppColors.secondaryLightRedColor, + textColor: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? AppColors.textColor : AppColors.primaryRedColor, padding: EdgeInsets.only(top: 12.h, bottom: 12.h, left: 8.w, right: 8.w), - ).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading), + ).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading), SizedBox(height: 16.h), Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false), @@ -64,20 +158,20 @@ class MedicalFileAppointmentCard extends StatelessWidget { Row( children: [ Image.network( - patientAppointmentHistoryResponseModel.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", + widget.patientAppointmentHistoryResponseModel.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", width: 25.w, height: 27.h, fit: BoxFit.fill, - ).circle(100).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading), + ).circle(100).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading), SizedBox(width: 8.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - (patientAppointmentHistoryResponseModel.doctorNameObj ?? "").toText14(isBold: true, maxlines: 1, isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? "")).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading), - (patientAppointmentHistoryResponseModel.clinicName ?? "") + (widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "").toText14(isBold: true, maxlines: 1, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "")).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading), + (widget.patientAppointmentHistoryResponseModel.clinicName ?? "") .toText12(maxLine: 1, isBold: true, color: AppColors.greyTextColor) - .toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading), + .toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading), ], ), ), @@ -86,42 +180,35 @@ class MedicalFileAppointmentCard extends StatelessWidget { SizedBox(height: 12.h), Row( children: [ - myAppointmentsViewModel.isMyAppointmentsLoading + widget.myAppointmentsViewModel.isMyAppointmentsLoading ? Container().toShimmer2(isShow: true, height: 40.h, width: 100.w, radius: 12.r) : Expanded( flex: 7, - child: AppointmentType.isArrived(patientAppointmentHistoryResponseModel) - ? getArrivedAppointmentButton(context).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading) + child: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) + ? getArrivedAppointmentButton(context).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading) : CustomButton( - text: AppointmentType.getNextActionText(patientAppointmentHistoryResponseModel.nextAction), + text: AppointmentType.getNextActionText(widget.patientAppointmentHistoryResponseModel.nextAction), onPressed: () { - Navigator.of(context) - .push(CustomPageRoute( - page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), - )) - .then((val) { - // widget.myAppointmentsViewModel.initAppointmentsViewModel(); - // widget.myAppointmentsViewModel.getPatientAppointments(true, false); - }); + handleAppointmentNextAction(widget.patientAppointmentHistoryResponseModel.nextAction, context); }, - backgroundColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.15), - borderColor: AppointmentType.getNextActionButtonColor(patientAppointmentHistoryResponseModel.nextAction).withOpacity(0.01), - textColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), + backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.15), + borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), + textColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction), fontSize: 14.f, fontWeight: FontWeight.w600, borderRadius: 12.r, padding: EdgeInsets.symmetric(horizontal: 10.w), height: 40.h, - icon: AppointmentType.getNextActionIcon(patientAppointmentHistoryResponseModel.nextAction), - iconColor: AppointmentType.getNextActionTextColor(patientAppointmentHistoryResponseModel.nextAction), + icon: AppointmentType.getNextActionIcon(widget.patientAppointmentHistoryResponseModel.nextAction), + iconColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction), iconSize: 14.h, - ).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading), + ).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading), ), SizedBox(width: 8.w), - ((((patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) || - (patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) || - !Utils.isClinicAllowedForRebook(patientAppointmentHistoryResponseModel.clinicID ?? 0))) && - AppointmentType.isArrived(patientAppointmentHistoryResponseModel)) + ((((widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) || + (widget.patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) || + !Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID ?? 0))) && + AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) ? SizedBox.shrink() : Expanded( flex: 2, @@ -145,11 +232,11 @@ class MedicalFileAppointmentCard extends StatelessWidget { ), ), ), - ).toShimmer2(isShow: myAppointmentsViewModel.isMyAppointmentsLoading).onPress(() { + ).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading).onPress(() { Navigator.of(context) .push( CustomPageRoute( - page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), ), ) .then((val) { @@ -168,22 +255,22 @@ class MedicalFileAppointmentCard extends StatelessWidget { } Widget getArrivedAppointmentButton(BuildContext context) { - if ((((patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) || - (patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) || - !Utils.isClinicAllowedForRebook(patientAppointmentHistoryResponseModel.clinicID ?? 0))) && - AppointmentType.isArrived(patientAppointmentHistoryResponseModel)) { + if ((((widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) || + (widget.patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) || + !Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID ?? 0))) && + AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) { return CustomButton( text: LocaleKeys.viewDetails.tr(context: context), onPressed: () { Navigator.of(context) .push( CustomPageRoute( - page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel), + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), ), ) .then((_) { - myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); + widget.myAppointmentsViewModel.initAppointmentsViewModel(); + widget.myAppointmentsViewModel.getPatientAppointments(true, false); }); }, backgroundColor: AppColors.secondaryLightRedColor, @@ -200,30 +287,10 @@ class MedicalFileAppointmentCard extends StatelessWidget { iconSize: 16.h, ); } else { - return - // DateTime.now().difference(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate)).inDays <= 15 - // ? CustomButton( - // text: LocaleKeys.askDoctor.tr(context: context), - // onPressed: () { - // onAskDoctorTap(); - // }, - // backgroundColor: AppColors.secondaryLightRedColor, - // borderColor: AppColors.secondaryLightRedColor, - // textColor: AppColors.primaryRedColor, - // fontSize: 14.f, - // fontWeight: FontWeight.w600, - // borderRadius: 12.r, - // padding: EdgeInsets.symmetric(horizontal: 10.w), - // height: 40.h, - // icon: AppAssets.ask_doctor_icon, - // iconColor: AppColors.primaryRedColor, - // iconSize: 16.h, - // ) - // : - CustomButton( + return CustomButton( text: LocaleKeys.rebook.tr(context: context), onPressed: () { - onRescheduleTap(); + widget.onRescheduleTap(); }, backgroundColor: AppColors.greyColor, borderColor: AppColors.greyColor, @@ -239,4 +306,141 @@ class MedicalFileAppointmentCard extends StatelessWidget { ); } } + + Future handleAppointmentNextAction(nextAction, BuildContext context) async { + switch (nextAction) { + case 0: + // No action needed - go to details + Navigator.of(context) + .push(CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + )) + .then((val) {}); + break; + case 10: + // Confirm appointment - go to details + Navigator.of(context) + .push(CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + )) + .then((val) {}); + break; + case 15: + // Pending payment - show waiting modal + // Ensure timer is running before showing modal + if (_countdownTimer == null || !_countdownTimer!.isActive) { + _startCountdownTimer(); + } + + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.notice.tr(), + child: StatefulBuilder( + builder: (context, setModalState) { + // Store the modal setState callback + _modalSetState = setModalState; + + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Message text + LocaleKeys.upcomingPaymentPending.tr(context: context).toText14( + color: AppColors.textColor, + isCenter: true, + ), + SizedBox(height: 24.h), + // Countdown Timer - DD : HH : MM : SS format with labels + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Days + _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inDays.toString().padLeft(2, '0') : '00', + LocaleKeys.days.tr(context: context), + ), + _buildTimeSeparator(), + // Hours + _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inHours.remainder(24).toString().padLeft(2, '0') : '00', + LocaleKeys.hours.tr(context: context), + ), + _buildTimeSeparator(), + // Minutes + _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inMinutes.remainder(60).toString().padLeft(2, '0') : '00', + LocaleKeys.minutes.tr(context: context), + ), + _buildTimeSeparator(), + // Seconds + _buildTimeUnit( + _timeRemaining != null ? _timeRemaining!.inSeconds.remainder(60).toString().padLeft(2, '0') : '00', + LocaleKeys.seconds.tr(context: context), + ), + ], + ), + SizedBox(height: 24.h), + // Green Acknowledge button with checkmark icon + CustomButton( + text: LocaleKeys.acknowledged.tr(context: context), + onPressed: () { + _modalSetState = null; // Clear callback when closing + Navigator.of(context).pop(); + }, + backgroundColor: AppColors.successColor, + borderColor: AppColors.successColor, + textColor: Colors.white, + fontSize: 16.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + height: 50.h, + icon: AppAssets.checkmark_icon, + iconColor: Colors.white, + ), + ], + ); + }, + ), + callBackFunc: () { + _modalSetState = null; // Clear callback when closing + }, + isFullScreen: false, + isCloseButtonVisible: true, + ); + break; + case 20: + // Pay now - navigate to payment page + widget.myAppointmentsViewModel.setIsPatientAppointmentShareLoading(true); + Navigator.of(context).push( + CustomPageRoute( + page: AppointmentPaymentPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + ), + ); + break; + case 50: + // Confirm livecare - go to details + Navigator.of(context) + .push(CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + )) + .then((val) {}); + break; + case 90: + // Check-in - go to details + Navigator.of(context) + .push(CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + )) + .then((val) {}); + break; + default: + // Default - go to details + Navigator.of(context) + .push(CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + )) + .then((val) {}); + } + } } + diff --git a/lib/presentation/medical_report/medical_reports_page.dart b/lib/presentation/medical_report/medical_reports_page.dart index f1b55e02..ee287662 100644 --- a/lib/presentation/medical_report/medical_reports_page.dart +++ b/lib/presentation/medical_report/medical_reports_page.dart @@ -398,14 +398,18 @@ class _MedicalReportsPageState extends State { onConfirmTap: () async { Navigator.pop(context); LoaderBottomSheet.showLoader(); - await medicalFileViewModel.insertRequestForMedicalReport(onSuccess: (val) { + await medicalFileViewModel.insertRequestForMedicalReport(onSuccess: (val) async{ LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight(context, child: Utils.getSuccessWidget(loadingText: LocaleKeys.yourMedicalReportRequestSubmittedSuccessfully.tr(context: context)), - callBackFunc: () { + callBackFunc: () async{ medicalFileViewModel.setIsPatientMedicalReportsLoading(true); medicalFileViewModel.onMedicalReportTabChange(0); medicalFileViewModel.getPatientMedicalReportList(); + + }); + await Future.delayed(Duration(milliseconds: 2000)).then((value) { + if (mounted) Navigator.pop(context); }); }, onError: (err) { LoaderBottomSheet.hideLoader(); diff --git a/lib/presentation/my_family/widget/family_cards.dart b/lib/presentation/my_family/widget/family_cards.dart index c8553207..84406aad 100644 --- a/lib/presentation/my_family/widget/family_cards.dart +++ b/lib/presentation/my_family/widget/family_cards.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -9,11 +10,13 @@ import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/core/utils/image_compression_helper.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/models/family_file_response_model.dart'; +import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; @@ -24,6 +27,8 @@ import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/user_avatar_widget.dart'; +import 'package:hmg_patient_app_new/widgets/image_picker.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; class FamilyCards extends StatefulWidget { @@ -58,6 +63,9 @@ class FamilyCards extends StatefulWidget { class _FamilyCardsState extends State { AppState appState = getIt(); late InsuranceViewModel insuranceViewModel; + late ProfileSettingsViewModel profileSettingsViewModel; + File? _selectedImage; + bool _isUploadingImage = false; @override void initState() { @@ -67,6 +75,321 @@ class _FamilyCardsState extends State { super.initState(); } + void _pickImage() { + // Show image picker options without checking permissions first + ImageOptions.showImageOptionsNew( + context, + false, // Don't show files option, only camera and gallery + (base64String, file) async { + try { + print('=== Starting image processing ==='); + print('File path: ${file.path}'); + print('File exists: ${await file.exists()}'); + print('Original file size: ${await file.length() / 1024} KB'); + + // Compress and resize the image + print('Calling compressAndResizeImage...'); + final compressedFile = await ImageCompressionHelper.compressAndResizeImage(file); + + File finalFile; + String finalBase64; + + if (compressedFile == null) { + print('⚠️ Compression failed - using original file as fallback'); + + // Fallback: use original image if compression fails + final originalSize = await file.length(); + final maxSize = 1048576; // 1MB + + if (originalSize > maxSize) { + print('❌ Original file is too large: ${originalSize / 1024} KB'); + if (mounted) { + Utils.showToast( + LocaleKeys.imageSizeTooLarge.tr(context: context), + ); + } + return; + } + + print('✅ Using original file (${originalSize / 1024} KB)'); + finalFile = file; + var bytes = await file.readAsBytes(); + finalBase64 = base64.encode(bytes); + } else { + // Check compressed file size + final fileSize = await compressedFile.length(); + final maxSize = 1048576; // 1MB + print('✅ Compression successful: ${fileSize / 1024} KB'); + + if (fileSize > maxSize) { + print('❌ Compressed file still too large'); + if (mounted) { + Utils.showToast( + LocaleKeys.imageSizeTooLarge.tr(context: context), + ); + } + return; + } + + finalFile = compressedFile; + var bytes = await compressedFile.readAsBytes(); + finalBase64 = base64.encode(bytes); + } + + print('Converting to base64... Length: ${finalBase64.length}'); + + if (mounted) { + setState(() { + _selectedImage = finalFile; + }); + + print('📤 Starting upload...'); + // Upload the image + _uploadImage(finalBase64); + } + + print('=== Image processing complete ==='); + } catch (e, stackTrace) { + print('❌ Error in _pickImage: $e'); + print('Stack trace: $stackTrace'); + if (mounted) { + Utils.showToast( + LocaleKeys.failedToProcessImage.tr(context: context), + ); + } + } + }, + checkCameraPermission: _checkCameraPermission, + checkGalleryPermission: _checkGalleryPermission, + ); + } + + Future _checkCameraPermission() async { + try { + print('=== Checking camera permission ==='); + + // First check current status + PermissionStatus currentStatus = await Permission.camera.status; + print('Current camera permission status: $currentStatus'); + + // If already granted, return true + if (currentStatus.isGranted) { + print('✅ Camera permission already granted'); + return true; + } + + // If denied or permanently denied, show settings dialog + if (currentStatus.isDenied || currentStatus.isPermanentlyDenied) { + // Request permission first + PermissionStatus newStatus = await Permission.camera.request(); + print('Camera permission after request: $newStatus'); + + if (newStatus.isGranted) { + print('✅ Camera permission granted'); + return true; + } + + // Still denied - show settings dialog + print('⚠️ Camera permission denied - showing settings dialog'); + if (mounted) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.cameraPermissionMessage.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + return false; + } + + // Request permission for the first time + PermissionStatus newStatus = await Permission.camera.request(); + print('Camera permission after request: $newStatus'); + + if (newStatus.isGranted) { + print('✅ Camera permission granted'); + return true; + } + + // Denied - show settings dialog + print('❌ Camera permission denied - showing settings dialog'); + if (mounted) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.cameraPermissionMessage.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + return false; + } catch (e) { + print('❌ Error checking camera permission: $e'); + if (mounted) { + Utils.showToast( + LocaleKeys.failedToCheckPermissions.tr(context: context), + ); + } + return false; + } + } + + Future _checkGalleryPermission() async { + try { + print('=== Checking gallery permission ==='); + + // Determine which permission to check based on platform and Android version + Permission galleryPermission; + + if (Platform.isIOS) { + galleryPermission = Permission.photos; + } else { + // Android: use photos permission which handles API level differences automatically + galleryPermission = Permission.photos; + } + + // First check current status + PermissionStatus currentStatus = await galleryPermission.status; + print('Current gallery permission status: $currentStatus'); + + // If already granted, return true + if (currentStatus.isGranted || currentStatus.isLimited) { + print('✅ Gallery permission already granted'); + return true; + } + + // If denied or permanently denied, request permission first + if (currentStatus.isDenied || currentStatus.isPermanentlyDenied) { + // Request permission first + PermissionStatus newStatus = await galleryPermission.request(); + print('Gallery permission after request: $newStatus'); + + if (newStatus.isGranted || newStatus.isLimited) { + print('✅ Gallery permission granted'); + return true; + } + + // Still denied - show settings dialog + print('⚠️ Gallery permission denied - showing settings dialog'); + if (mounted) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.galleryPermissionMessage.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + return false; + } + + // Request permission for the first time + PermissionStatus newStatus = await galleryPermission.request(); + print('Gallery permission after request: $newStatus'); + + if (newStatus.isGranted || newStatus.isLimited) { + print('✅ Gallery permission granted'); + return true; + } + + // Denied - show settings dialog + print('❌ Gallery permission denied - showing settings dialog'); + if (mounted) { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.galleryPermissionMessage.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + openAppSettings(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + return false; + } catch (e) { + print('❌ Error checking gallery permission: $e'); + if (mounted) { + Utils.showToast( + LocaleKeys.failedToCheckPermissions.tr(context: context), + ); + } + return false; + } + } + + void _uploadImage(String base64String) { + final patientID = appState.getAuthenticatedUser()?.patientId; + + if (patientID != null) { + setState(() { + _isUploadingImage = true; + }); + + profileSettingsViewModel.uploadProfileImage( + patientID: patientID, + imageData: base64String, + onSuccess: (data) { + if (mounted) { + setState(() { + _selectedImage = null; // Clear selected image after successful upload + _isUploadingImage = false; + }); + Utils.showToast( + LocaleKeys.profileImageUpdatedSuccessfully.tr(context: context), + ); + } + }, + onError: (error) { + if (mounted) { + setState(() { + _isUploadingImage = false; + }); + } + Utils.showToast(error); + }, + ); + } + } + double _calculateAspectRatio(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; @@ -96,6 +419,7 @@ class _FamilyCardsState extends State { widget.profileViewList!.addAll(widget.profiles); widget.profileViewList!.removeWhere((element) => element.responseId == appState.getAuthenticatedUser()?.patientId); insuranceViewModel = Provider.of(context, listen: false); + profileSettingsViewModel = Provider.of(context, listen: false); DialogService dialogService = getIt.get(); if (widget.isRequestDesign) { return Column( @@ -192,17 +516,61 @@ class _FamilyCardsState extends State { decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.r), child: Padding( padding: EdgeInsets.all(16.w), - child: Column( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - UserAvatarWidget( - width: 56.w, - height: 56.h, - fit: BoxFit.cover, - isCircular: true, + Stack( + children: [ + UserAvatarWidget( + width: 56.w, + height: 56.h, + fit: BoxFit.cover, + isCircular: true, + ), + // Camera/Edit button + Positioned( + right: 0, + bottom: 0, + child: GestureDetector( + onTap: () { + if (!_isUploadingImage) { + _pickImage(); + } + }, + child: Container( + width: 20.w, + height: 20.h, + decoration: BoxDecoration( + color: AppColors.primaryRedColor, + shape: BoxShape.circle, + border: Border.all( + color: AppColors.whiteColor, + width: 1.5.w, + ), + ), + child: _isUploadingImage + ? SizedBox( + width: 10.w, + height: 10.h, + child: CircularProgressIndicator( + strokeWidth: 1.5.w, + valueColor: AlwaysStoppedAnimation( + AppColors.whiteColor, + ), + ).paddingAll(4.w), + ) + : Icon( + Icons.camera_alt, + color: AppColors.whiteColor, + size: 10.w, + ), + ), + ), + ), + ], ), SizedBox(width: 8.w), Column( @@ -311,10 +679,10 @@ class _FamilyCardsState extends State { } }, backgroundColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor.withOpacity(0.1) + ? AppColors.primaryRedColor.withValues(alpha: 0.1) : insuranceVM.isInsuranceActive - ? AppColors.successColor.withOpacity(0.1) - : AppColors.warningColorYellow.withOpacity(0.1), + ? AppColors.successColor.withValues(alpha: 0.1) + : AppColors.warningColorYellow.withValues(alpha: 0.1), labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: insuranceVM.isInsuranceActive ? 6.w : 0.w), ).toShimmer2(isShow: insuranceVM.isInsuranceLoading); }), @@ -360,8 +728,9 @@ class _FamilyCardsState extends State { gender: profile.gender, age: profile.age, fit: BoxFit.cover, - customProfileImageData: "", // Prevent showing logged-in user's image for family members + customProfileImageData: profile.patientImageData, isCircular: true, + isFamilyMember: true, // Prevent AppState fallback ), SizedBox(height: 8.h), (profile.patientName ?? "Unknown").toText14(isBold: true, isCenter: true, maxlines: 1), diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index 377014ac..750ea8c4 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -422,7 +422,7 @@ class ProfileSettingsState extends State { } } -class FamilyCardWidget extends StatelessWidget { +class FamilyCardWidget extends StatefulWidget { final Function() onAddFamilyMemberPress; final Function(FamilyFileResponseModelLists member) onFamilySwitchPress; final FamilyFileResponseModelLists profile; @@ -434,12 +434,62 @@ class FamilyCardWidget extends StatelessWidget { required this.onFamilySwitchPress(FamilyFileResponseModelLists member), }); + @override + State createState() => _FamilyCardWidgetState(); +} + +class _FamilyCardWidgetState extends State { + @override + void initState() { + super.initState(); + // Sync ProfileSettingsViewModel with AppState when widget is created + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _syncProfileImageFromAppState(); + } + }); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Also sync when dependencies change (e.g., after user switch) + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _syncProfileImageFromAppState(); + } + }); + } + + void _syncProfileImageFromAppState() { + try { + final profileVm = context.read(); + final appState = getIt.get(); + + final appStateImageData = appState.getProfileImageData; + + // Sync profile image from AppState if it's different + if (appStateImageData != null && appStateImageData.isNotEmpty) { + if (profileVm.profileImageData != appStateImageData) { + print("🔄 Syncing profile image from AppState to ProfileSettingsViewModel"); + profileVm.syncProfileImageFromAppState(); + } + } else if (profileVm.profileImageData != null) { + // AppState is null but ViewModel has data - clear it + print("🧹 Clearing ProfileSettingsViewModel image data (AppState is null)"); + profileVm.clearProfileImageCache(); + } + } catch (e) { + print("⚠️ Error syncing profile image: $e"); + } + } + @override Widget build(BuildContext context) { AppState appState = getIt.get(); - final isActive = (profile.responseId == appState.getAuthenticatedUser()?.patientId); + final isActive = (widget.profile.responseId == appState.getAuthenticatedUser()?.patientId); // final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false; - // final canSwitch = isParentUser || (!isParentUser && profile.responseId == appState.getSuperUserID); + // final canSwitch = isParentUser || (!isParentUser && widget.profile.responseId == appState.getSuperUserID); return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, @@ -453,19 +503,44 @@ class FamilyCardWidget extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - UserAvatarWidget( - width: 56.w, - height: 56.h, - gender: profile.gender, - age: profile.age, - fit: BoxFit.cover, - isCircular: true, + // Wrap in Consumer to rebuild when ProfileSettingsViewModel changes + Consumer( + builder: (context, profileVm, _) { + final currentUserId = appState.getAuthenticatedUser()?.patientId; + final isCurrentUser = widget.profile.responseId == currentUserId; + + if (isCurrentUser) { + // Current user - use AppState directly (like home screen profile icon) + return UserAvatarWidget( + width: 56.w, + height: 56.h, + gender: widget.profile.gender, + age: widget.profile.age, + fit: BoxFit.cover, + // Don't pass customProfileImageData - let it use AppState + isCircular: true, + // Don't pass isFamilyMember - allow AppState fallback + ); + } else { + // Family member - use their specific image data + return UserAvatarWidget( + width: 56.w, + height: 56.h, + gender: widget.profile.gender, + age: widget.profile.age, + fit: BoxFit.cover, + customProfileImageData: widget.profile.patientImageData, + isCircular: true, + isFamilyMember: true, // Prevent AppState fallback + ); + } + }, ), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - "${profile.patientName}".toText18(isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 1, isEnglishOnly: true), + "${widget.profile.patientName}".toText18(isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 1, isEnglishOnly: true), Wrap( direction: Axis.horizontal, spacing: 4.w, @@ -474,7 +549,7 @@ class FamilyCardWidget extends StatelessWidget { AppCustomChipWidget( labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w), icon: AppAssets.file_icon, - labelText: "${LocaleKeys.fileno.tr(context: context)}: ${profile.responseId}", + labelText: "${LocaleKeys.fileno.tr(context: context)}: ${widget.profile.responseId}", iconSize: 12.w, ), isActive ? AppCustomChipWidget( @@ -488,7 +563,7 @@ class FamilyCardWidget extends StatelessWidget { ], ).expanded, // Icon(Icons.qr_code, size: 56.h) - Image.network("https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${profile.responseId.toString()}", fit: BoxFit.contain, height: 56.h, width: 56.w) + Image.network("https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${widget.profile.responseId.toString()}", fit: BoxFit.contain, height: 56.h, width: 56.w) ], ), SizedBox(height: 4.h), @@ -499,7 +574,7 @@ class FamilyCardWidget extends StatelessWidget { runSpacing: 4.h, children: [ AppCustomChipWidget( - labelText: LocaleKeys.ageYearsOld.tr(namedArgs: {'age': profile.age.toString(), 'yearsOld': LocaleKeys.yearsOld.tr(context: context)}), + labelText: LocaleKeys.ageYearsOld.tr(namedArgs: {'age': widget.profile.age.toString(), 'yearsOld': LocaleKeys.yearsOld.tr(context: context)}), ), // isActive && appState.getAuthenticatedUser()!.bloodGroup != null // ? @@ -598,12 +673,12 @@ class FamilyCardWidget extends StatelessWidget { } Widget _buildParentUserButton(int? currentUserId) { - final canSwitch = profile.responseId != currentUserId; + final canSwitch = widget.profile.responseId != currentUserId; return CustomButton( icon: canSwitch ? AppAssets.switch_user : AppAssets.add_family, text: canSwitch ? LocaleKeys.switchAccount.tr() : LocaleKeys.addANewFamilyMember.tr(), - onPressed: canSwitch ? () => onFamilySwitchPress(profile) : onAddFamilyMemberPress, + onPressed: canSwitch ? () => widget.onFamilySwitchPress(widget.profile) : widget.onAddFamilyMemberPress, backgroundColor: canSwitch ? AppColors.secondaryLightRedColor : AppColors.primaryRedColor, borderColor: canSwitch ? AppColors.secondaryLightRedColor : AppColors.primaryRedColor, textColor: canSwitch ? AppColors.primaryRedColor : Colors.white, @@ -614,7 +689,7 @@ class FamilyCardWidget extends StatelessWidget { } Widget _buildNonParentUserButton(int? superUserId) { - final canSwitchBack = superUserId != null && superUserId == profile.responseId; + final canSwitchBack = superUserId != null && superUserId == widget.profile.responseId; return CustomButton( icon: AppAssets.switch_user, @@ -623,7 +698,7 @@ class FamilyCardWidget extends StatelessWidget { borderColor: canSwitchBack ? AppColors.primaryRedColor : Colors.grey.shade200, textColor: canSwitchBack ? AppColors.whiteColor : AppColors.greyTextColor, iconColor: canSwitchBack ? AppColors.whiteColor : AppColors.greyTextColor, - onPressed: canSwitchBack ? () => onFamilySwitchPress(profile) : () {}, + onPressed: canSwitchBack ? () => widget.onFamilySwitchPress(widget.profile) : () {}, height: isFoldable ? 50.h : 40.h, fontSize: 14.f, ).paddingOnly(top: 12.h, right: 16.w, left: 16.w, bottom: 16.h); diff --git a/lib/presentation/profile_settings/widgets/family_card_widget.dart b/lib/presentation/profile_settings/widgets/family_card_widget.dart index 768fe538..fbd1d58b 100644 --- a/lib/presentation/profile_settings/widgets/family_card_widget.dart +++ b/lib/presentation/profile_settings/widgets/family_card_widget.dart @@ -56,8 +56,9 @@ class FamilyCardWidget extends StatelessWidget { gender: profile.gender, age: profile.age, fit: BoxFit.cover, - customProfileImageData: "", - isCircular: true,// Prevent showing logged-in user's image for family members + customProfileImageData: profile.patientImageData, + isCircular: true, + isFamilyMember: true, // Prevent AppState fallback ), Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart index 1b63fef3..b057591a 100644 --- a/lib/presentation/profile_settings/widgets/profile_picture_widget.dart +++ b/lib/presentation/profile_settings/widgets/profile_picture_widget.dart @@ -28,34 +28,100 @@ class ProfilePictureWidget extends StatefulWidget { class _ProfilePictureWidgetState extends State { final AppState _appState = getIt.get(); File? _selectedImage; + int? _currentPatientId; @override void initState() { super.initState(); + _currentPatientId = _appState.getAuthenticatedUser()?.patientId; + print('🎬 ProfilePictureWidget initState - patient: $_currentPatientId'); + // Use addPostFrameCallback to ensure widget is built before loading WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; final profileVm = context.read(); + final patientID = _appState.getAuthenticatedUser()?.patientId; - // Only sync if AppState has data and ViewModel doesn't - if (_appState.getProfileImageData != null && - _appState.getProfileImageData!.isNotEmpty && - (profileVm.profileImageData == null || profileVm.profileImageData!.isEmpty)) { - profileVm.syncProfileImageFromAppState(); + if (patientID == null) { + print('⚠️ No authenticated user found'); + return; } - // Only load if no data exists in both AppState and ViewModel - if ((_appState.getProfileImageData == null || _appState.getProfileImageData!.isEmpty) && - (profileVm.profileImageData == null || profileVm.profileImageData!.isEmpty)) { - _loadProfileImage(); + // Check if we have data in AppState that matches current user + final appStateImageData = _appState.getProfileImageData; + + if (appStateImageData != null && appStateImageData.isNotEmpty) { + // Sync to ViewModel if it doesn't have data + if (profileVm.profileImageData == null || profileVm.profileImageData!.isEmpty) { + print('🔄 Syncing AppState data to ViewModel'); + profileVm.syncProfileImageFromAppState(); + } + } else { + // No cached data - load from API + print('📥 No cached data - loading from API for patient: $patientID'); + _loadProfileImage(forceRefresh: false); } }); } - void _loadProfileImage() { - // Check if profile image is already loaded in AppState - if (_appState.getProfileImageData != null && _appState.getProfileImageData!.isNotEmpty) { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _checkAndUpdateUserImage(); + } + + @override + void didUpdateWidget(ProfilePictureWidget oldWidget) { + super.didUpdateWidget(oldWidget); + _checkAndUpdateUserImage(); + } + + void _checkAndUpdateUserImage() { + // Check if the authenticated user has changed (family member switch) + final currentPatientId = _appState.getAuthenticatedUser()?.patientId; + + if (currentPatientId != null && currentPatientId != _currentPatientId) { + print('🔄 User switched detected: $_currentPatientId -> $currentPatientId'); + + // Update patient ID IMMEDIATELY before any other operations + final oldPatientId = _currentPatientId; + _currentPatientId = currentPatientId; + + // Clear the old profile image data + try { + final profileVm = context.read(); + + print('🧹 Clearing cache for old user: $oldPatientId'); + profileVm.clearProfileImageCache(); + + print('📥 Loading profile image for new user: $currentPatientId'); + // Load the new user's profile image immediately + profileVm.getProfileImage( + patientID: currentPatientId, + forceRefresh: true, + onSuccess: (data) { + print('✅ Profile image loaded successfully for user: $currentPatientId'); + if (mounted) { + setState(() {}); // Force rebuild to show new data + } + }, + onError: (error) { + print('❌ Error loading profile image: $error'); + if (mounted) { + setState(() {}); // Force rebuild to show default avatar + } + }, + ); + } catch (e) { + print('❌ Error in _checkAndUpdateUserImage: $e'); + } + } + } + + void _loadProfileImage({bool forceRefresh = false}) { + // Check if profile image is already loaded in AppState (skip if forcing refresh) + if (!forceRefresh && _appState.getProfileImageData != null && _appState.getProfileImageData!.isNotEmpty) { // Image already loaded, no need to call API return; } @@ -64,12 +130,16 @@ class _ProfilePictureWidgetState extends State { final patientID = _appState.getAuthenticatedUser()?.patientId; if (patientID != null) { + print('📥 Loading profile image for patient: $patientID (forceRefresh: $forceRefresh)'); profileVm.getProfileImage( patientID: patientID, + forceRefresh: forceRefresh, onSuccess: (data) { + print('✅ Profile image loaded successfully'); // Image loaded successfully }, onError: (error) { + print('❌ Error loading profile image: $error'); // Error loading image }, ); @@ -383,8 +453,10 @@ class _ProfilePictureWidgetState extends State { } Widget _buildProfileImage(ProfileSettingsViewModel profileVm) { - final gender = _appState.getAuthenticatedUser()?.gender ?? 1; - final age = _appState.getAuthenticatedUser()?.age ?? 0; + // Always get fresh user data + final currentUser = _appState.getAuthenticatedUser(); + final gender = currentUser?.gender ?? 1; + final age = currentUser?.age ?? 0; // Determine the default image based on gender and age final String defaultImage; @@ -396,7 +468,7 @@ class _ProfilePictureWidgetState extends State { defaultImage = age < 7 ? AppAssets.babyGirlImg : AppAssets.femaleImg; } - // Show selected image if available + // Show selected image if available (only during upload) if (_selectedImage != null) { return ClipOval( child: Image.file( @@ -408,9 +480,12 @@ class _ProfilePictureWidgetState extends State { ); } - // Check both ViewModel and AppState for profile image + // Use ViewModel data if available, otherwise fall back to AppState + // This ensures we show the current logged-in user's image (same as homepage profile icon) final String? imageData = profileVm.profileImageData ?? _appState.getProfileImageData; + print('🖼️ Building profile image - has data: ${imageData != null && imageData.isNotEmpty}, patient: ${currentUser?.patientId}'); + // Show uploaded image if available if (imageData != null && imageData.isNotEmpty) { try { @@ -424,7 +499,7 @@ class _ProfilePictureWidgetState extends State { ), ); } catch (e) { - print('Error decoding profile image: $e'); + print('❌ Error decoding profile image: $e'); // If decoding fails, show default image return Image.asset( defaultImage, @@ -434,7 +509,8 @@ class _ProfilePictureWidgetState extends State { } } - // Show default image + // Show default image (no image data or user has no uploaded image) + print('📷 Showing default avatar for user ${currentUser?.patientId}'); return Image.asset( defaultImage, width: 136.w, @@ -444,6 +520,13 @@ class _ProfilePictureWidgetState extends State { @override Widget build(BuildContext context) { + // Check for user change on every build (handles navigation scenarios) + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _checkAndUpdateUserImage(); + } + }); + return Consumer( builder: (context, profileVm, _) { return Center( diff --git a/lib/presentation/radiology/radiology_orders_page.dart b/lib/presentation/radiology/radiology_orders_page.dart index 80f6aed5..e8c1cedc 100644 --- a/lib/presentation/radiology/radiology_orders_page.dart +++ b/lib/presentation/radiology/radiology_orders_page.dart @@ -21,6 +21,7 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../../features/radiology/radiology_view_model.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -439,12 +440,25 @@ class _RadiologyOrdersPageState extends State { SizedBox(height: 16.h), CustomButton( text: LocaleKeys.viewReport.tr(), - onPressed: () { - model.navigationService.push( - CustomPageRoute( - page: RadiologyResultPage(patientRadiologyResponseModel: group), - ), - ); + onPressed: () async { + // Check if reportData is empty or null + if (group.reportData == null || group.reportData!.trim().isEmpty) { + // Open image directly + await model.getRadiologyImage(patientRadiologyResponseModel: group); + if (model.radiologyImageURL.isNotEmpty) { + Uri uri = Uri.parse(model.radiologyImageURL); + await launchUrl(uri, mode: LaunchMode.platformDefault, webOnlyWindowName: ""); + } else { + Utils.showToast(LocaleKeys.noDataAvailable.tr()); + } + } else { + // Navigate to details page + model.navigationService.push( + CustomPageRoute( + page: RadiologyResultPage(patientRadiologyResponseModel: group), + ), + ); + } }, backgroundColor: AppColors.infoColor.withAlpha(20), borderColor: AppColors.infoColor.withAlpha(0), diff --git a/lib/presentation/symptoms_checker/risk_factors_screen.dart b/lib/presentation/symptoms_checker/risk_factors_screen.dart index 969a884d..49aa5150 100644 --- a/lib/presentation/symptoms_checker/risk_factors_screen.dart +++ b/lib/presentation/symptoms_checker/risk_factors_screen.dart @@ -125,7 +125,7 @@ class _RiskFactorsScreenState extends State { fontSize: 13.f, fontWeight: FontWeight.w600, color: AppColors.greyInfoTextColor, - fontFamily: isArabic ? 'GESSTwo' : 'Poppins' + fontFamily: isArabic ? 'CairoArabic' : 'Poppins' ), children: [ TextSpan( diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index 76fad545..c9291275 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -468,14 +468,14 @@ class _TriagePageState extends State { text: TextSpan( text: LocaleKeys.possibleSymptom.tr(context: context), style: TextStyle( - color: AppColors.greyTextColor, fontWeight: FontWeight.w600, fontSize: 14.f, fontFamily: isArabic ? 'GESSTwo' : 'Poppins'), + color: AppColors.greyTextColor, fontWeight: FontWeight.w600, fontSize: 14.f, fontFamily: isArabic ? 'CairoArabic' : 'Poppins'), children: [ TextSpan( text: suggestedCondition, style: TextStyle( color: AppColors.textColor, fontWeight: FontWeight.w600, - fontSize: 14.f, fontFamily: isArabic ? 'GESSTwo' : 'Poppins'), + fontSize: 14.f, fontFamily: isArabic ? 'CairoArabic' : 'Poppins'), ), ], ), @@ -491,7 +491,7 @@ class _TriagePageState extends State { text: TextSpan( text: "${probability.toStringAsFixed(1)}% ", style: TextStyle( - fontFamily: 'Poppins', + fontFamily: isArabic ? 'CairoArabic' : 'Poppins', color: AppColors.primaryRedColor, fontWeight: FontWeight.w600, fontSize: 14.f, @@ -503,7 +503,7 @@ class _TriagePageState extends State { color: AppColors.textColor, fontWeight: FontWeight.w600, fontSize: 13.f, - fontFamily: isArabic ? 'GESSTwo' : 'Poppins', + fontFamily: isArabic ? 'CairoArabic' : 'Poppins', ), ), ], diff --git a/lib/presentation/vital_sign/vital_sign_details_page.dart b/lib/presentation/vital_sign/vital_sign_details_page.dart index eb137e47..63b21291 100644 --- a/lib/presentation/vital_sign/vital_sign_details_page.dart +++ b/lib/presentation/vital_sign/vital_sign_details_page.dart @@ -196,7 +196,7 @@ class _VitalSignDetailsPageState extends State { style: TextStyle( fontSize: 12.f, fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', color: AppColors.greyTextColor, ), softWrap: true, @@ -335,7 +335,6 @@ class _VitalSignDetailsPageState extends State { Widget _buildHistoryGraph(List history, {List? secondaryHistory}) { final minY = _minY(history, secondaryHistory: secondaryHistory); final maxY = _maxY(history, secondaryHistory: secondaryHistory); - final scheme = VitalSignUiModel.scheme(status: _statusForLatest(null), label: args.title); final isRTL = getIt.get().isArabic(); return CustomGraph( @@ -344,44 +343,43 @@ class _VitalSignDetailsPageState extends State { makeGraphBasedOnActualValue: true, leftLabelReservedSize: 40, showGridLines: true, - showShadow: true, + showShadow: false, isRTL: isRTL, leftLabelInterval: _leftInterval(history, secondaryHistory: secondaryHistory), maxY: maxY, minY: minY, maxX: history.length.toDouble() - .75, - horizontalInterval: _leftInterval(history, secondaryHistory: secondaryHistory), + horizontalInterval: .01, leftLabelFormatter: (value) { - // Show only numeric labels at regular intervals - return _axisLabel(value.toStringAsFixed(0)); + // Format value to 2 decimal places for consistency + value = double.parse(value.toStringAsFixed(2)); + return _axisLabel(value.toStringAsFixed(2)); }, getDrawingHorizontalLine: (value) { - // Draw reference lines for high/low bounds + value = double.parse(value.toStringAsFixed(2)); + // Draw solid reference lines for high/low bounds (matching lab result style) if (args.high != null && (value - args.high!).abs() < 0.1) { return FlLine( - color: AppColors.bgGreenColor.withOpacity(0.2), + color: AppColors.bgGreenColor.withValues(alpha: 0.6), strokeWidth: 1, - dashArray: [5, 5], ); } if (args.low != null && (value - args.low!).abs() < 0.1) { return FlLine( - color: AppColors.bgGreenColor.withOpacity(0.2), + color: AppColors.bgGreenColor.withValues(alpha: 0.6), strokeWidth: 1, - dashArray: [5, 5], ); } - // Draw grid lines at intervals + // Hide other grid lines return FlLine( - color: AppColors.bgGreenColor.withOpacity(0.2), + color: Colors.transparent, strokeWidth: 1, - dashArray: [5, 5], ); }, - graphColor: AppColors.bgGreenColor, - secondaryGraphColor: AppColors.blueColor, - graphShadowColor: AppColors.lightGreenColor.withOpacity(.4), - graphGridColor: scheme.iconFg, + graphColor: AppColors.textColor.withValues(alpha: 90 / 255), + secondaryGraphColor: AppColors.textColor.withValues(alpha: 90 / 255), + graphShadowColor: AppColors.greyColor.withValues(alpha: 0.5), + graphGridColor: AppColors.textColor.withValues(alpha: 0.4), bottomLabelFormatter: (value, data) { if (data.isEmpty) return const SizedBox.shrink(); if (value == 0) return _bottomLabel(data[value.toInt()].label); @@ -497,32 +495,35 @@ class _VitalSignDetailsPageState extends State { final List ranges = []; + // Below low reference (transparent - matching lab result style) if (args.low != null) { ranges.add( HorizontalRangeAnnotation( y1: minY, y2: args.low!, - color: AppColors.highAndLow.withOpacity(0.05), + color: Colors.transparent, ), ); } + // Normal range (light green shading - matching lab result style) if (args.low != null && args.high != null) { ranges.add( HorizontalRangeAnnotation( y1: args.low!, y2: args.high!, - color: AppColors.bgGreenColor.withOpacity(0.05), + color: AppColors.bgGreenColor.withValues(alpha: 0.05), ), ); } + // Above high reference (transparent - matching lab result style) if (args.high != null) { ranges.add( HorizontalRangeAnnotation( y1: args.high!, y2: maxY, - color: AppColors.criticalLowAndHigh.withOpacity(0.05), + color: Colors.transparent, ), ); } @@ -755,7 +756,7 @@ class _VitalSignDetailsPageState extends State { value, style: TextStyle( fontWeight: FontWeight.w600, - fontFamily: 'Poppins', + fontFamily: getIt.get().isArabic() ? 'CairoArabic' : 'Poppins', fontSize: 8.f, color: AppColors.textColor, ), diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index e9cd83eb..1e39eac8 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -90,6 +90,16 @@ class AppColors { static Color get myChildVaccineCardColor => isDarkMode ? dark.myChildVaccineCardColor : const Color(0xFFFF2D78); static Color get bookAppointment => isDarkMode ? dark.bookAppointment : const Color(0xFF415364); + // ── Health Calculators Colors ───────────────────────────────────────────── + static Color get bmiCalculatorColor => isDarkMode ? dark.bmiCalculatorColor : const Color(0xFFF43333); + static Color get caloriesCalculatorColor => isDarkMode ? dark.caloriesCalculatorColor : const Color(0xFF0B85F7); + static Color get bmrCalculatorColor => isDarkMode ? dark.bmrCalculatorColor : const Color(0xFF18C273); + static Color get idealBodyWeightColor => isDarkMode ? dark.idealBodyWeightColor : const Color(0xFFFF8012); + static Color get bodyFatCalculatorColor => isDarkMode ? dark.bodyFatCalculatorColor : const Color(0xFFFF5662); + static Color get carbsProteinFatColor => isDarkMode ? dark.carbsProteinFatColor : const Color(0xFF18C273); + static Color get ovulationPeriodColor => isDarkMode ? dark.ovulationPeriodColor : const Color(0xFFFF47A9); + static Color get deliveryDueDateColor => isDarkMode ? dark.deliveryDueDateColor : const Color(0xFFFF2D78); + // ── Water Monitor ───────────────────────────────────────────────────────── static Color get blueColor => isDarkMode ? dark.blueColor : const Color(0xFF4EB5FF); static Color get blueGradientColorOne => isDarkMode ? dark.blueGradientColorOne : const Color(0xFFF1F7FD); @@ -217,6 +227,16 @@ class AppColorsDark { Color get myChildVaccineCardColor => const Color(0xFFFF2D78); Color get bookAppointment => const Color(0xFF415364); + // Health Calculators + Color get bmiCalculatorColor => const Color(0xFFF43333); + Color get caloriesCalculatorColor => const Color(0xFF0B85F7); + Color get bmrCalculatorColor => const Color(0xFF18C273); + Color get idealBodyWeightColor => const Color(0xFFFF8012); + Color get bodyFatCalculatorColor => const Color(0xFFFF5662); + Color get carbsProteinFatColor => const Color(0xFF18C273); + Color get ovulationPeriodColor => const Color(0xFFFF47A9); + Color get deliveryDueDateColor => const Color(0xFFFF2D78); + // Water Monitor Color get blueColor => const Color(0xFF4EB5FF); Color get blueGradientColorOne => const Color(0xFF101C2A); diff --git a/lib/widgets/common_bottom_sheet.dart b/lib/widgets/common_bottom_sheet.dart index d47971d7..267fb5c5 100644 --- a/lib/widgets/common_bottom_sheet.dart +++ b/lib/widgets/common_bottom_sheet.dart @@ -20,7 +20,12 @@ class BottomSheetUtils { showReminderBottomSheet(BuildContext context, DateTime dateTime, String doctorName, String eventId, String appoDateFormatted, String appoTimeFormatted, {required Function() onSuccess, String? title, String? description, Function(int)? onMultiDateSuccess, bool isMultiAllowed = false}) async { if (Platform.isAndroid) { - if (await PermissionService.isCalendarPermissionEnabled()) { + bool hasPermission = await PermissionService.isCalendarPermissionEnabled(); + if (!hasPermission) { + PermissionStatus status = await Permission.calendarFullAccess.request(); + hasPermission = status.isGranted; + } + if (hasPermission) { _showReminderBottomSheet(context, dateTime, doctorName, eventId, appoDateFormatted, appoTimeFormatted, onSuccess: onSuccess, title: title, description: description, onMultiDateSuccess: onMultiDateSuccess, isMultiAllowed: isMultiAllowed); } else { diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index a134ac79..d8a74d17 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -301,9 +301,11 @@ class TextInputWidget extends StatelessWidget { return Directionality( textDirection: isArabic ? TextDirection.rtl : TextDirection.ltr, child: Localizations.override( + context: context, locale: const Locale('en', 'US'), // Force English locale for TextField child: TextField( + hintLocales: const [Locale('en', 'US')], enabled: isEnable, scrollPadding: EdgeInsets.zero, @@ -344,6 +346,7 @@ class TextInputWidget extends StatelessWidget { isDense: true, hintText: hintText, hintStyle: TextStyle( + fontFamily: isArabic ? 'CairoArabic' : 'Poppins', fontSize: 14.f, height: 21 / 16, @@ -353,7 +356,7 @@ class TextInputWidget extends StatelessWidget { ), prefixIconConstraints: BoxConstraints(minWidth: 30.h), prefixIcon: prefix == null ? null : "+${prefix!}".toText14(letterSpacing: -1, color: AppColors.textColor, isBold: true), - contentPadding: EdgeInsets.zero, + contentPadding: EdgeInsets.only(right:isArabic ? 10.w :0), border: InputBorder.none, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, diff --git a/lib/widgets/user_avatar_widget.dart b/lib/widgets/user_avatar_widget.dart index 6132401c..3271adf4 100644 --- a/lib/widgets/user_avatar_widget.dart +++ b/lib/widgets/user_avatar_widget.dart @@ -14,6 +14,7 @@ class UserAvatarWidget extends StatelessWidget { final bool isCircular; final double? borderRadius; final String? customProfileImageData; // For family members or other users + final bool isFamilyMember; // New flag to prevent AppState fallback const UserAvatarWidget({ Key? key, @@ -25,6 +26,7 @@ class UserAvatarWidget extends StatelessWidget { this.isCircular = false, this.borderRadius, this.customProfileImageData, + this.isFamilyMember = false, // Default to false for backward compatibility }) : super(key: key); @override @@ -33,9 +35,17 @@ class UserAvatarWidget extends StatelessWidget { final userGender = gender ?? appState.getAuthenticatedUser()?.gender ?? 1; final userAge = age ?? appState.getAuthenticatedUser()?.age ?? 0; - // Use custom profile image data if provided, otherwise use AppState data - final profileImageData = customProfileImageData ?? appState.getProfileImageData; - + // Determine which profile image data to use: + // - For family members: ONLY use customProfileImageData (no AppState fallback) + // - For main user: Use customProfileImageData if provided, otherwise AppState + final String? profileImageData; + if (isFamilyMember) { + // Family member mode: only use their specific image data, no fallback + profileImageData = customProfileImageData; + } else { + // Main user mode: use custom data or fall back to AppState + profileImageData = customProfileImageData ?? appState.getProfileImageData; + } // Determine the default image based on gender and age final String defaultImage;