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 f491be4d..7294feb4 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1178,14 +1178,22 @@ "monitorCholesterolLevels": "راقب مستويات الكوليسترول، وقيّم مخاطر صحة القلب، واتخذ خطوات استباقية للرفاهية.", "triglyceridesFatBlood": "الدهون الثلاثية في الدم", "understandTriglyceridesImpact": "افهم تأثير الدهون الثلاثية على صحة القلب مع رؤى مخصصة وتوصيات الخبراء.", - "bmiCalculator": "حاسبة\nمؤشر كتلة الجسم", - "caloriesCalculator": "حاسبة\nالسعرات الحرارية", - "bmrCalculator": "حاسبة\nمعدل الأيض الأساسي", - "idealBodyWeight": "الوزن المثالي\nللجسم", - "bodyFatCalculator": "حاسبة\nدهون الجسم", - "carbsProteinFat": "الكربوهيدرات\nالبروتين والدهون", - "ovulationPeriod": "فترة\nالإباضة", - "deliveryDueDate": "تاريخ الولادة\nالمتوقع", + "bmiCalculator": "حاسبة مؤشر كتلة الجسم", + "bmiCalculatorDesc": "احسب مؤشر كتلة جسمك لفهم نطاق وزنك الصحي.", + "caloriesCalculator": "حاسبةالسعرات الحرارية", + "caloriesCalculatorDesc": "اكتشف عدد السعرات الحرارية التي تحتاجها يومياً للحفاظ على الوزن أو فقدانه.", + "bmrCalculator": "حاسبة معدل الأيض الأساسي", + "bmrCalculatorDesc": "حدد معدل الأيض الأساسي ومتطلباتك اليومية من الطاقة.", + "idealBodyWeight": "الوزن المثالي للجسم", + "idealBodyWeightDesc": "اكتشف وزنك المثالي بناءً على الطول والعمر وبنية الجسم.", + "bodyFatCalculator": "حاسبة دهون الجسم", + "bodyFatCalculatorDesc": "قدّر نسبة دهون جسمك لتتبع أهداف اللياقة والصحة.", + "carbsProteinFat": "الكربوهيدرات البروتين والدهون", + "carbsProteinFatDesc": "وازن بين العناصر الغذائية مع النسب الصحيحة من الكربوهيدرات والبروتين والدهون.", + "ovulationPeriod": "فترة الإباضة", + "ovulationPeriodDesc": "تتبع فترة خصوبتك وخطط للحمل بدقة.", + "deliveryDueDate": "تاريخ الولادة المتوقع", + "deliveryDueDateDesc": "احسب تاريخ ولادتك المتوقع وراقب مراحل الحمل.", "low": "منخفض", "preDiabetic": "ما قبل السكري", "high": "مرتفع", diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index d9773a81..9305c378 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/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/features/medical_file/medical_file_view_model.dart b/lib/features/medical_file/medical_file_view_model.dart index b94d1103..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(); } 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/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 43248236..2b55cfc1 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -2910,14 +2910,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 64577cbb..10528bb3 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1176,13 +1176,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 78532cce..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) @@ -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_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index 293f58ac..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( @@ -146,39 +154,39 @@ class AppointmentDoctorCard extends StatelessWidget { ? 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/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/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/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 {