diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 302c188e..176f1e5f 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -219,7 +219,7 @@ "unit": "الوحدة", "request": "الطلبات", "memberName": "اسم العضو", - "switchLogin": "عرض", + "switchLogin": "عرض الملف", "removeMember": "إزالة", "allowView": "السماح", "rejectView": "رفض", @@ -1349,7 +1349,7 @@ "monthlyHealthSummaryReportDisclaimer": "يعكس تقرير الملخص الصحي الشهري هذا المؤشرات الصحية ونتائج التحليل لأحدث الزيارات. يرجى ملاحظة أن هذا سيتم إرساله تلقائيًا من النظام ولا يعتبر تقريرًا رسميًا لذا لا ينبغي اتخاذ أي قرار طبي بناءً عليه", "updatingMonthlyReportStatus": "جاري تحديث حالة التقرير الشهري...", "monthlyReportStatusUpdatedSuccessfully": "تم تحديث حالة التقرير الشهري بنجاح", - "whoCanViewMyMedicalFile": "من يمكنه عرض ملفي الطبي؟", + "whoCanViewMyMedicalFile": "من يمكنه الاطلاع على ملفي الطبي", "acceptedYourRequestToBeYourFamilyMember": "{status} طلب الانضمام الى أفراد عائلتك", "canViewYourFile": "يمكنه عرض ملفك", "hasARequestPendingToBeYourFamilyMember": "لديه طلب {status} ليكون فردًا من عائلتك", @@ -1360,7 +1360,7 @@ "selectAProfile": "الرجاء تحديد ملف تعريف", "switchFamilyFile": "قم بالتبديل من قائمة الملفات الطبية أدناه", "medicalFiles": "الملفات الطبية", - "addANewFamilyMember": "إضافة فرد جديد", + "addANewFamilyMember": "اضافة", "viewInvoiceDetails": "عرض تفاصيل الفاتورة", "outPatient": "مريض خارجي", "invoiceDetails": "تفاصيل الفاتورة", @@ -1664,6 +1664,7 @@ "sortByLocation": "الترتيب حسب الموقع", "timeForFirstReminder": "اضافةتذكير", "reminderRemovalNote": "يمكنك إلغاء التذكير لاحقاً من التقويم.", + "prescriptionDatePassedError": "تاريخ الوصفة الطبية قد انتهى. لا يمكنك إضافة تذكير لهذه الوصفة.", "communicationLanguage": "لغة التواصل", "cmcServiceHeader": "فحص صحي شامل: تشخيص متقدم، معلومات صحية مفصلة", "cmcServiceDescription": "احصل على معلومات تفصيلية عن صحتك من خلال خدمات التشخيص المتقدمة لدينا. افهم جسمك بشكل أفضل لمستقبل صحي.", @@ -1702,7 +1703,7 @@ "doctorRemovedFromFavourite": "تمت إزالة من قائمة الأطباء المفضلين", "addNewFavorite": "إضافة مفضلة جديدة", "favourites": "المفضلة", - "pendingAncillaryOrders": "مدفوعات طلبات الاضافية", + "pendingAncillaryOrders": "طلبات غير مكتمله", "liveCarePermissions": "يتطلب لايف كير أذونات الكاميرا والميكروفون وإشعارات، يرجى السماح لها بالمتابعة.", "containsImage": "يحتوي على صورة", "servicesSelected": "الخدمة / الخدمات", @@ -1810,5 +1811,18 @@ "watchUsOnYoutube": "شاهدنا على إنستغرام", "connectOnLinkedin": "تواصل معنا على لينكد إن", "riskFactorPageTitle": "اعراض تشعر بها", - "symptomsCheckSuggestions": "الاحتمالات" + "bloodDonationService": "التبرع \n بالدم", + "bookAppointmentService": "حجز\nموعد", + "pinchToZoom": "اضغط للتكبير", + "gotIt": "فهمت", + "zoomIntoDetails": "قم بالتكبير لرؤية التفاصيل", + "pinchWithTwoFingersToZoom": "اضغط بإصبعين للتكبير", + "dragToMoveAround": "اسحب للتحرك", + "startExploring": "ابدأ الاستكشاف", + "symptomsCheckSuggestions": "الاحتمالات", + "appointmentArrived": "تم الحضور", + "calendarPermissionAlertForAppointment": "يرجى منح إذن الوصول إلى التقويم من إعدادات التطبيق لضبط تذكير الموعد", +"rechargePageKey": "اختر المستفيد", + "selectFamilyFile": "اختيار ", + "ancillaryOrdersListNew": "طلبات غير مكتملة" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 3c9facad..f23e12fb 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1652,6 +1652,7 @@ "calendarPermissionAlert": "Please grant calendar access permission from app settings to set medication reminder.", "timeForFirstReminder": "Add Reminder", "reminderRemovalNote": "You can remove it from your calendar later by switching off the reminder", + "prescriptionDatePassedError": "Prescription date has already passed. You cannot add a reminder for this prescription.", "sortByLocation": "Sort by location", "sortByNearestLocation": "Sort by nearest to your location", "giveLocationPermissionForNearestList": "Please grant location permission from app settings to see the nearest locations.", @@ -1800,7 +1801,20 @@ "watchUsOnYoutube": "Watch us on Instagram", "connectOnLinkedin": "Connect on LinkedIn", "riskFactorPageTitle": "What do you feel?", - "symptomsCheckSuggestions": "Suggestions" + "bloodDonationService": "Blood Donation", + "bookAppointmentService": "Book\nAppointment", + "pinchToZoom": "Pinch to zoom", + "gotIt": "Got it", + "zoomIntoDetails": "Zoom into details", + "pinchWithTwoFingersToZoom": "Pinch with two fingers to zoom", + "dragToMoveAround": "Drag to move around", + "startExploring": "Start exploring", + "symptomsCheckSuggestions": "Suggestions", +"appointmentArrived": "Arrived", + "calendarPermissionAlertForAppointment": "Please grant calendar access permission from app settings to set appointment reminder.", + "rechargePageKey": "Recharge Wallet", + "selectFamilyFile": "Select", + "ancillaryOrdersListNew": "Ancillary Orders List" } diff --git a/lib/core/cache_consts.dart b/lib/core/cache_consts.dart index c829adbf..118fed3b 100644 --- a/lib/core/cache_consts.dart +++ b/lib/core/cache_consts.dart @@ -80,4 +80,5 @@ class CacheConst { static const String zoomRoomID = 'zoom-room-id'; static const String callTypeID = 'call-type-id'; static String isAppOpenedFromCall = "is_app_opened_from_call"; + static const String organSelectorTutorialShown = 'organ_selector_tutorial_shown'; } diff --git a/lib/core/utils/calender_utils_new.dart b/lib/core/utils/calender_utils_new.dart index 59c0e170..d5e6b446 100644 --- a/lib/core/utils/calender_utils_new.dart +++ b/lib/core/utils/calender_utils_new.dart @@ -126,8 +126,7 @@ class CalenderUtilsNew { 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 startTimeOfDay = 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); @@ -139,28 +138,29 @@ class CalenderUtilsNew { // Validate that prescription is still active if (remainingDays <= 0) { - onFailure?.call("Prescription date has already passed. You cannot add a reminder for this prescription."); + onFailure?.call(LocaleKeys.prescriptionDatePassedError.tr()); return false; } bool statusOfOperation = false; for (int i = 0; i < remainingDays; i++) { - //event for number of days. + // Reset to the original time for each new day + DateTime actualDate = startTimeOfDay.add(Duration(days: i)); + + //event for number of days (frequency per day) for (int j = 0; j < frequencyNumber; j++) { statusOfOperation = await createOrUpdateEvent( - title: "$itemDescriptionN} Medication about to due for prescription , #$prescriptionNumber", + title: "$itemDescriptionN Medication about to due for prescription, #$prescriptionNumber", description: "$itemDescriptionN $frequencyNumber $route ", scheduleDateTime: actualDate, location: '', - //event id with varitions + //event id with variations reminderMinutes: reminderMinutes); if (!statusOfOperation) return false; actualDate = actualDate.add(Duration(hours: interval)); - // if (actualDate.difference(currentDay).inDays == 1) break; } - actualDate = actualDate.add(Duration(days: 1)); } return statusOfOperation; @@ -171,14 +171,17 @@ class CalenderUtilsNew { } FutureOr> getEvents() async { - var availableCalender = writableCalender.first; + var availableCalender = writableCalender.first; DateTime startEventsDate = Jiffy.parseFromDateTime(DateTime.now()).subtract(days: 30).dateTime; DateTime endEventsDate = Jiffy.parseFromDateTime(DateTime.now()).add(days: 120).dateTime; return await calender.listEvents(startEventsDate, endEventsDate, calendarIds: [availableCalender.id]); } FutureOr checkIfEventExist(String admissionId) async { - await ensureCalendarsLoaded(); + // await ensureCalendarsLoaded(); + if (writableCalender.isEmpty) { + await ensureCalendarsLoaded(); + } List events = await getEvents(); if (events.isEmpty) return false; for (var event in events) { diff --git a/lib/features/blood_donation/blood_donation_view_model.dart b/lib/features/blood_donation/blood_donation_view_model.dart index 7aa67d4f..3da82a75 100644 --- a/lib/features/blood_donation/blood_donation_view_model.dart +++ b/lib/features/blood_donation/blood_donation_view_model.dart @@ -231,30 +231,31 @@ class BloodDonationViewModel extends ChangeNotifier { } Future validateSelections() async { + AppState appState = getIt(); if (selectedCity == null) { await dialogService.showErrorBottomSheet( - message: "Please choose city", + message: appState.isArabic() ? "اختر المدينة" :"Please choose city", ); return false; } if (selectedGender == null) { await dialogService.showErrorBottomSheet( - message: "Please choose Gender", + message: appState.isArabic() ? "اختير الجنس" :"Please choose Gender", ); return false; } if (selectedBloodType == null) { await dialogService.showErrorBottomSheet( - message: "Please choose Blood Group", + message: appState.isArabic() ? "اختير فصيلة الدم" :"Please choose Blood Group", ); return false; } if (!isTermsAccepted) { await dialogService.showErrorBottomSheet( - message: "Please accept Terms and Conditions to continue", + message:appState.isArabic() ? "يرجى الموافقة على الشروط والأحكام للمتابعة" : "Please accept Terms and Conditions to continue", ); return false; } diff --git a/lib/features/my_appointments/utils/appointment_type.dart b/lib/features/my_appointments/utils/appointment_type.dart index ae3c7516..13811737 100644 --- a/lib/features/my_appointments/utils/appointment_type.dart +++ b/lib/features/my_appointments/utils/appointment_type.dart @@ -37,7 +37,7 @@ class AppointmentType { case 42: return LocaleKeys.confirmed.tr(); case 43: - return LocaleKeys.arrived.tr(); + return LocaleKeys.appointmentArrived.tr(); default: return LocaleKeys.booked.tr(); } diff --git a/lib/features/prescriptions/prescriptions_view_model.dart b/lib/features/prescriptions/prescriptions_view_model.dart index c374b3e2..577c68a1 100644 --- a/lib/features/prescriptions/prescriptions_view_model.dart +++ b/lib/features/prescriptions/prescriptions_view_model.dart @@ -74,7 +74,7 @@ class PrescriptionsViewModel extends ChangeNotifier { Future checkIfReminderExistForPrescription(int index) async { prescriptionDetailsList[index].hasReminder = await CalenderUtilsNew.instance.checkIfEventExist(prescriptionDetailsList[index].itemID?.toString() ?? ""); - notifyListeners(); + // notifyListeners(); return prescriptionDetailsList[index].hasReminder ?? false; } @@ -226,7 +226,11 @@ class PrescriptionsViewModel extends ChangeNotifier { } showError(String errorMessage) { - getIt().showErrorBottomSheet(message: errorMessage); + getIt().pop(); + Future.delayed(Duration(milliseconds: 500), (){ + getIt().showErrorBottomSheet(message: errorMessage); + }); + } Future submitPrescriptionDeliveryRequest( diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index a90fa9b5..9ed02e8a 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1658,6 +1658,7 @@ abstract class LocaleKeys { static const sortByLocation = 'sortByLocation'; static const timeForFirstReminder = 'timeForFirstReminder'; static const reminderRemovalNote = 'reminderRemovalNote'; + static const prescriptionDatePassedError = 'prescriptionDatePassedError'; static const communicationLanguage = 'communicationLanguage'; static const cmcServiceHeader = 'cmcServiceHeader'; static const cmcServiceDescription = 'cmcServiceDescription'; @@ -1803,5 +1804,10 @@ abstract class LocaleKeys { static const connectOnLinkedin = 'connectOnLinkedin'; static const riskFactorPageTitle = 'riskFactorPageTitle'; static const symptomsCheckSuggestions = 'symptomsCheckSuggestions'; + static const appointmentArrived = 'appointmentArrived'; + static const calendarPermissionAlertForAppointment = 'calendarPermissionAlertForAppointment'; + static const rechargePageKey = 'rechargePageKey'; + static const selectFamilyFile = 'selectFamilyFile'; + static const ancillaryOrdersListNew = 'ancillaryOrdersListNew'; } diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index 974c6019..87ca5e93 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -478,6 +478,7 @@ class _AppointmentDetailsPageState extends State { myAppointmentsViewModel.setAppointmentReminder(isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel); }); }, + isForAppointment: true ); // If permission was not granted, revert the switch back to OFF diff --git a/lib/presentation/e_referral/new_e_referral.dart b/lib/presentation/e_referral/new_e_referral.dart index 53c91680..1d34b38c 100644 --- a/lib/presentation/e_referral/new_e_referral.dart +++ b/lib/presentation/e_referral/new_e_referral.dart @@ -131,9 +131,13 @@ class _NewReferralPageState extends State { hmgServicesVM.createEReferral( requestModel: createReferralRequestModel, - onSuccess: (GenericApiModel response) { + onSuccess: (GenericApiModel response) async{ LoaderBottomSheet.hideLoader(); showSuccessBottomSheet(int.parse(response.data), hmgServicesVM); + await Future.delayed(Duration(milliseconds: 2000)).then((value) { + if (mounted) Navigator.pop(context); + }); + }, onError: (errorMessage) { // Handle error (e.g., show error message) diff --git a/lib/presentation/habib_wallet/recharge_wallet_page.dart b/lib/presentation/habib_wallet/recharge_wallet_page.dart index c671152f..538317bd 100644 --- a/lib/presentation/habib_wallet/recharge_wallet_page.dart +++ b/lib/presentation/habib_wallet/recharge_wallet_page.dart @@ -62,7 +62,7 @@ class _RechargeWalletPageState extends State { children: [ Expanded( child: CollapsingListView( - title: LocaleKeys.createAdvancedPayment.tr(context: context), + title: LocaleKeys.rechargePageKey.tr(context: context), child: SingleChildScrollView( child: Padding( padding: EdgeInsets.all(24.h), diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index 9ad0fa31..4ea290b0 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -88,7 +88,7 @@ class _ServicesPageState extends State { // await getIt.get().onLoginPressed(); // } }), - HmgServicesComponentModel(11, LocaleKeys.bookAppointment.tr(), "", AppAssets.appointment_calendar_icon, bgColor: AppColors.bookAppointment, true, route: null, onTap: () { + HmgServicesComponentModel(11, LocaleKeys.bookAppointmentService.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())); }), @@ -169,7 +169,7 @@ class _ServicesPageState extends State { }), HmgServicesComponentModel( 3, - LocaleKeys.bloodDonation.tr(context: getIt.get().navigatorKey.currentContext!), + LocaleKeys.bloodDonationService.tr(context: getIt.get().navigatorKey.currentContext!), "".needTranslation, AppAssets.blood_donation_icon, bgColor: AppColors.bloodDonationCardColor, @@ -632,7 +632,7 @@ class _ServicesPageState extends State { iconSize: 24.h, iconColor: AppColors.primaryRedColor, textColor: AppColors.primaryRedColor, - text: LocaleKeys.addMember.tr(), + text: getIt.get().isArabic() ? LocaleKeys.add.tr() :LocaleKeys.addMember.tr(), borderWidth: 0.w, isBold: true, borderColor: Colors.transparent, diff --git a/lib/presentation/hmg_services/services_view.dart b/lib/presentation/hmg_services/services_view.dart index 7192ce23..906c23f6 100644 --- a/lib/presentation/hmg_services/services_view.dart +++ b/lib/presentation/hmg_services/services_view.dart @@ -57,6 +57,7 @@ class ServiceGridViewItem extends StatelessWidget { isBold: true, color: AppColors.textColor, maxLine: 2, + ), ], )); diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 6c1e74c2..86807150 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -1219,7 +1219,7 @@ class _MedicalFilePageState extends State { ); }), MedicalFileCard( - label: LocaleKeys.ancillaryOrdersList.tr(context: context), + label: LocaleKeys.ancillaryOrdersListNew.tr(context: context), textColor: AppColors.blackColor, backgroundColor: AppColors.whiteColor, svgIcon: AppAssets.ancillary_orders_list_icon, diff --git a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart index a67a6765..393dc3f6 100644 --- a/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart +++ b/lib/presentation/medical_file/widgets/medical_file_appointment_card.dart @@ -178,13 +178,15 @@ class _MedicalFileAppointmentCardState extends State ], ), SizedBox(height: 12.h), - Row( - children: [ - widget.myAppointmentsViewModel.isMyAppointmentsLoading - ? Container().toShimmer2(isShow: true, height: 40.h, width: 100.w, radius: 12.r) - : Expanded( - flex: 7, - child: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) + // Check if doctor is active - if not, show only View Details button + (widget.patientAppointmentHistoryResponseModel.isActiveDoctor ?? true) + ? // Doctor is active - check rebooking logic + (AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) && + widget.patientAppointmentHistoryResponseModel.isClinicReBookingAllowed == false) + ? // Show only the button without arrow when rebooking not allowed + widget.myAppointmentsViewModel.isMyAppointmentsLoading + ? Container().toShimmer2(isShow: true, height: 40.h, width: 100.w, radius: 12.r) + : AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) ? getArrivedAppointmentButton(context).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading) : CustomButton( text: AppointmentType.getNextActionText(widget.patientAppointmentHistoryResponseModel.nextAction), @@ -202,51 +204,101 @@ class _MedicalFileAppointmentCardState extends State icon: AppointmentType.getNextActionIcon(widget.patientAppointmentHistoryResponseModel.nextAction), iconColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction), iconSize: 14.h, - ).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading), - ), - SizedBox(width: 8.w), - ((((widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) || - (widget.patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) || - !Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID ?? 0))) && - AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) - ? SizedBox.shrink() - : Expanded( - flex: 2, - child: Container( - height: 40.h, - width: 40.w, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.textColor, - borderRadius: 10.r, - ), - child: Padding( - padding: EdgeInsets.all(10.w), - child: Transform.flip( - flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - iconColor: AppColors.whiteColor, - icon: AppAssets.forward_arrow_icon_small, - width: 40.w, - height: 40.h, - fit: BoxFit.contain, + ).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading) + : // Normal flow - show button with arrow + Row( + children: [ + widget.myAppointmentsViewModel.isMyAppointmentsLoading + ? Container().toShimmer2(isShow: true, height: 40.h, width: 100.w, radius: 12.r) + : Expanded( + flex: 7, + child: AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) + ? getArrivedAppointmentButton(context).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading) + : CustomButton( + text: AppointmentType.getNextActionText(widget.patientAppointmentHistoryResponseModel.nextAction), + onPressed: () { + handleAppointmentNextAction(widget.patientAppointmentHistoryResponseModel.nextAction, context); + }, + backgroundColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.15), + borderColor: AppointmentType.getNextActionButtonColor(widget.patientAppointmentHistoryResponseModel.nextAction).withValues(alpha: 0.01), + textColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction), + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + icon: AppointmentType.getNextActionIcon(widget.patientAppointmentHistoryResponseModel.nextAction), + iconColor: AppointmentType.getNextActionTextColor(widget.patientAppointmentHistoryResponseModel.nextAction), + iconSize: 14.h, + ).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading), + ), + SizedBox(width: 8.w), + ((((widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) || + (widget.patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) || + !Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID ?? 0))) && + AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) + ? SizedBox.shrink() + : Expanded( + flex: 2, + child: Container( + height: 40.h, + width: 40.w, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.textColor, + borderRadius: 10.r, + ), + child: Padding( + padding: EdgeInsets.all(10.w), + child: Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets( + iconColor: AppColors.whiteColor, + icon: AppAssets.forward_arrow_icon_small, + width: 40.w, + height: 40.h, + fit: BoxFit.contain, + ), + ), + ), + ).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading).onPress(() { + Navigator.of(context) + .push( + CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + ), + ) + .then((val) { + // widget.myAppointmentsViewModel.initAppointmentsViewModel(); + // widget.myAppointmentsViewModel.getPatientAppointments(true, false); + }); + }), + ), + ], + ) + : // Doctor is not active - show only View Details button + CustomButton( + text: LocaleKeys.viewDetails.tr(context: context), + onPressed: () { + Navigator.of(context) + .push( + CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), ), - ), - ), - ).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading).onPress(() { - Navigator.of(context) - .push( - CustomPageRoute( - page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), - ), - ) - .then((val) { - // widget.myAppointmentsViewModel.initAppointmentsViewModel(); - // widget.myAppointmentsViewModel.getPatientAppointments(true, false); - }); - }), - ), - ], - ), + ) + .then((_) { + widget.myAppointmentsViewModel.initAppointmentsViewModel(); + widget.myAppointmentsViewModel.getPatientAppointments(true, false); + }); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: (isFoldable || isTablet) ? 12.f : 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + ).toShimmer2(isShow: widget.myAppointmentsViewModel.isMyAppointmentsLoading), ], ).paddingAll(16.w), ), @@ -255,6 +307,34 @@ class _MedicalFileAppointmentCardState extends State } Widget getArrivedAppointmentButton(BuildContext context) { + // Check if rebooking is not allowed - show View Details button + if (widget.patientAppointmentHistoryResponseModel.isClinicReBookingAllowed == false) { + return CustomButton( + text: LocaleKeys.viewDetails.tr(context: context), + onPressed: () { + Navigator.of(context) + .push( + CustomPageRoute( + page: AppointmentDetailsPage(patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel), + ), + ) + .then((_) { + widget.myAppointmentsViewModel.initAppointmentsViewModel(); + widget.myAppointmentsViewModel.getPatientAppointments(true, false); + }); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: (isFoldable || isTablet) ? 12.f : 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + ); + } + + // Check if clinic allows rebooking or if doctor is excluded if ((((widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) || (widget.patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) || !Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID ?? 0))) && diff --git a/lib/presentation/prescriptions/prescription_detail_page.dart b/lib/presentation/prescriptions/prescription_detail_page.dart index a13dbcdd..01cee861 100644 --- a/lib/presentation/prescriptions/prescription_detail_page.dart +++ b/lib/presentation/prescriptions/prescription_detail_page.dart @@ -9,12 +9,14 @@ import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/prescriptions/models/resp_models/patient_prescriptions_response_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_item_view.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'; @@ -237,7 +239,8 @@ class _PrescriptionDetailPageState extends State { ), ), ), - Container( + Column( + children: [ Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 24.h, @@ -268,8 +271,55 @@ class _PrescriptionDetailPageState extends State { icon: AppAssets.prescription_refill_icon, iconColor: widget.prescriptionsResponseModel.isHomeMedicineDeliverySupported! ? Colors.white : AppColors.textColor.withOpacity(0.35), iconSize: 20.h, - ).paddingSymmetrical(24.h, 24.h), + ).paddingSymmetrical(24.h, 0.h), ), + Utils.havePrivilege(119) ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + child: Container( + height: 56.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + stops: [0.236, 1.0], // 53.6% and 100% + colors: [ + Color(0xFF8A38F5), // Transparent + Color(0xFFE20BBB), // Solid #F8F8F8 + ], + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.only(right: 4.w, left: 4.w), + child: Utils.buildSvgWithAssets(icon: AppAssets.aiOverView, width: 16.h, height: 16.h, iconColor: Colors.white), + ), + LocaleKeys.generateAiAnalysisResult.tr(context: context).toText16(isBold: true, color: Colors.white) + ], + ), + ).paddingSymmetrical(24.h, 24.h).onPress(() async { + final _dialogService = getIt.get(); + await _dialogService.showCommonBottomSheetWithoutH( + message: LocaleKeys.aiDisclaimer.tr(), + label: LocaleKeys.consent.tr(), + okLabel: LocaleKeys.acceptLbl.tr(), + cancelLabel: LocaleKeys.rejectView.tr(), + onOkPressed: () { + + }, + onCancelPressed: () { + context.pop(); + }); + }), + ) : SizedBox(height: 24,) , + ]) ], ), ); diff --git a/lib/presentation/prescriptions/prescription_item_view.dart b/lib/presentation/prescriptions/prescription_item_view.dart index 66ba13ed..e6f08496 100644 --- a/lib/presentation/prescriptions/prescription_item_view.dart +++ b/lib/presentation/prescriptions/prescription_item_view.dart @@ -1,3 +1,5 @@ +import 'dart:io' show Platform; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -8,12 +10,14 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/services/permission_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/scroll_wheel_time_picker.dart'; +import 'package:permission_handler/permission_handler.dart'; class PrescriptionItemView extends StatelessWidget { int index; @@ -233,4 +237,71 @@ class PrescriptionItemView extends StatelessWidget { ], ); } + + /// Checks and requests calendar permissions for both Android and iOS + /// Returns true if all required permissions are granted + Future _checkAndRequestCalendarPermissions() async { + if (Platform.isAndroid) { + // Android: Check existing permission first using PermissionService + bool hasPermission = await PermissionService.isCalendarPermissionEnabled(); + if (hasPermission) { + return true; + } + + // Request calendarFullAccess permission (Android 14+ requires this) + // This internally requests both READ_CALENDAR and WRITE_CALENDAR + PermissionStatus status = await Permission.calendarFullAccess.request(); + + // If calendarFullAccess request didn't show dialog or failed, + // fallback to requesting individual permissions + if (!status.isGranted) { + Map statuses = await [ + Permission.calendarWriteOnly, + ].request(); + + return statuses[Permission.calendarWriteOnly]?.isGranted ?? false; + } + + return status.isGranted; + } else { + // iOS: Check if we already have full access + PermissionStatus fullAccessStatus = await Permission.calendarFullAccess.status; + + if (fullAccessStatus.isGranted) { + return true; + } + + // Request write permission first (required before full access on iOS) + PermissionStatus writeStatus = await Permission.calendarWriteOnly.request(); + if (!writeStatus.isGranted) { + return false; + } + + // Then request full access permission + PermissionStatus fullStatus = await Permission.calendarFullAccess.request(); + return fullStatus.isGranted; + } + } + + /// Shows permission denied dialog with option to open settings + Future _showPermissionDeniedDialog(BuildContext context) async { + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.notice.tr(context: context), + child: Utils.getWarningWidget( + loadingText: LocaleKeys.calendarPermissionAlert.tr(), + isShowActionButtons: true, + onCancelTap: () { + Navigator.of(context).pop(); + }, + onConfirmTap: () async { + Navigator.of(context).pop(); + await openAppSettings(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } } diff --git a/lib/presentation/symptoms_checker/organ_selector_screen.dart b/lib/presentation/symptoms_checker/organ_selector_screen.dart index ea85b3b5..93e826df 100644 --- a/lib/presentation/symptoms_checker/organ_selector_screen.dart +++ b/lib/presentation/symptoms_checker/organ_selector_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; @@ -11,6 +12,8 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/interactive_body_widget.dart'; +import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart'; +import 'package:hmg_patient_app_new/services/cache_service.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/buttons/custom_button.dart'; @@ -28,12 +31,36 @@ class OrganSelectorPage extends StatefulWidget { class _OrganSelectorPageState extends State { late final AppState _appState; late final DialogService dialogService; + late final CacheService cacheService; + bool _showTutorial = false; @override void initState() { super.initState(); _appState = getIt.get(); dialogService = getIt(); + cacheService = getIt(); + _checkAndShowTutorial(); + } + + Future _checkAndShowTutorial() async { + final hasSeenTutorial = cacheService.getBool(key: CacheConst.organSelectorTutorialShown) ?? false; + if (!hasSeenTutorial) { + // Show tutorial after a short delay to ensure the screen is fully built + await Future.delayed(const Duration(milliseconds: 500)); + if (mounted) { + setState(() { + _showTutorial = true; + }); + } + } + } + + void _onTutorialComplete() async { + await cacheService.saveBool(key: CacheConst.organSelectorTutorialShown, value: true); + setState(() { + _showTutorial = false; + }); } void _onNextPressed(SymptomsCheckerViewModel viewModel) async { @@ -65,30 +92,39 @@ class _OrganSelectorPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: AppColors.bgScaffoldColor, - appBar: _buildAppBar(), - body: Consumer( - builder: (context, viewModel, _) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTitle(), - SizedBox(height: 8.h), - Expanded( - child: Stack( - children: [ - _buildBodyViewer(viewModel), - _buildViewToggleButtons(viewModel), - _buildViewZoomButtons(viewModel), - _buildBottomSheet(viewModel), - ], - ), - ), - ], - ); - }, - ), + return Stack( + children: [ + Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + appBar: _buildAppBar(), + body: Consumer( + builder: (context, viewModel, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTitle(), + SizedBox(height: 8.h), + Expanded( + child: Stack( + children: [ + _buildBodyViewer(viewModel), + _buildViewToggleButtons(viewModel), + // _buildViewZoomButtons(viewModel), + _buildBottomSheet(viewModel), + ], + ), + ), + ], + ); + }, + ), + ), + // Tutorial overlay + if (_showTutorial) + PinchZoomTutorialOverlay( + onComplete: _onTutorialComplete, + ), + ], ); } diff --git a/lib/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart b/lib/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart new file mode 100644 index 00000000..307445a4 --- /dev/null +++ b/lib/presentation/symptoms_checker/widgets/pinch_zoom_tutorial_overlay.dart @@ -0,0 +1,261 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.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'; + +class PinchZoomTutorialOverlay extends StatefulWidget { + final VoidCallback onComplete; + + const PinchZoomTutorialOverlay({ + Key? key, + required this.onComplete, + }) : super(key: key); + + @override + State createState() => _PinchZoomTutorialOverlayState(); +} + +class _PinchZoomTutorialOverlayState extends State with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _fadeAnimation; + int _currentStep = 0; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _fadeAnimation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ); + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + void _nextStep() { + if (_currentStep < 1) { + setState(() { + _currentStep++; + }); + } else { + _animationController.reverse().then((_) { + widget.onComplete(); + }); + } + } + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: _fadeAnimation, + child: Material( + color: Colors.black.withValues(alpha: 0.85), + child: SafeArea( + child: Stack( + children: [ + // Tap to dismiss overlay + Positioned.fill( + child: GestureDetector( + onTap: () {}, + child: Container(color: Colors.transparent), + ), + ), + // Tutorial content + Center( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: _currentStep == 0 ? _buildFirstStep() : _buildSecondStep(), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildFirstStep() { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Pinch gesture illustration + Container( + width: 200.w, + height: 200.h, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20.r), + ), + child: Center( + child: _buildPinchGestureAnimation(), + ), + ), + SizedBox(height: 32.h), + // Title + Text( + LocaleKeys.pinchToZoom.tr(context: context), + style: TextStyle( + color: Colors.white, + fontSize: 24.f, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 32.h), + // Button + CustomButton( + text: LocaleKeys.gotIt.tr(context: context), + onPressed: _nextStep, + backgroundColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + height: 50.h, + ), + ], + ); + } + + Widget _buildSecondStep() { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Illustration container with rounded corners + Container( + padding: EdgeInsets.all(24.w), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.95), + borderRadius: BorderRadius.circular(20.r), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + LocaleKeys.zoomIntoDetails.tr(context: context), + style: TextStyle( + color: AppColors.textColor, + fontSize: 20.f, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 24.h), + // Pinch instruction + _buildInstructionRow( + icon: Icons.pinch_outlined, + text: LocaleKeys.pinchWithTwoFingersToZoom.tr(context: context), + ), + SizedBox(height: 16.h), + // Drag instruction + _buildInstructionRow( + icon: Icons.pan_tool_outlined, + text: LocaleKeys.dragToMoveAround.tr(context: context), + ), + ], + ), + ), + SizedBox(height: 32.h), + // Button + CustomButton( + text: LocaleKeys.startExploring.tr(context: context), + onPressed: _nextStep, + backgroundColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + height: 50.h, + ), + ], + ); + } + + Widget _buildInstructionRow({required IconData icon, required String text}) { + return Row( + children: [ + Container( + width: 48.w, + height: 48.h, + decoration: BoxDecoration( + color: AppColors.primaryRedColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12.r), + ), + child: Icon( + icon, + color: AppColors.primaryRedColor, + size: 24.w, + ), + ), + SizedBox(width: 16.w), + Expanded( + child: Text( + text, + style: TextStyle( + color: AppColors.textColor, + fontSize: 14.f, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ); + } + + Widget _buildPinchGestureAnimation() { + return TweenAnimationBuilder( + tween: Tween(begin: 1.0, end: 0.7), + duration: const Duration(milliseconds: 1500), + curve: Curves.easeInOut, + builder: (context, value, child) { + return Transform.scale( + scale: value, + child: Stack( + alignment: Alignment.center, + children: [ + // Left hand + Positioned( + left: 40.w * (1 - value), + child: Transform.rotate( + angle: -0.3, + child: Icon( + Icons.pan_tool, + size: 50.w, + color: Colors.white.withValues(alpha: 0.9), + ), + ), + ), + // Right hand + Positioned( + right: 40.w * (1 - value), + child: Transform.rotate( + angle: 0.3, + child: Icon( + Icons.pan_tool, + size: 50.w, + color: Colors.white.withValues(alpha: 0.9), + ), + ), + ), + ], + ), + ); + }, + onEnd: () { + // Restart animation + setState(() {}); + }, + ); + } +} + diff --git a/lib/widgets/common_bottom_sheet.dart b/lib/widgets/common_bottom_sheet.dart index 069022d1..f7145976 100644 --- a/lib/widgets/common_bottom_sheet.dart +++ b/lib/widgets/common_bottom_sheet.dart @@ -18,7 +18,7 @@ import 'package:permission_handler/permission_handler.dart'; class BottomSheetUtils { Future 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 { + {required Function() onSuccess, String? title, String? description, Function(int)? onMultiDateSuccess, bool isMultiAllowed = false, bool isForAppointment = false}) async { // Check and request permissions based on platform bool hasPermission = await _checkAndRequestCalendarPermissions(); @@ -79,7 +79,7 @@ class BottomSheetUtils { } /// Shows permission denied dialog with option to open settings - Future _showPermissionDeniedDialog() async { + Future _showPermissionDeniedDialog({bool isForAppointment = false}) async { BuildContext? context = GetIt.instance().navigatorKey.currentContext; if (context == null) return; @@ -87,7 +87,7 @@ class BottomSheetUtils { context, title: LocaleKeys.notice.tr(context: context), child: Utils.getWarningWidget( - loadingText: LocaleKeys.calendarPermissionAlert.tr(), + loadingText: isForAppointment ? LocaleKeys.calendarPermissionAlertForAppointment.tr() : LocaleKeys.calendarPermissionAlert.tr(), isShowActionButtons: true, onCancelTap: () { GetIt.instance().pop();