Merge branch 'refs/heads/master' into faiz_dev

# Conflicts:
#	assets/langs/ar-SA.json
#	assets/langs/en-US.json
#	lib/core/api/api_client.dart
#	lib/core/api_consts.dart
#	lib/generated/locale_keys.g.dart
#	lib/presentation/habib_wallet/habib_wallet_page.dart
#	lib/presentation/my_invoices/widgets/invoice_list_card.dart
#	lib/widgets/image_picker.dart
faiz_dev
faizatflutter 4 months ago
commit 42aad44cd4

@ -77,4 +77,15 @@
-keep class com.peng.pennavmap.db.** { *; }
-keep class com.hiennv.flutter_callkit_incoming.** { *; }
-keepattributes Signature, Annotation, InnerClasses, EnclosingMethod
-keepattributes Signature, Annotation, InnerClasses, EnclosingMethod
-keep class com.peng.pennavmap.** { *; }
-keep class com.peng.bus.** { *; }
# EventBus subscribers (reflection-based)
-keepclassmembers class * {
@com.peng.bus.PISubscribe <methods>;
}
-keep class com.peng.pennavmap.models.bus.BackButtonEventBusData { *; }
-keep interface com.payfort.fortpaymentsdk.** {*;}
-keep class com.payfort.fortpaymentsdk.** {*;}

@ -41,6 +41,11 @@
android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"
tools:node="remove" />
<!-- Gallery/Photo permissions for Android 13+ (API 33+) -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove" />
<!-- <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />-->
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove" />
<!-- Gallery/Photo permissions for Android 12 and below -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
@ -158,7 +163,7 @@
android:enabled="true"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleInstance"
android:launchMode="singleTop"
android:showOnLockScreen="true"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize"

@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="10" fill="#EFEFF0"/>
<path d="M21.0004 20.0004H18.0004V16.4004C18.0004 16.2034 18.0008 16.0957 18.005 16.0174L18.0057 16.0057L18.0174 16.005C18.0957 16.0008 18.2034 16.0004 18.4004 16.0004H21.0004C22.105 16.0004 23.0004 16.8958 23.0004 18.0004C23.0004 19.105 22.105 20.0004 21.0004 20.0004Z" fill="#8F9AA3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0513 9.75H19.9487C17.8773 9.74999 16.2537 9.74998 14.9758 9.90522C13.6698 10.0639 12.6246 10.3941 11.7657 11.1277C11.5366 11.3233 11.3233 11.5366 11.1277 11.7657C10.3941 12.6246 10.0639 13.6698 9.90522 14.9758C9.74998 16.2538 9.74999 17.8773 9.75 19.9487V20.0513C9.74999 22.1227 9.74998 23.7462 9.90522 25.0242C10.0639 26.3302 10.3941 27.3754 11.1277 28.2343C11.3233 28.4634 11.5366 28.6767 11.7657 28.8723C12.6246 29.6059 13.6698 29.9361 14.9758 30.0948C16.2538 30.25 17.8773 30.25 19.9487 30.25H20.0513C22.1227 30.25 23.7462 30.25 25.0242 30.0948C26.3302 29.9361 27.3754 29.6059 28.2343 28.8723C28.4634 28.6767 28.6767 28.4634 28.8723 28.2343C29.6059 27.3754 29.9361 26.3302 30.0948 25.0242C30.25 23.7462 30.25 22.1227 30.25 20.0513V19.9487C30.25 17.8773 30.25 16.2538 30.0948 14.9758C29.9361 13.6698 29.6059 12.6246 28.8723 11.7657C28.6767 11.5366 28.4634 11.3233 28.2343 11.1277C27.3754 10.3941 26.3302 10.0639 25.0242 9.90522C23.7463 9.74998 22.1227 9.74999 20.0513 9.75ZM18.4004 14.0004L18.3194 14.0002C18.0377 13.9992 17.6913 13.9979 17.3824 14.0983C16.7735 14.2961 16.2961 14.7735 16.0983 15.3824C15.9979 15.6913 15.9992 16.0377 16.0002 16.3194L16.0004 16.4004V26.0004C16.0004 26.5527 16.4481 27.0004 17.0004 27.0004C17.5527 27.0004 18.0004 26.5527 18.0004 26.0004V22.0004H21.0004C23.2096 22.0004 25.0004 20.2096 25.0004 18.0004C25.0004 15.7913 23.2096 14.0004 21.0004 14.0004H18.4004Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -230,6 +230,17 @@
"companyName": "اسم الشركة:",
"receiptOn": "الإيصال على:",
"expiryDate": "تاريخ الانتهاء:",
"myCards": "بطاقاتي",
"savedCards": "البطاقات المحفوظة",
"addNewCard": "إضافة بطاقة جديدة",
"noSavedCards": "لا توجد بطاقات محفوظة",
"cardEndingWith": "البطاقة المنتهية بـ",
"expiry": "الصلاحية",
"defaultCard": "افتراضي",
"deleteCard": "حذف البطاقة؟",
"deleteCardConfirmation": "هل أنت متأكد من حذف هذه البطاقة المنتهية بـ",
"cardDeletedSuccessfully": "تم حذف البطاقة بنجاح",
"addCardComingSoon": "وظيفة إضافة البطاقة قريباً",
"expiryPoints": "منتهي الصلاحية",
"expiryOn": "ينتهي في:",
"procedureName": "اسم الإجراء:",
@ -370,6 +381,12 @@
"clickHere": "انقر هنا",
"iAgreeToTheTermsAndConditions": "أوافق على الشروط والأحكام ",
"iAgreeToTheTermsAndConditionsSubtitle": "أوافق على الشروط والأحكام ",
"iAgreeAndAcceptTermsAndConditions": "أوافق وأقبل ",
"termsAndConditions": "الشروط والأحكام",
"iAgreeHealthInfoCanBeUsed": "أوافق على استخدام معلوماتي الصحية للمقابلة. مزيد من المعلومات في ",
"privacyPolicyText": "سياسة الخصوصية",
"outPatient": "العيادات الخارجيه",
"thisInvoiceIsNotEligibleForRefund": "هذه الفاتورة غير مؤهلة لاسترداد الأموال",
"save": "حفظ",
"userAgreement": "اتفاقية المستخدم",
"updateSuccessfully": "تم التحديث بنجاح",
@ -666,11 +683,20 @@
"emrgNo": "رقم جهة الاتصال في حالات الطوارئ",
"modes": "الأوضاع",
"vibration": "اهتزاز عند اللمس",
"blindModes": "أوضاع للمكفوفين جزئيًا",
"blindModes": "وضع للمكفوفين جزئياً",
"invertTheme": "عكس",
"offTheme": "إيقاف",
"dimTheme": "خافت",
"bwTheme": "أسود وأبيض",
"highContrastTheme": "تباين عالي",
"fontSize": "حجم الخط",
"fontSizeSmall": "صغير",
"fontSizeMedium": "متوسط",
"fontSizeLarge": "كبير",
"saveChanges": "حفظ التغييرات",
"settingsSavedSuccessfully": "تم حفظ الإعدادات بنجاح",
"cardHeading": "عنوان البطاقة",
"buttonText": "نص الزر",
"permissions": "إذن",
"cameraPermission": "الكاميرا",
"locationPermission": "الموقع",
@ -678,7 +704,7 @@
"outOfStockMsg": "لقد أضفت منتجًا غير متوفر حاليًا، يرجى إزالته!",
"noArabicLetters": "لا يُسمح باستخدام الحروف العربية",
"noOffersAvailable": "لا توجد عروض متاحة!",
"accessibility": "وضع الوصول",
"accessibility": "إمكانية الوصول",
"orderStatus": "حالة الطلب",
"findUs": "فروعنا",
"liveChat": "محادثة مباشرة",
@ -945,7 +971,7 @@
"immediateConsultation": "استشارة فورية",
"scheduledConsultation": "استشارة مجدولة",
"pharmaLiveCare": "لايف كير الصيدلية",
"notSureHelpMeChooseClinic": "هل تريد مساعدة في اختيار العياة؟",
"notSureHelpMeChooseClinic": "هل تريد مساعدة في اختيار العيادة؟",
"mentionYourSymptomsAndFindDoctors": "أدخل حالتك الصحية للحصول على قائمة الأطباء",
"immediateService": "خدمة فورية",
"noNeedToWaitGetMedicalConsultation": "لا حاجة للانتظار، ستحصل على استشارة طبية فورية عبر مكالمة فيديو",
@ -1427,8 +1453,11 @@
"suggestions": "التوصيات",
"pleaseGoBackAndSelectOrgansFirst": "يرجى العودة واختيار الأعضاء أولاً",
"symptomsSelector": "محدد الأعراض",
"possibleSymptomsRelatedTo": "أعراض مرتبطة بـ",
"possibleSymptomsRelatedTo": "الأعراض المحتملة المتعلقة بأعضاء محددة",
"suggestionAIListDescription": "هذه قائمة بالأعراض المقترحة من قبل الذكاء الاصطناعي، بناءً على المعلومات التي تم جمعها حتى الآن خلال المقابلة",
"whyAmIBeingAskedThis": "لماذا يُطرح علي هذا السؤال؟",
"weAreAskingThisQuestionToBetterUnderstand": "نطرح هذا السؤال لفهم {symptoms} بشكل أفضل وتحسين دقة التقييم.",
"analyzingYourSymptoms": "أعراضك",
"analyzing": "جاري التحليل...",
"seekMedicalAdvice": "اطلب المشورة الطبية",
"monitorDescription": "لا حاجة لطلب المشورة الطبية. فقط حافظ على روتين صحي.",
@ -1447,6 +1476,30 @@
"areYouSureYouWantToExitProgress": "هل تود الخروج؟ ستفقد تقدمك الحالي.",
"noQuestionAvailable": "لا يوجد سؤال متاح",
"possibleSymptom": "تشخيص محتمل",
"triageLevelEmergencyAmbulance": "طوارئ - اتصل بالإسعاف",
"triageLevelEmergency": "طوارئ",
"triageLevelConsultation24": "استشارة خلال 24 ساعة",
"triageLevelConsultation": "استشارة طبية",
"triageLevelSelfCare": "رعاية ذاتية",
"triageLevelEmergencyAmbulanceDesc": "الأعراض المبلغ عنها خطيرة جدًا وقد تحتاج إلى رعاية طارئة. يجب عليك الاتصال بالإسعاف الآن.",
"triageLevelEmergencyDesc": "تبدو الأدلة المبلغ عنها خطيرة ويجب عليك الذهاب إلى قسم الطوارئ. إذا لم تتمكن من ال<D8A7><D984>صول إلى أقرب قسم طوارئ، اتصل بالإسعاف.",
"triageLevelConsultation24Desc": "يجب عليك زيارة الطبيب خلال 24 ساعة. إذا ساءت الأعراض فجأة، توجه إلى أقرب قسم طوارئ.",
"triageLevelConsultationDesc": "قد تحتاج إلى تقييم طبي وقد تحتاج إلى تحديد موعد مع طبيب. إذا ساءت الأعراض، راجع الطبيب على الفور.",
"triageLevelSelfCareDesc": "قد لا تتطلب الأعراض المعلنة تقييمًا طبيًا وعادة ما تزول من تلقاء نفسها. في بعض الأحيان يمكن علاجها من خلال طرق الرعاية الذاتية. راقب أعراضك واستشر الطبيب إذا ساءت الأعراض أو ظهرت أعراض جديدة.",
"rootCauseEmergencyEvidencePresent": "تم الإبلاغ عن دليل طوارئ",
"rootCauseSeriousEvidencePresent": "تم الإبلاغ عن دليل خطير",
"rootCauseEmergencyConditionLikely": "حالة تهدد الحياة محتملة",
"rootCauseEmergencyConditionPossible": "حالة تهدد الحياة ممكنة",
"rootCauseConsultationConditionLikely": "استشارة طبية مطلوبة",
"rootCauseSelfCareSufficient": "لا يوجد سبب محدد للتقييم الطبي",
"rootCauseDiagnosisUnknown": "التقييم غير ممكن",
"teleconsultationAvailable": "الاستشارة عن بُعد متاحة",
"seriousSymptoms": "أعراض خطيرة",
"howTo": "كيفية القياس؟",
"explainWhy": "اشرح، لماذا؟",
"whyThisCondition": "لماذا هذه الحالة؟",
"supportingEvidence": "الأدلة الداعمة",
"explanationDescription": "بناءً على الأعراض التي أبلغت عنها، تم تحديد هذه الحالة كتطابق محتمل. الأعراض التالية تدعم هذا التقييم:",
"symptomsCheckerFindingScore": "نسبة التطابق",
"notSet": "غير محدد",
"years": "سنوات",
@ -1812,6 +1865,7 @@
"watchUsOnYoutube": "شاهدنا على يوتيوب",
"connectOnLinkedin": "تواصل معنا على لينكد إن",
"riskFactorPageTitle": "اعراض تشعر بها",
"doAnyOfTheseApply": "هل ينطبق عليك أي من هذه الأمور؟",
"bloodDonationService": "التبرع \n بالدم",
"bookAppointmentService": "حجز\nموعد",
"pinchToZoom": "اضغط للتكبير",
@ -1883,12 +1937,18 @@
"refundReason6": "تم إلغاء الإجراء السريري أو تغييره من قِبل الطبيب المحيل",
"refundReason7": "تم إلغاء إجراء وحدة الخدمة / المختبر / الأشعة أو تغييره من قِبل الطبيب المحيل",
"selectProcedures": "اختر العمليات",
"beforeUsingSymptomsChecker": "قبل استخدام فاحص الأعراض، يرجى قراءة شروط الخدمة وتذكر:",
"symptomsCheckerNotDiagnosis": "• فاحص الأعراض ليس تشخيصًا. إنه للمعلومات الخاصة بك فقط وليس رأيًا طبيًا مؤهلاً.",
"symptomsCheckerNotEmergency": "• فاحص الأعراض ليس للطوارئ. اتصل برقم الطوارئ المحلي فورًا عند وجود حالة طوارئ صحية.",
"yourDataIsSafe": "• بياناتك آمنة. تضمن تدابير الخصوصية حماية المعلومات التي تشاركها.",
"submitEReferral": "تقديم الطلب",
"redeem": "استبدال",
"points": "نقاط",
"transactions": "المعاملات",
"pharmacy": "الصيدلية",
"visitsOrders": "الزيارات/الطلبات",
"earned": "حصل",
"getParkingQR": "الحصول على رمز الاستجابة السريعة لوقوف السيارات",
"cannotOpenThisFile": "لا يمكن فتح هذا الملف",
"thisInvoiceIsNotEligibleForRefund": "هذه الفاتورة غير مؤهلة للاسترداد",
"refundDetails": "تفاصيل الاسترداد",
@ -1909,4 +1969,10 @@
"cashRefundInstructions": "بمجرد الموافقة على الطلب، يرجى زيارة صراف مستشفى الحبيب الطبي أو الحبيب للرعاية الأولية لاستلام النقد. عند نافذة الصراف، سوف تتلقى رمز التحقق على رقم هاتفك المسجل. قدم الرمز للصراف للتحقق منه. سيتم صرف النقد فقط بعد التحقق الناجح.",
"cashRefundApprovalInstructions": "بمجرد الموافقة على الطلب،\nيرجى زيارة صراف مستشفى الحبيب الطبي أو الحبيب للرعاية الأولية لاستلام النقد.\nعند نافذة الصراف، سوف تتلقى رمز التحقق على رقم هاتفك المسجل. قدم الرمز للصراف للتحقق منه. سيتم صرف النقد فقط بعد التحقق الناجح.",
"packageRefundValidationMessage": "طلب استرداد الباقة الخاص بك يتطلب التحقق بسبب استخدام الباقة والفواتير الفرعية ذات الصلة. سنقوم بمراجعة ومعالجة الطلب ضمن اتفاقية مستوى الخدمة المعتمدة بعد تأكيد الأهلية."
"invoiceHistory": "سجل الفواتير",
"refund": "استرداد",
"invoiceHistory": "سجل الفواتير",
"refund": "استرداد",
"parkingQR": "رمز الاستجابة السريعة لوقوف السيارات",
"parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:"
}

@ -227,6 +227,17 @@
"companyName": "Company Name:",
"receiptOn": "Receipt on:",
"expiryDate": "Expiry Date:",
"myCards": "My Cards",
"savedCards": "Saved Cards",
"addNewCard": "Add New Card",
"noSavedCards": "No saved cards",
"cardEndingWith": "Card ending with",
"expiry": "Expiry",
"defaultCard": "Default",
"deleteCard": "Delete Card?",
"deleteCardConfirmation": "Are you sure you want to delete this card ending in",
"cardDeletedSuccessfully": "Card deleted successfully",
"addCardComingSoon": "Add card functionality coming soon",
"expiryPoints": "Expired",
"expiryOn": "Expiry on:",
"procedureName": "Procedure Name:",
@ -366,6 +377,12 @@
"clickHere": "Click here",
"iAgreeToTheTermsAndConditions": "I agree to the terms and conditions ",
"iAgreeToTheTermsAndConditionsSubtitle": "I agree to the terms and conditions ",
"iAgreeAndAcceptTermsAndConditions": "I agree and accept ",
"termsAndConditions": "Terms and Conditions",
"iAgreeHealthInfoCanBeUsed": "I agree that my health information can be used for the interview. More information in the ",
"privacyPolicyText": "Privacy Policy",
"outPatient": "Out Patient",
"thisInvoiceIsNotEligibleForRefund": "This invoice is not eligible for refund",
"save": "Save",
"userAgreement": "User Agreement",
"updateSuccessfully": "Update Successfully",
@ -656,11 +673,20 @@
"emrgNo": "Emergency Contact Number",
"modes": "Modes",
"vibration": "Vibration Touch Feedback",
"blindModes": "Modes for Partially Blind",
"blindModes": "Mode for partially blind",
"invertTheme": "Invert",
"offTheme": "Off",
"dimTheme": "Dim",
"bwTheme": "Black and White",
"highContrastTheme": "High Contrast",
"fontSize": "Font Size",
"fontSizeSmall": "Small",
"fontSizeMedium": "Medium",
"fontSizeLarge": "Large",
"saveChanges": "Save Changes",
"settingsSavedSuccessfully": "Settings saved successfully",
"cardHeading": "Card Heading",
"buttonText": "Button Text",
"permissions": "Permission",
"cameraPermission": "Camera",
"locationPermission": "Location",
@ -668,7 +694,7 @@
"outOfStockMsg": "You have added product which is out of stock now, Please remove that!",
"noArabicLetters": "It Is Not Allow to Use Arabic Letters",
"noOffersAvailable": "No Offers Available!",
"accessibility": "Accessibility Mode",
"accessibility": "Accessibility",
"orderStatus": "Order Status",
"findUs": "Find Us",
"liveChat": "Live Chat",
@ -1419,8 +1445,11 @@
"suggestions": "Suggestions",
"pleaseGoBackAndSelectOrgansFirst": "Please go back and select organs first",
"symptomsSelector": "Symptoms Selector",
"possibleSymptomsRelatedTo": "Possible symptoms related to",
"possibleSymptomsRelatedTo": "Possible symptoms related to selected organs",
"suggestionAIListDescription": "This is a list of symptoms suggested by our AI, based on the information gathered so far during the interview",
"whyAmIBeingAskedThis": "Why am I being asked this?",
"weAreAskingThisQuestionToBetterUnderstand": "We are asking this question to better understand your {symptoms} and improve the accuracy of the assessment.",
"analyzingYourSymptoms": "your symptoms",
"analyzing": "Analyzing...",
"seekMedicalAdvice": "Seek Medical Advice",
"monitorDescription": "No need to seek medical advice. Just keep healthy routine.",
@ -1439,6 +1468,30 @@
"areYouSureYouWantToExitProgress": "Are you sure you want to exit? Your progress will be lost.",
"noQuestionAvailable": "No question available",
"possibleSymptom": "Possible symptom: ",
"triageLevelEmergencyAmbulance": "Emergency - Call Ambulance",
"triageLevelEmergency": "Emergency",
"triageLevelConsultation24": "Consultation Within 24 Hours",
"triageLevelConsultation": "Medical Consultation",
"triageLevelSelfCare": "Self-Care",
"triageLevelEmergencyAmbulanceDesc": "The reported symptoms are very serious and you may require emergency care. You should call an ambulance right now.",
"triageLevelEmergencyDesc": "The reported evidence appears serious and you should go to an emergency department. If you can't get to the nearest emergency department, call an ambulance.",
"triageLevelConsultation24Desc": "You should see a doctor within 24 hours. If the symptoms suddenly get worse, go to the nearest emergency department.",
"triageLevelConsultationDesc": "You may require medical evaluation and may need to schedule an appointment with a doctor. If the symptoms get worse, see a doctor immediately.",
"triageLevelSelfCareDesc": "The declared symptoms may not require a medical evaluation and they usually resolve on their own. Sometimes they can be treated through self-care methods. Observe your symptoms and consult a doctor if the symptoms get worse or new ones appear.",
"rootCauseEmergencyEvidencePresent": "Emergency evidence reported",
"rootCauseSeriousEvidencePresent": "Serious evidence reported",
"rootCauseEmergencyConditionLikely": "Life-threatening condition likely",
"rootCauseEmergencyConditionPossible": "Life-threatening condition possible",
"rootCauseConsultationConditionLikely": "Medical consultation required",
"rootCauseSelfCareSufficient": "No identified reason for medical evaluation",
"rootCauseDiagnosisUnknown": "Assessment not possible",
"teleconsultationAvailable": "Teleconsultation Available",
"seriousSymptoms": "Serious Symptoms",
"howTo": "How to?",
"explainWhy": "Explain, Why?",
"whyThisCondition": "Why this condition?",
"supportingEvidence": "Supporting Evidence",
"explanationDescription": "Based on your reported symptoms, this condition has been identified as a possible match. The following symptoms support this assessment:",
"symptomsCheckerFindingScore": "- Symptoms checker finding score",
"notSet": "Not set",
"years": "Years",
@ -1804,6 +1857,7 @@
"watchUsOnYoutube": "Watch us on youtube",
"connectOnLinkedin": "Connect on LinkedIn",
"riskFactorPageTitle": "What do you feel?",
"doAnyOfTheseApply": "Do any of these apply to you?",
"bloodDonationService": "Blood \nDonation",
"bookAppointmentService": "Book\nAppointment",
"pinchToZoom": "Pinch to zoom",
@ -1875,13 +1929,18 @@
"refundReason6": "Clinical procedure cancelled or changed by requesting doctor",
"refundReason7": "Service unit/lab/radiology procedure cancelled or changed by requesting doctor",
"selectProcedures": "Select Procedures",
"beforeUsingSymptomsChecker": "Before using the Symptoms Checker, please read the Terms of Service and remember:",
"symptomsCheckerNotDiagnosis": "• Symptoms Checker isn't a diagnosis. It's only for your information and not a qualified medical opinion.",
"symptomsCheckerNotEmergency": "• Symptoms Checker isn't for emergencies. Call your local emergency number right away when there's a health emergency.",
"yourDataIsSafe": "• Your data is safe. Privacy measures ensure the information you share is protected.",
"redeem": "Redeem",
"points": "Points",
"transactions": "Transactions",
"pharmacy": "Pharmacy",
"visitsOrders": "Visits/Orders",
"earned": "Earned",
"getParkingQR": "Get Parking QR",
"cannotOpenThisFile": "Cannot open this file",
"thisInvoiceIsNotEligibleForRefund": "This invoice is not eligible for refund",
"refundDetails": "Refund Details",
"requestNo": "Request No.",
"referenceNo": "Reference No.",
@ -1901,6 +1960,9 @@
"cashRefundApprovalInstructions": "Once the request is approved,\nPlease visit HMG Hospital or HMG Primary Care cashier to receive cash.\nAt the cashier counter, you will receive an OTP on your registered mobile number. Provide the OTP to the cashier for validation. Cash will be released only after successful validation.",
"packageRefundValidationMessage": "Your package refund request required validation because of package use and related child invoices. We will review the process the request within the approved SLA after confirming eligibility."
"invoiceHistory": "Invoice History",
"parkingQR": "Parking QR Code",
"parkingQRDesc": "Scan the below QR Code at the exit parking gate:"
}

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '14.0'
platform :ios, '15.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@ -54,6 +54,7 @@ post_install do |installer|
'PERMISSION_EVENTS_FULL_ACCESS=1',
## dart: PermissionGroup.reminders
'PERMISSION_REMINDERS=1',
'PERMISSION_PHOTOS=1',
## dart: PermissionGroup.notification
'PERMISSION_NOTIFICATIONS=1',
]

@ -1,626 +0,0 @@
PODS:
- amazon_payfort (1.1.4):
- Flutter
- PayFortSDK
- audio_session (0.0.1):
- Flutter
- barcode_scan2 (0.0.1):
- Flutter
- SwiftProtobuf (~> 1.33)
- connectivity_plus (0.0.1):
- Flutter
- CryptoSwift (1.8.4)
- device_calendar (0.0.1):
- Flutter
- device_calendar_plus_ios (0.0.1):
- Flutter
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.9):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.9)
- DKImagePickerController/PhotoGallery (4.3.9):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.9)
- DKPhotoGallery (0.0.19):
- DKPhotoGallery/Core (= 0.0.19)
- DKPhotoGallery/Model (= 0.0.19)
- DKPhotoGallery/Preview (= 0.0.19)
- DKPhotoGallery/Resource (= 0.0.19)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.19):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/Analytics (11.15.0):
- Firebase/Core
- Firebase/Core (11.15.0):
- Firebase/CoreOnly
- FirebaseAnalytics (~> 11.15.0)
- Firebase/CoreOnly (11.15.0):
- FirebaseCore (~> 11.15.0)
- Firebase/Crashlytics (11.15.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 11.15.0)
- Firebase/Messaging (11.15.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.15.0)
- firebase_analytics (11.6.0):
- Firebase/Analytics (= 11.15.0)
- firebase_core
- Flutter
- firebase_core (3.15.2):
- Firebase/CoreOnly (= 11.15.0)
- Flutter
- firebase_crashlytics (4.3.10):
- Firebase/Crashlytics (= 11.15.0)
- firebase_core
- Flutter
- firebase_messaging (15.2.10):
- Firebase/Messaging (= 11.15.0)
- firebase_core
- Flutter
- FirebaseAnalytics (11.15.0):
- FirebaseAnalytics/Default (= 11.15.0)
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/Default (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- GoogleAppMeasurement/Default (= 11.15.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseCore (11.15.0):
- FirebaseCoreInternal (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreExtension (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseCoreInternal (11.15.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseCrashlytics (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- FirebaseRemoteConfigInterop (~> 11.0)
- FirebaseSessions (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/Environment (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- FirebaseInstallations (11.15.0):
- FirebaseCore (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- FirebaseRemoteConfigInterop (11.15.0)
- FirebaseSessions (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseCoreExtension (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- PromisesSwift (~> 2.1)
- FLAnimatedImage (1.0.17)
- Flutter (1.0.0)
- flutter_callkit_incoming (0.0.1):
- CryptoSwift
- Flutter
- flutter_image_compress_common (1.0.0):
- Flutter
- Mantle
- SDWebImage
- SDWebImageWebPCoder
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview_ios/Core (= 0.0.1)
- OrderedSet (~> 6.0.3)
- flutter_inappwebview_ios/Core (0.0.1):
- Flutter
- OrderedSet (~> 6.0.3)
- flutter_ios_voip_kit_karmm (0.8.0):
- Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_nfc_kit (3.6.0):
- Flutter
- flutter_zoom_videosdk (0.0.1):
- Flutter
- ZoomVideoSDK/CptShare (= 2.1.10)
- ZoomVideoSDK/zm_annoter_dynamic (= 2.1.10)
- ZoomVideoSDK/zoomcml (= 2.1.10)
- ZoomVideoSDK/ZoomVideoSDK (= 2.1.10)
- fluttertoast (0.0.2):
- Flutter
- geolocator_apple (1.2.0):
- Flutter
- FlutterMacOS
- Google-Maps-iOS-Utils (5.0.0):
- GoogleMaps (~> 8.0)
- google_maps_flutter_ios (0.0.1):
- Flutter
- Google-Maps-iOS-Utils (< 7.0, >= 5.0)
- GoogleMaps (< 11.0, >= 8.4)
- GoogleAdsOnDeviceConversion (2.1.0):
- GoogleUtilities/Logger (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Core (11.15.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Default (11.15.0):
- GoogleAdsOnDeviceConversion (= 2.1.0)
- GoogleAppMeasurement/Core (= 11.15.0)
- GoogleAppMeasurement/IdentitySupport (= 11.15.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/IdentitySupport (11.15.0):
- GoogleAppMeasurement/Core (= 11.15.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleMaps (8.4.0):
- GoogleMaps/Maps (= 8.4.0)
- GoogleMaps/Base (8.4.0)
- GoogleMaps/Maps (8.4.0):
- GoogleMaps/Base
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/MethodSwizzler (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- health (13.1.4):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- in_app_review (2.0.0):
- Flutter
- just_audio (0.0.1):
- Flutter
- FlutterMacOS
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0)
- libwebp/sharpyuv (= 1.5.0)
- libwebp/webp (= 1.5.0)
- libwebp/demux (1.5.0):
- libwebp/webp
- libwebp/mux (1.5.0):
- libwebp/demux
- libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0):
- libwebp/sharpyuv
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
- location (0.0.1):
- Flutter
- manage_calendar_events (0.0.1):
- Flutter
- Mantle (2.2.0):
- Mantle/extobjc (= 2.2.0)
- Mantle/extobjc (2.2.0)
- map_launcher (0.0.1):
- Flutter
- MapboxCommon (23.11.0)
- MapboxCoreMaps (10.19.1):
- MapboxCommon (~> 23.11)
- MapboxCoreNavigation (2.19.0):
- MapboxDirections (~> 2.14)
- MapboxNavigationNative (< 207.0.0, >= 206.0.1)
- MapboxDirections (2.14.3):
- Polyline (~> 5.0)
- Turf (~> 2.8.0)
- MapboxMaps (10.19.0):
- MapboxCommon (= 23.11.0)
- MapboxCoreMaps (= 10.19.1)
- MapboxMobileEvents (= 2.0.0)
- Turf (= 2.8.0)
- MapboxMobileEvents (2.0.0)
- MapboxNavigation (2.19.0):
- MapboxCoreNavigation (= 2.19.0)
- MapboxMaps (~> 10.18)
- MapboxSpeech (~> 2.0)
- Solar-dev (~> 3.0)
- MapboxNavigationNative (206.2.2):
- MapboxCommon (~> 23.10)
- MapboxSpeech (2.1.1)
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- network_info_plus (0.0.1):
- Flutter
- open_filex (0.0.2):
- Flutter
- OrderedSet (6.0.3)
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- PayFortSDK (3.2.2)
- permission_handler_apple (9.3.0):
- Flutter
- Polyline (5.1.0)
- PromisesObjC (2.4.0)
- PromisesSwift (2.4.0):
- PromisesObjC (= 2.4.0)
- SDWebImage (5.21.5):
- SDWebImage/Core (= 5.21.5)
- SDWebImage/Core (5.21.5)
- SDWebImageWebPCoder (0.15.0):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- Solar-dev (3.0.1)
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- SwiftProtobuf (1.36.1)
- SwiftyGif (5.4.5)
- Turf (2.8.0)
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
- wakelock_plus (0.0.1):
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
- ZoomVideoSDK/CptShare (2.1.10)
- ZoomVideoSDK/zm_annoter_dynamic (2.1.10)
- ZoomVideoSDK/zoomcml (2.1.10)
- ZoomVideoSDK/ZoomVideoSDK (2.1.10)
DEPENDENCIES:
- amazon_payfort (from `.symlinks/plugins/amazon_payfort/ios`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_calendar (from `.symlinks/plugins/device_calendar/ios`)
- device_calendar_plus_ios (from `.symlinks/plugins/device_calendar_plus_ios/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- FLAnimatedImage
- Flutter (from `Flutter`)
- flutter_callkit_incoming (from `.symlinks/plugins/flutter_callkit_incoming/ios`)
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_ios_voip_kit_karmm (from `.symlinks/plugins/flutter_ios_voip_kit_karmm/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_nfc_kit (from `.symlinks/plugins/flutter_nfc_kit/ios`)
- flutter_zoom_videosdk (from `.symlinks/plugins/flutter_zoom_videosdk/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`)
- health (from `.symlinks/plugins/health/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- just_audio (from `.symlinks/plugins/just_audio/darwin`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- location (from `.symlinks/plugins/location/ios`)
- manage_calendar_events (from `.symlinks/plugins/manage_calendar_events/ios`)
- map_launcher (from `.symlinks/plugins/map_launcher/ios`)
- MapboxMaps (= 10.19.0)
- MapboxNavigation (= 2.19.0)
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
- open_filex (from `.symlinks/plugins/open_filex/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
SPEC REPOS:
trunk:
- CryptoSwift
- DKImagePickerController
- DKPhotoGallery
- Firebase
- FirebaseAnalytics
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- FirebaseCrashlytics
- FirebaseInstallations
- FirebaseMessaging
- FirebaseRemoteConfigInterop
- FirebaseSessions
- FLAnimatedImage
- Google-Maps-iOS-Utils
- GoogleAdsOnDeviceConversion
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleMaps
- GoogleUtilities
- libwebp
- Mantle
- MapboxCommon
- MapboxCoreMaps
- MapboxCoreNavigation
- MapboxDirections
- MapboxMaps
- MapboxMobileEvents
- MapboxNavigation
- MapboxNavigationNative
- MapboxSpeech
- nanopb
- OrderedSet
- PayFortSDK
- Polyline
- PromisesObjC
- PromisesSwift
- SDWebImage
- SDWebImageWebPCoder
- Solar-dev
- SwiftProtobuf
- SwiftyGif
- Turf
- ZoomVideoSDK
EXTERNAL SOURCES:
amazon_payfort:
:path: ".symlinks/plugins/amazon_payfort/ios"
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
barcode_scan2:
:path: ".symlinks/plugins/barcode_scan2/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
device_calendar:
:path: ".symlinks/plugins/device_calendar/ios"
device_calendar_plus_ios:
:path: ".symlinks/plugins/device_calendar_plus_ios/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
firebase_analytics:
:path: ".symlinks/plugins/firebase_analytics/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_crashlytics:
:path: ".symlinks/plugins/firebase_crashlytics/ios"
firebase_messaging:
:path: ".symlinks/plugins/firebase_messaging/ios"
Flutter:
:path: Flutter
flutter_callkit_incoming:
:path: ".symlinks/plugins/flutter_callkit_incoming/ios"
flutter_image_compress_common:
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_ios_voip_kit_karmm:
:path: ".symlinks/plugins/flutter_ios_voip_kit_karmm/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_nfc_kit:
:path: ".symlinks/plugins/flutter_nfc_kit/ios"
flutter_zoom_videosdk:
:path: ".symlinks/plugins/flutter_zoom_videosdk/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
geolocator_apple:
:path: ".symlinks/plugins/geolocator_apple/darwin"
google_maps_flutter_ios:
:path: ".symlinks/plugins/google_maps_flutter_ios/ios"
health:
:path: ".symlinks/plugins/health/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_review:
:path: ".symlinks/plugins/in_app_review/ios"
just_audio:
:path: ".symlinks/plugins/just_audio/darwin"
local_auth_darwin:
:path: ".symlinks/plugins/local_auth_darwin/darwin"
location:
:path: ".symlinks/plugins/location/ios"
manage_calendar_events:
:path: ".symlinks/plugins/manage_calendar_events/ios"
map_launcher:
:path: ".symlinks/plugins/map_launcher/ios"
network_info_plus:
:path: ".symlinks/plugins/network_info_plus/ios"
open_filex:
:path: ".symlinks/plugins/open_filex/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
amazon_payfort: 782d287860ac8962baf4e0a2a0fb2f81fdf27204
audio_session: 19e9480dbdd4e5f6c4543826b2e8b0e4ab6145fe
barcode_scan2: 050b7b327e85954c8918715d42e83c5a35c3ed89
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
CryptoSwift: e64e11850ede528a02a0f3e768cec8e9d92ecb90
device_calendar: 9cb33f88a02e19652ec7b8b122ca778f751b1f7b
device_calendar_plus_ios: 34dca387f44fad29b840096a840f94edc55af858
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_analytics: bf93e20703c95030404d6ddbb1adf05bf5c3885b
firebase_core: 99a37263b3c27536063a7b601d9e2a49400a433c
firebase_crashlytics: 986773c0c7ea4c3f5a4ead141b58522fcd9dfdb1
firebase_messaging: bf6697c61f31c7cc0f654131212ff04c0115c2c7
FirebaseAnalytics: 6433dfd311ba78084fc93bdfc145e8cb75740eae
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
FirebaseCoreExtension: edbd30474b5ccf04e5f001470bdf6ea616af2435
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
FirebaseCrashlytics: e09d0bc19aa54a51e45b8039c836ef73f32c039a
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09
FirebaseRemoteConfigInterop: 1c6135e8a094cc6368949f5faeeca7ee8948b8aa
FirebaseSessions: b9a92c1c51bbb81e78fc3142cda6d925d700f8e7
FLAnimatedImage: bbf914596368867157cc71b38a8ec834b3eeb32b
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_callkit_incoming: 417dd1b46541cdd5d855ad795ccbe97d1c18155e
flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
flutter_ios_voip_kit_karmm: 7ea37381a8841c92d186edf1f4604df5cc437579
flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
flutter_nfc_kit: 3985c93f749b9cb4747479205c2f10bd2f877a11
flutter_zoom_videosdk: 7ddc97d617b85aeace252165d2b5fc5638678c38
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
geolocator_apple: 66b711889fd333205763b83c9dcf0a57a28c7afd
Google-Maps-iOS-Utils: 66d6de12be1ce6d3742a54661e7a79cb317a9321
google_maps_flutter_ios: ffdd933832966673a1f57f5effd014dc8d4d43a2
GoogleAdsOnDeviceConversion: 2be6297a4f048459e0ae17fad9bfd2844e10cf64
GoogleAppMeasurement: 700dce7541804bec33db590a5c496b663fbe2539
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleMaps: 8939898920281c649150e0af74aa291c60f2e77d
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
health: 9cd70a5c9d9ec559a12d639a1471091c0cbb6b48
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
in_app_review: 436034b18594851a7328d7f1c2ed5ec235b79cfc
just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
local_auth_darwin: 63c73d6d28cc3e239be2b6aa460ea6e317cd5100
location: d5cf8598915965547c3f36761ae9cc4f4e87d22e
manage_calendar_events: 9b2889799340398027b3e3f5c4891d41599ec257
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
map_launcher: 8a4e484aaddff55680a7da9bc9e25f512067fb87
MapboxCommon: 119f3759f7dc9457f0695848108ab323eb643cb4
MapboxCoreMaps: ca17f67baced23f8c952166ac6314c35bad3f66c
MapboxCoreNavigation: 3be9990fae3ed732a101001746d0e3b4234ec023
MapboxDirections: d9ad8452e8927d95ed21e35f733834dbca7e0eb1
MapboxMaps: b7f29ec7c33f7dc6d2947c1148edce6db81db9a7
MapboxMobileEvents: d044b9edbe0ec7df60f6c2c9634fe9a7f449266b
MapboxNavigation: da9cf3d773ed5b0fa0fb388fccdaa117ee681f31
MapboxNavigationNative: 629e359f3d2590acd1ebbacaaf99e1a80ee57e42
MapboxSpeech: cd25ef99c3a3d2e0da72620ff558276ea5991a77
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
network_info_plus: 6613d9d7cdeb0e6f366ed4dbe4b3c51c52d567a9
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
PayFortSDK: e12f79051ae15b2e29536771a45354900180bfcb
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
Polyline: 2a1f29f87f8d9b7de868940f4f76deb8c678a5b1
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
Solar-dev: 4612dc9878b9fed2667d23b327f1d4e54e16e8d0
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
SwiftProtobuf: 9e106a71456f4d3f6a3b0c8fd87ef0be085efc38
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
Turf: aa2ede4298009639d10db36aba1a7ebaad072a5e
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
video_player_avfoundation: 7993f492ae0bd77edaea24d9dc051d8bb2cd7c86
wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e
webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4
ZoomVideoSDK: 94e939820e57a075c5e712559f927017da0de06a
PODFILE CHECKSUM: 8235407385ddd5904afc2563d65406117a51993e
COCOAPODS: 1.16.2

@ -3,7 +3,9 @@ import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/api/http_client_manager.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
@ -209,6 +211,7 @@ class ApiClientImp implements ApiClient {
}
body['TokenID'] = "@dm!n";
// body['PatientID'] = 6424682;
// body['PatientID'] = 4770714;
body['PatientID'] = 4771727;
// body['PatientID'] = 89754444;
@ -216,7 +219,7 @@ class ApiClientImp implements ApiClient {
// body['PatientOutSA'] = 0;
// body['SessionID'] = "45786230487560q";
//VIP Patient: 1181868body:
//VIP Patient: 1181868
// body['IdentificationNo'] = "2235558844";
// body['MobileNo'] = "966533147722";
@ -244,10 +247,13 @@ class ApiClientImp implements ApiClient {
// Handle body encoding based on isBodyPlainText flag
final dynamic requestBody = isBodyPlainText ? body : json.encode(body);
debugPrint("uri: ${Uri.parse(url.trim())}");
var requestBodyJSON = json.encode(body);
// debugPrint("body: $requestBodyJSON", wrapWidth: 2048);
log("body: $requestBodyJSON");
if (kDebugMode) {
debugPrint("uri: ${Uri.parse(url.trim())}");
var requestBodyJSON = json.encode(body);
debugPrint("body: $requestBodyJSON", wrapWidth: 2048);
// log("body: $requestBodyJSON");
}
http.Response response;
try {
@ -256,11 +262,16 @@ class ApiClientImp implements ApiClient {
body: requestBody,
headers: headers,
);
log("response: ${response.body.toString()}");
// log("response: ${response.body.toString()}");
} on SocketException catch (e) {
final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr();
onFailure(message, -1, failureType: ConnectivityFailure(message));
_analytics.errorTracking.log(endPoint, error: "SocketException: $e");
Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
// Clarity.setCustomTag('api_err_request', _sanitizeRequestBody(requestBody));
Clarity.sendCustomEvent('api_failure');
return;
} on http.ClientException catch (e) {
final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr();
@ -271,6 +282,10 @@ class ApiClientImp implements ApiClient {
final message = LocaleKeys.networkTimeout.tr();
onFailure(message, -1, failureType: ConnectivityFailure(message));
_analytics.errorTracking.log(endPoint, error: "TimeoutException");
Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
// Clarity.setCustomTag('api_err_request', _sanitizeRequestBody(requestBody));
Clarity.sendCustomEvent('api_timeout_failure');
return;
} catch (e) {
final message = LocaleKeys.networkUnknownError.tr();
@ -284,6 +299,11 @@ class ApiClientImp implements ApiClient {
if (statusCode < 200 || statusCode >= 400) {
onFailure(LocaleKeys.networkErrorWhileFetching.tr(), statusCode, failureType: StatusCodeFailure(LocaleKeys.networkErrorWhileFetching.tr()));
logApiEndpointError(endPoint, LocaleKeys.networkErrorWhileFetching.tr(), statusCode);
Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
// Clarity.setCustomTag('api_err_request', _sanitizeRequestBody(requestBody));
Clarity.setCustomTag('api_err_status', statusCode.toString());
Clarity.sendCustomEvent('api_status_code_failure');
} else {
var parsed = json.decode(utf8.decode(response.bodyBytes));
if (isAllowAny) {
@ -385,6 +405,11 @@ class ApiClientImp implements ApiClient {
);
logApiEndpointError(endPoint, parsed['message'] ?? parsed['message'], statusCode);
}
Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim()));
// Clarity.setCustomTag('api_err_request', _sanitizeRequestBody(requestBody));
Clarity.setCustomTag('api_err_status', statusCode.toString());
Clarity.sendCustomEvent('api_message_status_failure');
}
} else {
if (parsed['SameClinicApptList'] != null) {
@ -663,4 +688,42 @@ class ApiClientImp implements ApiClient {
logApiEndpointError(String endPoint, error, code) {
_analytics.errorTracking.log(endPoint, error: error);
}
String _getEndofUrl(String url) {
const int clarityLimit = 255;
if (url.length <= clarityLimit) return url;
// Extracts the final 252 characters and prepends '...'
return '...${url.substring(url.length - (clarityLimit - 3))}';
}
String _sanitizeRequestBody(String body) {
if (body.isEmpty) return '';
try {
// Handle standard JSON bodies
final Map<String, dynamic> data = json.decode(body);
// Case-insensitive key matching and removal
data.removeWhere((key, value) => key.toLowerCase() == 'sessionid' || key.toLowerCase() == 'tokenid');
return json.encode(data);
} catch (_) {
// Fallback: Handle Form URL Encoded strings or raw key-value pairs (SessionID=XYZ&TokenID=abc)
String modifiedBody = body;
final sensitiveKeys = ['SessionID', 'TokenID'];
for (var key in sensitiveKeys) {
// Matches "key=value" surrounded by standard query delimiters
final regExp = RegExp(r'' + key + r'=[^&]*&?', caseSensitive: false);
modifiedBody = modifiedBody.replaceAll(regExp, '');
}
// Clean up any trailing trailing ampersands
if (modifiedBody.endsWith('&')) {
modifiedBody = modifiedBody.substring(0, modifiedBody.length - 1);
}
return modifiedBody;
}
}
}

@ -1,4 +1,5 @@
import 'package:amazon_payfort/amazon_payfort.dart';
import 'package:flutter_amazonpaymentservices/environment_type.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
class ApiConsts {
@ -12,7 +13,7 @@ class ApiConsts {
static String rcBaseUrl = 'https://rc.hmg.com/'; // dRC API URL PROD
static String hmgPharmacyApiBaseUrl = 'https://hmgpharmacyapi.hmg.com/'; // symptoms API URL PROD
static String symptomsCheckerApiUAT = '${hmgPharmacyApiBaseUrl}symptomsapi/api/SymptomChecker'; // dRC API URL PROD
static String symptomsCheckerApiUAT = '${hmgPharmacyApiBaseUrl}symptomsapi_live/api/SymptomChecker'; // dRC API URL PROD
static String symptomsCheckerApiLive = '${hmgPharmacyApiBaseUrl}symptomsapi_live/api/SymptomChecker'; // dRC API URL PROD
static String symptomsCheckerApi = '${hmgPharmacyApiBaseUrl}symptomsapi_live/api/SymptomChecker'; // dRC API URL PROD
static String symptomsCheckerScheduleAppointment = '$symptomsCheckerApi/ScheduleAppointment';
@ -23,6 +24,7 @@ class ApiConsts {
static var payFortEnvironment = FortEnvironment.production;
static var applePayMerchantId = "merchant.com.hmgwebservices";
static var payFortEnvironmentType = EnvironmentType.production;
static String SERVICE_URL = 'https://hmgwebservices.com/PayFortWebLive/pages/SendPayFortRequest.aspx'; //Payfort Payment Gateway URL LIVE
// static String SERVICE_URL = 'https://hmgwebservices.com/PayFortWeb/pages/SendPayFortRequest.aspx'; // Payfort Payment Gateway URL UAT
@ -47,6 +49,7 @@ class ApiConsts {
case AppEnvironmentTypeEnum.prod:
baseUrl = "https://hmgwebservices.com/";
payFortEnvironment = FortEnvironment.production;
payFortEnvironmentType = EnvironmentType.production;
applePayMerchantId = "merchant.com.hmgwebservices";
SERVICE_URL = "https://hmgwebservices.com/PayFortWebLive/pages/SendPayFortRequest.aspx";
TAMARA_URL = "https://mdlaboratories.com/tamaralive/Home/Checkout";
@ -62,6 +65,7 @@ class ApiConsts {
case AppEnvironmentTypeEnum.dev:
baseUrl = "https://uat.hmgwebservices.com/";
payFortEnvironment = FortEnvironment.test;
payFortEnvironmentType = EnvironmentType.sandbox;
applePayMerchantId = "merchant.com.hmgwebservices.uat";
SERVICE_URL = 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx';
TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout";
@ -78,6 +82,7 @@ class ApiConsts {
case AppEnvironmentTypeEnum.uat:
baseUrl = "https://uat.hmgwebservices.com/";
payFortEnvironment = FortEnvironment.test;
payFortEnvironmentType = EnvironmentType.sandbox;
applePayMerchantId = "merchant.com.hmgwebservices.uat";
SERVICE_URL = 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx';
TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout";
@ -94,6 +99,7 @@ class ApiConsts {
case AppEnvironmentTypeEnum.preProd:
baseUrl = "https://webservices.hmg.com/";
payFortEnvironment = FortEnvironment.production;
payFortEnvironmentType = EnvironmentType.production;
applePayMerchantId = "merchant.com.hmgwebservices";
SERVICE_URL = "https://hmgwebservices.com/PayFortWebLive/pages/SendPayFortRequest.aspx";
TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout";
@ -110,6 +116,7 @@ class ApiConsts {
case AppEnvironmentTypeEnum.qa:
baseUrl = "https://uat.hmgwebservices.com/";
payFortEnvironment = FortEnvironment.test;
payFortEnvironmentType = EnvironmentType.sandbox;
applePayMerchantId = "merchant.com.hmgwebservices.uat";
SERVICE_URL = 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx';
TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout";
@ -126,6 +133,7 @@ class ApiConsts {
case AppEnvironmentTypeEnum.staging:
baseUrl = "https://uat.hmgwebservices.com/";
payFortEnvironment = FortEnvironment.test;
payFortEnvironmentType = EnvironmentType.sandbox;
applePayMerchantId = "merchant.com.hmgwebservices.uat";
SERVICE_URL = 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx';
TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout";
@ -211,6 +219,9 @@ class ApiConsts {
static final String diagnosis = '$symptomsCheckerApi/GetDiagnosis';
static final String explain = '$symptomsCheckerApi/ExplainDiagnosisResult';
static final String getClinicFromCondition = '$symptomsCheckerApi/GetClinicsByCondition?condition=';
static final String getRationale = '$symptomsCheckerApi/GetRationale';
static final String getTriage = '$symptomsCheckerApi/GetTriage';
static final String getExplanations = '$symptomsCheckerApi/GetExplanations';
//E-REFERRAL SERVICES
static final getAllRelationshipTypes = "Services/Patients.svc/REST/GetAllRelationshipTypes";
@ -290,9 +301,8 @@ class ApiConsts {
static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations";
// ************ static values for Api ****************
// static final double appVersionID = 21.2;
static final double appVersionID = 21.3;
static final double appVersionID = 21.4;
static final int appChannelId = 3;
static final String appIpAddress = "10.20.10.20";
static final String appGeneralId = "Cs2020@2016\$2958";
@ -1008,6 +1018,15 @@ const downloadInvoicePdf = 'Services/Notifications.svc/REST/DownloadInvoiceRepor
const downloadPharmacyInvoicePdf = 'Services/Notifications.svc/REST/DownloadInvoiceReport';
const GET_APPOINTMENT_PARKING_QR = 'Services/outps.svc/rest/pms_getParkingTicketByAppointmentNo';
class ApiKeyConstants {
static final String googleMapsApiKey = 'AIzaSyB6TERnxIr0yJ3qG4ULBZbu0sAD4tGqtng';
}
class AppUrls {
static final hmgTermsUrlEn = 'https://hmg.com/en/Pages/Terms.aspx';
static final hmgPrivacyUrlEn = 'https://hmg.com/en/Pages/Privacy.aspx';
static final hmgTermsUrlAr = 'https://hmg.com/ar/Pages/Terms.aspx';
static final hmgPrivacyUrlAr = 'https://hmg.com/ar/Pages/Privacy.aspx';
}

@ -256,6 +256,7 @@ class AppAssets {
static const String back_top_nav_icon = '$svgBasePath/back_top_nav_icon.svg';
static const String bluetooth = '$svgBasePath/bluetooth.svg';
static const String calendar_filled_icon = '$svgBasePath/calendar_filled_icon.svg';
static const String appointment_parking_icon = '$svgBasePath/appointment_parking_icon.svg';
//smartwatch
static const String watchActivity = '$svgBasePath/watch_activity.svg';

@ -48,6 +48,17 @@ class AppState {
AuthenticatedUser? _authenticatedChildUser;
bool isPaytabsEnabled = false;
bool isPayfortHostedPageEnabled = true;
void setIsPaytabsEnabled(bool value) {
isPaytabsEnabled = value;
// isPaytabsEnabled = true;
}
void setIsPayfortHostedPageEnabled(bool value) {
isPayfortHostedPageEnabled = value;
// isPayfortHostedPageEnabled = true;
}
int? _superUserID;
bool isChildLoggedIn = false;

@ -9,7 +9,7 @@ class DateUtil {
/// convert String To Date function
/// [date] String we want to convert
static DateTime convertStringToDate(String? date) {
if (date == null) return DateTime.now();
if (date == null || date == "null") return DateTime.now();
if (date.isEmpty) return DateTime.now();
const start = "/Date(";
@ -18,7 +18,7 @@ class DateUtil {
final endIndex = date.indexOf(end, startIndex + start.length);
return DateTime.fromMillisecondsSinceEpoch(int.parse(
date.substring(startIndex + start.length, endIndex),
));
), isUtc: true).add(Duration(hours: 3));
}
static DateTime convertStringToDateSaudiTimezone(String date, int projectId) {

@ -49,10 +49,10 @@ _incomingCall(Map<String, dynamic> data) async {
log('the value of the _incomingCall remote message is $data');
// Check if there's already a call in progress to prevent duplicates
if (_isCallInProgress && _currentCallId != null) {
log('⚠️ Call already in progress (ID: $_currentCallId), ignoring duplicate notification');
return;
}
// if (_isCallInProgress && _currentCallId != null) {
// log('⚠️ Call already in progress (ID: $_currentCallId), ignoring duplicate notification');
// return;
// }
String roomID = data['session_id'] ?? '';
String callTypeID = data['AppointmentNo'] ?? '';

@ -26,6 +26,8 @@ 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/dialogs/confirm_dialog.dart';
import 'package:hmg_patient_app_new/widgets/loading_dialog.dart';
import 'package:hmg_patient_app_new/widgets/accessibility/preserve_image_colors.dart';
import 'package:hmg_patient_app_new/widgets/accessibility/preserve_svg_colors.dart';
import 'package:lottie/lottie.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -120,10 +122,8 @@ class Utils {
// ),
// ));
return !isAddHours
? DateFormat('hh:mm a', appState.isArabic() ? "ar_SA" : "en_US")
.format(DateTime.tryParse(startTime.contains("T") ? startTime : convertStringToDateTime(startTime))!.toLocal())
: DateFormat('hh:mm a', appState.isArabic() ? "ar_SA" : "en_US")
.format(DateTime.tryParse(startTime.contains("T") ? startTime : convertStringToDateTime(startTime))!.add(
? DateFormat('hh:mm a', appState.isArabic() ? "ar_SA" : "en_US").format(DateTime.tryParse(startTime.contains("T") ? startTime : convertStringToDateTime(startTime))!.toLocal())
: DateFormat('hh:mm a', appState.isArabic() ? "ar_SA" : "en_US").format(DateTime.tryParse(startTime.contains("T") ? startTime : convertStringToDateTime(startTime))!.add(
Duration(
hours: isAddHours ? 3 : 0,
),
@ -356,12 +356,9 @@ class Utils {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: isSmallWidget ? 0.h : 48.h),
Lottie.asset(AppAnimations.noData,
repeat: false, reverse: false, frameRate: FrameRate(60), width: width.w, height: height.h, fit: BoxFit.fill),
Lottie.asset(AppAnimations.noData, repeat: false, reverse: false, frameRate: FrameRate(60), width: width.w, height: height.h, fit: BoxFit.fill),
SizedBox(height: 16.h),
(noDataText ?? LocaleKeys.noDataAvailable.tr())
.toText14(isBold: true, color: AppColors.greyTextColor, isCenter: true)
.paddingSymmetrical(64.w, 0.h),
(noDataText ?? LocaleKeys.noDataAvailable.tr()).toText14(isBold: true, color: AppColors.greyTextColor, isCenter: true).paddingSymmetrical(64.w, 0.h),
SizedBox(height: 16.h),
callToActionButton
],
@ -373,8 +370,7 @@ class Utils {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Lottie.asset(AppAnimations.loadingAnimation,
repeat: true, reverse: false, frameRate: FrameRate(60), width: 80.h, height: 80.h, fit: BoxFit.fill),
Lottie.asset(AppAnimations.loadingAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 80.h, height: 80.h, fit: BoxFit.fill),
SizedBox(height: 8.h),
(loadingText ?? LocaleKeys.loadingText.tr()).toText16(color: AppColors.blackColor, isCenter: true, isBold: true),
SizedBox(height: 8.h),
@ -400,8 +396,7 @@ class Utils {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Lottie.asset(AppAnimations.errorAnimation,
repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
Lottie.asset(AppAnimations.errorAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
SizedBox(height: 8.h),
(loadingText ?? LocaleKeys.loadingText.tr()).toText16(color: AppColors.blackColor).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 8.h),
@ -502,8 +497,7 @@ class Utils {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Lottie.asset(AppAnimations.warningAnimation,
repeat: false, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
Lottie.asset(AppAnimations.warningAnimation, repeat: false, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
SizedBox(height: 8.h),
(loadingText ?? LocaleKeys.loadingText.tr()).toText15(color: AppColors.blackColor, letterSpacing: 0),
SizedBox(height: 16.h),
@ -623,10 +617,8 @@ class Utils {
}
// Replace HTML line breaks with newlines
var withLineBreaks = htmlString
.replaceAll(RegExp(r'<br\s*\/?>', multiLine: true), '\n')
.replaceAll(RegExp(r'<\/p>', multiLine: true), '\n')
.replaceAll(RegExp(r'<divider>', multiLine: true), '\n');
var withLineBreaks =
htmlString.replaceAll(RegExp(r'<br\s*\/?>', multiLine: true), '\n').replaceAll(RegExp(r'<\/p>', multiLine: true), '\n').replaceAll(RegExp(r'<divider>', multiLine: true), '\n');
// Remove all other HTML tags
var withoutTags = withLineBreaks.replaceAll(RegExp(r'<[^>]*>'), '');
@ -704,20 +696,7 @@ class Utils {
final year = parts[0];
// Map month number to short month name (Hijri months)
const hijriMonthNames = [
'Muharram',
'Safar',
'Rabi I',
'Rabi II',
'Jumada I',
'Jumada II',
'Rajab',
'Sha\'ban',
'Ramadan',
'Shawwal',
'Dhu al-Qi\'dah',
'Dhu al-Hijjah'
];
const hijriMonthNames = ['Muharram', 'Safar', 'Rabi I', 'Rabi II', 'Jumada I', 'Jumada II', 'Rajab', 'Sha\'ban', 'Ramadan', 'Shawwal', 'Dhu al-Qi\'dah', 'Dhu al-Hijjah'];
final monthIndex = int.tryParse(parts[1]) ?? 1;
final month = hijriMonthNames[monthIndex - 1];
@ -771,14 +750,26 @@ class Utils {
double height = 24,
BoxFit fit = BoxFit.cover,
bool applyThemeColor = true,
String? errorAsset,
}) {
final Color? resolvedColor = iconColor ?? (applyThemeColor && AppColors.isDarkMode ? AppColors.textColor : null);
return SvgPicture.asset(
icon,
colorFilter: resolvedColor != null ? ColorFilter.mode(isDisabled ? resolvedColor.withOpacity(0.5) : resolvedColor, BlendMode.srcIn) : null,
width: width,
height: height,
fit: fit,
return PreserveSvgColors(
child: SvgPicture.asset(
icon,
colorFilter: resolvedColor != null ? ColorFilter.mode(isDisabled ? resolvedColor.withValues(alpha: 0.5) : resolvedColor, BlendMode.srcIn) : null,
width: width,
height: height,
fit: fit,
errorBuilder: errorAsset != null
? (context, error, stackTrace) => SvgPicture.asset(
errorAsset,
colorFilter: resolvedColor != null ? ColorFilter.mode(isDisabled ? resolvedColor.withValues(alpha: 0.5) : resolvedColor, BlendMode.srcIn) : null,
width: width,
height: height,
fit: fit,
)
: null,
),
);
}
@ -792,14 +783,16 @@ class Utils {
}) {
final iconH = height ?? 24.h;
final iconW = width ?? 24.w;
return SvgPicture.network(
url,
colorFilter: ColorFilter.mode(
isDisabled ? iconColor.withOpacity(0.5) : iconColor,
BlendMode.srcIn,
return PreserveSvgColors(
child: SvgPicture.network(
url,
colorFilter: ColorFilter.mode(
isDisabled ? iconColor.withValues(alpha: 0.5) : iconColor,
BlendMode.srcIn,
),
width: iconW,
height: iconH,
),
width: iconW,
height: iconH,
);
}
@ -815,14 +808,15 @@ class Utils {
}) {
final iconH = height ?? 24.h;
final iconW = width ?? 24.w;
return Container(
decoration: BoxDecoration(
border: border != null ? Border.all(color: AppColors.whiteColor, width: border) : null,
borderRadius: border != null ? BorderRadius.circular(borderRadius ?? 12.r) : null,
return PreserveImageColors(
child: Container(
decoration: BoxDecoration(
border: border != null ? Border.all(color: AppColors.whiteColor, width: border) : null,
borderRadius: border != null ? BorderRadius.circular(borderRadius ?? 12.r) : null,
),
child: Image.asset(icon, width: iconW, height: iconH, fit: fit),
),
child: Image.asset(icon, width: iconW, height: iconH, fit: fit),
);
// return Image.asset(icon, width: width, height: height, fit: fit, );
}
static Widget buildImgWithAssetsWithBorderRadius({
@ -837,21 +831,23 @@ class Utils {
}) {
final iconH = height ?? 24.h;
final iconW = width ?? 24.w;
return Container(
width: iconW,
height: iconH,
decoration: BoxDecoration(
border: border != null ? Border.all(color: AppColors.whiteColor, width: border) : null,
borderRadius: borderRadius != null ? BorderRadius.circular(borderRadius ?? 12.r) : null,
image: DecorationImage(
image: AssetImage(
icon,
),
fit: fit)),
return PreserveImageColors(
child: Container(
width: iconW,
height: iconH,
decoration: BoxDecoration(
border: border != null ? Border.all(color: AppColors.whiteColor, width: border) : null,
borderRadius: BorderRadius.circular(borderRadius ?? 12.r),
image: DecorationImage(
image: AssetImage(
icon,
),
fit: fit)),
),
);
}
/// Widget to build an SVG from network
/// Widget to build an image from network - automatically preserves colors in visual modes
static Widget buildImgWithNetwork({
required String url,
required Color iconColor,
@ -863,16 +859,18 @@ class Utils {
}) {
final iconH = height ?? 24.h;
final iconW = width ?? 24.w;
return Image.network(
url,
width: iconW,
height: iconH,
fit: fit,
errorBuilder: errorBuilder ??
(_, __, ___) {
//todo_section change the error builder icon that it is returning
return Utils.buildSvgWithAssets(width: iconW, height: iconH, icon: AppAssets.no_visit_icon);
},
return PreserveImageColors(
child: Image.network(
url,
width: iconW,
height: iconH,
fit: fit,
errorBuilder: errorBuilder ??
(_, __, ___) {
//todo_section change the error builder icon that it is returning
return Utils.buildSvgWithAssets(width: iconW, height: iconH, icon: AppAssets.no_visit_icon);
},
),
);
}
@ -881,26 +879,37 @@ class Utils {
spacing: spacing.w,
mainAxisSize: MainAxisSize.max,
children: [
Image.asset(AppAssets.mada, width: width.h, height: height.h),
Image.asset(
AppAssets.tamaraEng,
width: width.h,
height: height.h,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
debugPrint('Failed to load Tamara PNG in payment methods: $error');
return Utils.buildSvgWithAssets(icon: AppAssets.tamara, width: 35.h, height: 35.h, fit: BoxFit.contain);
},
PreserveImageColors(
child: Image.asset(AppAssets.mada, width: width.h, height: height.h),
),
Image.asset(AppAssets.visa, width: width.h, height: height.h),
Image.asset(AppAssets.mastercard, width: width.h, height: height.h),
Platform.isIOS ? Image.asset(AppAssets.applePay, width: width.h, height: height.h) : SizedBox.shrink(),
PreserveImageColors(
child: Image.asset(
AppAssets.tamaraEng,
width: width.h,
height: height.h,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
debugPrint('Failed to load Tamara PNG in payment methods: $error');
return Utils.buildSvgWithAssets(icon: AppAssets.tamara, width: 35.h, height: 35.h, fit: BoxFit.contain);
},
),
),
PreserveImageColors(
child: Image.asset(AppAssets.visa, width: width.h, height: height.h),
),
PreserveImageColors(
child: Image.asset(AppAssets.mastercard, width: width.h, height: height.h),
),
Platform.isIOS
? PreserveImageColors(
child: Image.asset(AppAssets.applePay, width: width.h, height: height.h),
)
: SizedBox.shrink(),
],
);
}
static Widget getPaymentAmountWithSymbol(Widget paymentAmountWidget, Color iconColor, double iconSize,
{bool isSaudiCurrency = true, bool isExpanded = true}) {
static Widget getPaymentAmountWithSymbol(Widget paymentAmountWidget, Color iconColor, double iconSize, {bool isSaudiCurrency = true, bool isExpanded = true}) {
return Row(
mainAxisAlignment: isExpanded ? MainAxisAlignment.spaceBetween : MainAxisAlignment.start,
children: [
@ -955,13 +964,7 @@ class Utils {
// text: NumberFormat.currency(locale: 'en_US', symbol: " ", decimalDigits: 0).format(habibWalletAmount),
// text: " ${(NumberFormat.decimalPattern().format(habibWalletAmount)).toStringAsFixed(2)}",
text: " ${(NumberFormat.decimalPattern().format(habibWalletAmount))}",
style: TextStyle(
color: resolvedTextColor,
fontSize: fontSize ?? 32.f,
letterSpacing: letterSpacing ?? -4,
fontFamily: "Poppins",
fontWeight: fontWeight ?? FontWeight.w600,
height: 1),
style: TextStyle(color: resolvedTextColor, fontSize: fontSize ?? 32.f, letterSpacing: letterSpacing ?? -4, fontFamily: "Poppins", fontWeight: fontWeight ?? FontWeight.w600, height: 1),
),
],
),
@ -1025,12 +1028,7 @@ class Utils {
static PatientDoctorAppointmentList? convertToPatientDoctorAppointmentList(HospitalsModel? hospital) {
if (hospital == null) return null;
return PatientDoctorAppointmentList(
filterName: hospital.name,
distanceInKMs: hospital.distanceInKilometers?.toString(),
projectTopName: hospital.name,
projectBottomName: hospital.name,
model: hospital,
isHMC: hospital.isHMC);
filterName: hospital.name, distanceInKMs: hospital.distanceInKilometers?.toString(), projectTopName: hospital.name, projectBottomName: hospital.name, model: hospital, isHMC: hospital.isHMC);
}
static HospitalsModel? convertToHospitalsModel(PatientDoctorAppointmentList? item) {
@ -1144,4 +1142,30 @@ class Utils {
}
return result;
}
/// Normalize Arabic text for search by handling Hamza variations and removing diacritics
/// This makes searching more flexible for Arabic text
static String normalizeArabicText(String text) {
if (text.isEmpty) return text;
String normalized = text;
// Normalize Hamza variations to a single character 'ا'
normalized = normalized.replaceAll('أ', 'ا'); // Hamza on Alif
normalized = normalized.replaceAll('إ', 'ا'); // Hamza below Alif
normalized = normalized.replaceAll('آ', 'ا'); // Madda on Alif
normalized = normalized.replaceAll('ء', 'ا'); // Hamza alone
normalized = normalized.replaceAll('ؤ', 'و'); // Hamza on Waw
normalized = normalized.replaceAll('ئ', 'ي'); // Hamza on Ya
// Normalize Alif Maqsura to Ya
normalized = normalized.replaceAll('ى', 'ي');
// Remove Arabic diacritics (Tashkeel)
normalized = normalized.replaceAll(RegExp(r'[\u064B-\u065F]'), ''); // Fatha, Damma, Kasra, etc.
normalized = normalized.replaceAll(RegExp(r'[\u0670]'), ''); // Superscript Alif
normalized = normalized.replaceAll(RegExp(r'[\u0640]'), ''); // Tatweel
return normalized;
}
}

@ -127,6 +127,7 @@ extension EmailValidator on String {
Widget toText12(
{bool isEnglishOnly = false,
Color? color,
Color? decorationColor,
bool isUnderLine = false,
TextAlign textAlignment = TextAlign.start,
bool isBold = false,
@ -147,7 +148,7 @@ extension EmailValidator on String {
color: color ?? AppColors.blackColor,
letterSpacing: letterSpacing ?? 0,
height: height,
decorationColor: isUnderLine ? AppColors.blackColor : null,
decorationColor: decorationColor ?? AppColors.blackColor,
decoration: isUnderLine ? TextDecoration.underline : null,
fontFamily: getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins',
),

@ -26,6 +26,7 @@ import 'package:hmg_patient_app_new/features/authentication/models/resp_models/a
import 'package:hmg_patient_app_new/features/authentication/models/resp_models/check_activation_code_resp_model.dart';
import 'package:hmg_patient_app_new/features/authentication/models/resp_models/check_user_staus_nhic_response_model.dart';
import 'package:hmg_patient_app_new/features/authentication/models/resp_models/select_device_by_imei.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart';
@ -1221,6 +1222,8 @@ class AuthenticationViewModel extends ChangeNotifier {
_appState.setAuthenticatedUser(activation.list!.first);
_appState.setPrivilegeModelList(activation.list!.first.listPrivilege!);
_appState.setUserBloodGroup = activation.patientBlodType ?? "N/A";
_appState.setIsPaytabsEnabled(Utils.havePrivilege(120));
_appState.setIsPayfortHostedPageEnabled(Utils.havePrivilege(121));
// Refresh privileges after user switching to ensure correct privileges are loaded
if (isSwitchUser) {
@ -1251,6 +1254,7 @@ class AuthenticationViewModel extends ChangeNotifier {
}
await clearDefaultInputValues();
myAppointmentsVM.setIsAppointmentDataToBeLoaded(true);
getIt.get<HabibWalletViewModel>().setIsWalletAmountToBeLoaded(true);
getIt.get<InsuranceViewModel>().setIsInsuranceDataToBeLoaded(true);
getIt.get<HmgServicesViewModel>().setHasVitalSignDataLoaded(false);
if (isUserAgreedBefore) {
@ -1409,15 +1413,14 @@ class AuthenticationViewModel extends ChangeNotifier {
checkLastLoginStatus(Function() onSuccess) async {
Future.delayed(Duration(seconds: 1), () async {
if (cacheService.getBool(key: CacheConst.quickLoginEnabled) == null) {
if (_appState.getSelectDeviceByImeiRespModelElement != null &&
(_appState.getSelectDeviceByImeiRespModelElement!.logInType == 1 || _appState.getSelectDeviceByImeiRespModelElement!.logInType == 4)) {
final deviceElement = _appState.getSelectDeviceByImeiRespModelElement;
if (deviceElement != null && (deviceElement.logInType == 1 || deviceElement.logInType == 4)) {
phoneNumberController.text = (_appState.getAuthenticatedUser()!.mobileNumber!.startsWith("0")
? _appState.getAuthenticatedUser()!.mobileNumber!.replaceFirst("0", "")
: _appState.getAuthenticatedUser()!.mobileNumber)!;
nationalIdController.text = _appState.getAuthenticatedUser()!.patientIdentificationNo!;
onSuccess();
} else if ((loginTypeEnum == LoginTypeEnum.sms || loginTypeEnum == LoginTypeEnum.whatsapp && _appState.getSelectDeviceByImeiRespModelElement == null) &&
_appState.getAuthenticatedUser() != null) {
} else if ((loginTypeEnum == LoginTypeEnum.sms || loginTypeEnum == LoginTypeEnum.whatsapp && deviceElement == null) && _appState.getAuthenticatedUser() != null) {
phoneNumberController.text = (_appState.getAuthenticatedUser()!.mobileNumber!.startsWith("0")
? _appState.getAuthenticatedUser()!.mobileNumber!.replaceFirst("0", "")
: _appState.getAuthenticatedUser()!.mobileNumber)!;
@ -1668,14 +1671,26 @@ class AuthenticationViewModel extends ChangeNotifier {
patientId: _appState.getSelectDeviceByImeiRespModelElement!.patientId!,
patientType: _appState.getSelectDeviceByImeiRespModelElement!.patientType,
patientOutSa: _appState.getSelectDeviceByImeiRespModelElement!.outSa == true ? 1 : 0,
projectOutSa: _appState.getSelectDeviceByImeiRespModelElement!.outSa,
loginType: loginType,
languageId: _appState.getLanguageID(),
latitude: _appState.userLat,
longitude: _appState.userLong,
mobileNo: _appState.getSelectDeviceByImeiRespModelElement!.mobile!,
patientMobileNumber: int.parse(_appState.getSelectDeviceByImeiRespModelElement!.mobile!),
nationalId: _appState.getSelectDeviceByImeiRespModelElement!.identificationNo)
nationalId: _appState.getSelectDeviceByImeiRespModelElement!.identificationNo,
zipCode: _appState.getSelectDeviceByImeiRespModelElement!.isOther == true
? "0"
: _appState.getSelectDeviceByImeiRespModelElement!.outSa == true
? CountryEnum.unitedArabEmirates.countryCode
: CountryEnum.saudiArabia.countryCode,
isRegister: false,
logInTokenId: "",
searchType: 2,
patientIdentificationId: "0",
otpSendType: loginType)
.toJson());
resultEither.fold((failure) async => await _errorHandlerService.handleError(failure: failure), (apiResponse) async {
if (apiResponse.messageStatus == 1) {
dynamic deviceInfo = apiResponse.data['List_MobileLoginInfo'];
@ -1752,6 +1767,8 @@ class AuthenticationViewModel extends ChangeNotifier {
projectDetailListModel.add(ProjectDetailListModel.fromJson(v));
});
_appState.setProjectsDetailList(projectDetailListModel);
_appState.setIsPaytabsEnabled(Utils.havePrivilege(120));
_appState.setIsPayfortHostedPageEnabled(Utils.havePrivilege(121));
}
},
);

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -139,6 +140,7 @@ class OTPWidgetState extends State<OTPWidget> with SingleTickerProviderStateMixi
@override
void initState() {
Clarity.setCurrentScreenName('OTP Verification Screen');
super.initState();
authVm = context.read<AuthenticationViewModel>();
focusNode = widget.focusNode ?? FocusNode();
@ -449,6 +451,7 @@ class _OTPVerificationScreenState extends State<OTPVerificationScreen> {
@override
void initState() {
Clarity.setCurrentScreenName('OTP Verification Screen');
super.initState();
_otpController = TextEditingController();
_startResendTimer();

@ -372,7 +372,15 @@ class BookAppointmentsViewModel extends ChangeNotifier {
_filteredClinicsList = List.from(clinicsList);
showSortFilterButtons = false;
} else {
_filteredClinicsList = clinicsList.where((clinic) => clinic.clinicDescription?.toLowerCase().contains(query!.toLowerCase()) ?? false).toList();
// Normalize the search query for better Arabic text matching
String normalizedQuery = Utils.normalizeArabicText(query.toLowerCase());
_filteredClinicsList = clinicsList.where((clinic) {
String clinicName = clinic.clinicDescription?.toLowerCase() ?? '';
String normalizedClinicName = Utils.normalizeArabicText(clinicName);
return normalizedClinicName.contains(normalizedQuery);
}).toList();
showSortFilterButtons = query.length >= 3;
}
notifyListeners();
@ -735,7 +743,9 @@ class BookAppointmentsViewModel extends ChangeNotifier {
result.fold(
(failure) async {
onError?.call(selectedClinic.clinicID == 23 ? failure.message : LocaleKeys.noDoctorFound.tr());
if (onError != null) {
onError.call(selectedClinic.clinicID == 23 ? failure.message : LocaleKeys.noDoctorFound.tr());
}
},
(apiResponse) async {
if (apiResponse.messageStatus == 2) {
@ -834,7 +844,7 @@ class BookAppointmentsViewModel extends ChangeNotifier {
Future<void> getDoctorFreeSlots({bool isBookingForLiveCare = false, Function(dynamic)? onSuccess, Function(String)? onError}) async {
docFreeSlots.clear();
DateTime date;
final DateFormat formatter = DateFormat('HH:mm');
final DateFormat formatter = DateFormat('HH:mm', "en-US");
final DateFormat dateFormatter = DateFormat('yyyy-MM-dd');
Map<DateTime, List> _eventsParsed;

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
class LaserBodyParts {
static final LaserBodyParts _instance = LaserBodyParts._internal();
@ -9,11 +9,11 @@ class LaserBodyParts {
factory LaserBodyParts() => _instance;
static Widget image(String assetUri) {
return new SvgPicture.asset(assetUri, fit: BoxFit.cover, width: 88, height: 88);
return Utils.buildSvgWithAssets(icon: assetUri, fit: BoxFit.cover, width: 88, height: 88, applyThemeColor: false);
}
static Widget imagePng(String assetUri) {
return Image.asset(assetUri, fit: BoxFit.cover, width: 88, height: 88);
return Utils.buildImgWithAssets(icon: assetUri, fit: BoxFit.cover, width: 88, height: 88);
}
Map<String, Widget> maleBodyMap = {

@ -117,6 +117,13 @@ class EmergencyServicesViewModel extends ChangeNotifier {
bool historyLoading = false;
OrderDislpay currentlyDisplayedOrder = OrderDislpay.ALL;
bool isAdvanceERBalanceNeedToBeLoaded = true;
setIsAdvanceERBalanceNeedToBeLoaded(bool value) {
isAdvanceERBalanceNeedToBeLoaded = value;
notifyListeners();
}
setSelectedRRTProcedure(RRTProceduresResponseModel procedure) {
selectedRRTProcedure = procedure;
notifyListeners();
@ -331,6 +338,8 @@ class EmergencyServicesViewModel extends ChangeNotifier {
}
Future<void> checkPatientERAdvanceBalance({Function(dynamic)? onSuccess, Function(String)? onError}) async {
if (!isAdvanceERBalanceNeedToBeLoaded) return;
final result = await emergencyServicesRepo.checkPatientERAdvanceBalance();
result.fold(
@ -338,6 +347,7 @@ class EmergencyServicesViewModel extends ChangeNotifier {
(failure) {
patientHasAdvanceERBalance = false;
isERBookAppointment = true;
isAdvanceERBalanceNeedToBeLoaded = false;
if (onSuccess != null) {
onSuccess(failure.message);
}
@ -347,9 +357,11 @@ class EmergencyServicesViewModel extends ChangeNotifier {
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
patientHasAdvanceERBalance = false;
isERBookAppointment = true;
isAdvanceERBalanceNeedToBeLoaded = false;
} else if (apiResponse.messageStatus == 1) {
patientHasAdvanceERBalance = apiResponse.data;
isERBookAppointment = !patientHasAdvanceERBalance;
isAdvanceERBalanceNeedToBeLoaded = false;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);

@ -31,6 +31,7 @@ class HabibWalletViewModel extends ChangeNotifier {
num habibWalletAmount = 0;
num walletRechargeAmount = 0;
String notesText = "";
bool isWalletAmountToBeLoaded = true;
bool isBottomSheetContentLoading = false;
@ -401,10 +402,13 @@ class HabibWalletViewModel extends ChangeNotifier {
}
initHabibWalletProvider() {
isWalletAmountLoading = true;
if (isWalletAmountToBeLoaded) {
isWalletAmountLoading = true;
habibWalletAmount = 0;
isLakumAccountInfoLoading = true;
yahalaAccountNumber = "";
}
isBottomSheetContentLoading = false;
isLakumAccountInfoLoading = true;
habibWalletAmount = 0;
walletRechargeAmount = 0;
selectedRechargeType = 1;
advancePaymentHospitals.clear();
@ -413,7 +417,6 @@ class HabibWalletViewModel extends ChangeNotifier {
fileNumber = '';
depositorName = '';
mobileNumber = '';
yahalaAccountNumber = "";
notifyListeners();
}
@ -474,6 +477,11 @@ class HabibWalletViewModel extends ChangeNotifier {
notifyListeners();
}
setIsWalletAmountToBeLoaded(bool value) {
isWalletAmountToBeLoaded = value;
notifyListeners();
}
String getSelectedRechargeTypeValue() {
switch (selectedRechargeType) {
case 1:
@ -488,6 +496,9 @@ class HabibWalletViewModel extends ChangeNotifier {
}
Future<void> getPatientBalanceAmount({Function(dynamic)? onSuccess, Function(String)? onError}) async {
if (!isWalletAmountToBeLoaded) {
return;
}
isWalletAmountLoading = true;
notifyListeners();
@ -507,6 +518,7 @@ class HabibWalletViewModel extends ChangeNotifier {
habibWalletBalanceList.removeWhere((element) => element.patientAdvanceBalanceAmount == 0);
isWalletAmountLoading = false;
isWalletAmountToBeLoaded = false;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
@ -544,12 +556,7 @@ class HabibWalletViewModel extends ChangeNotifier {
Function(dynamic)? onSuccess,
Function(String)? onError}) async {
final result = await habibWalletRepo.HISCreateAdvancePayment(
paymentMethodName: paymentMethodName,
paidAmount: paidAmount,
paymentReference: paymentReference,
patientID: patientID,
projectID: projectID,
depositorName: depositorName);
paymentMethodName: paymentMethodName, paidAmount: paidAmount, paymentReference: paymentReference, patientID: patientID, projectID: projectID, depositorName: depositorName);
result.fold(
// (failure) async => await errorHandlerService.handleError(failure: failure),
@ -571,8 +578,7 @@ class HabibWalletViewModel extends ChangeNotifier {
);
}
Future<void> addAdvanceNumberRequest(
{required String advanceNumber, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async {
Future<void> addAdvanceNumberRequest({required String advanceNumber, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await habibWalletRepo.addAdvanceNumberRequest(advanceNumber: advanceNumber, paymentReference: paymentReference);
result.fold(
@ -616,15 +622,23 @@ class HabibWalletViewModel extends ChangeNotifier {
}
Future<void> getLakumAccountInformation({Function(dynamic)? onSuccess, Function(String)? onError}) async {
if (!isWalletAmountToBeLoaded) {
return;
}
isLakumAccountInfoLoading = true;
lakumAccountInfo = LakumInquiryInformationResponseModel();
notifyListeners();
final result = await habibWalletRepo.getLakumAccountInformation(
identificationNumber: getIt.get<AppState>().getAuthenticatedUser()!.patientIdentificationNo ?? "");
final result = await habibWalletRepo.getLakumAccountInformation(identificationNumber: getIt.get<AppState>().getAuthenticatedUser()!.patientIdentificationNo ?? "");
result.fold(
(failure) async => await errorHandlerService.handleError(failure: failure),
// (failure) async => await errorHandlerService.handleError(failure: failure),
(failure) async {
yahalaAccountNumber = "0";
isLakumAccountInfoLoading = false;
notifyListeners(); // 675534
},
(apiResponse) async {
if (apiResponse.messageStatus == 2) {
yahalaAccountNumber = "0";

@ -99,7 +99,7 @@ class LakumInquiryInformationResponseModel {
memberName = json['MemberName'];
memberUniversalId = json['MemberUniversalId'];
mobileNumber = json['MobileNumber'];
pointsBalance = json['PointsBalance'];
pointsBalance = json['PointsBalance'] ?? 0;
pointsBalanceAmount = json['PointsBalanceAmount'];
pointsWillBeExpired = json['PointsWillBeExpired'];
prefLang = json['PrefLang'];

@ -121,8 +121,8 @@ class ImmediateLiveCareViewModel extends ChangeNotifier {
}
Future<void> getLiveCareImmediateAppointmentFees({Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result =
await immediateLiveCareRepo.getLiveCareImmediateAppointmentFees(_appState.getAuthenticatedUser()!.age!, _appState.getAuthenticatedUser()!.gender!, immediateLiveCareSelectedClinic.serviceID!);
final result = await immediateLiveCareRepo.getLiveCareImmediateAppointmentFees(
_appState.getAuthenticatedUser()!.age ?? 22, _appState.getAuthenticatedUser()!.gender ?? 1, immediateLiveCareSelectedClinic.serviceID ?? 1);
result.fold(
(failure) async {

@ -25,6 +25,7 @@ class AppointmentRatingViewModel extends ChangeNotifier {
bool isRateClinic = false;
Future<void> getLastRatingAppointment({Function(dynamic)? onSuccess, Function(String)? onError}) async {
appointmentRatedList.clear();
final result = await myAppointmentsRepo.getLastRatingAppointment();
result.fold(

@ -0,0 +1,85 @@
class AppointmentParkingQRResponseModel {
String? admissionDate;
int? admissionNo;
String? appointmentDate;
int? appointmentNo;
int? createdBy;
String? createdOn;
String? gUID;
String? invoiceDate;
int? invoiceNo;
bool? isActive;
bool? isOutInPatient;
int? isPatient;
bool? isUsed;
int? patientID;
int? projectID;
dynamic ticketBase64;
int? ticketID;
String? ticketURL;
AppointmentParkingQRResponseModel(
{this.admissionDate,
this.admissionNo,
this.appointmentDate,
this.appointmentNo,
this.createdBy,
this.createdOn,
this.gUID,
this.invoiceDate,
this.invoiceNo,
this.isActive,
this.isOutInPatient,
this.isPatient,
this.isUsed,
this.patientID,
this.projectID,
this.ticketBase64,
this.ticketID,
this.ticketURL});
AppointmentParkingQRResponseModel.fromJson(Map<String, dynamic> json) {
admissionDate = json['AdmissionDate'];
admissionNo = json['AdmissionNo'];
appointmentDate = json['AppointmentDate'];
appointmentNo = json['AppointmentNo'];
createdBy = json['CreatedBy'];
createdOn = json['CreatedOn'];
gUID = json['GUID'];
invoiceDate = json['InvoiceDate'];
invoiceNo = json['InvoiceNo'];
isActive = json['IsActive'];
isOutInPatient = json['IsOutInPatient'];
isPatient = json['IsPatient'];
isUsed = json['IsUsed'];
patientID = json['PatientID'];
projectID = json['ProjectID'];
// ticketBase64 = json['TicketBase64'].cast<int>();
ticketBase64 = json['TicketBase64String'];
ticketID = json['TicketID'];
ticketURL = json['TicketURL'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['AdmissionDate'] = this.admissionDate;
data['AdmissionNo'] = this.admissionNo;
data['AppointmentDate'] = this.appointmentDate;
data['AppointmentNo'] = this.appointmentNo;
data['CreatedBy'] = this.createdBy;
data['CreatedOn'] = this.createdOn;
data['GUID'] = this.gUID;
data['InvoiceDate'] = this.invoiceDate;
data['InvoiceNo'] = this.invoiceNo;
data['IsActive'] = this.isActive;
data['IsOutInPatient'] = this.isOutInPatient;
data['IsPatient'] = this.isPatient;
data['IsUsed'] = this.isUsed;
data['PatientID'] = this.patientID;
data['ProjectID'] = this.projectID;
data['TicketBase64'] = this.ticketBase64;
data['TicketID'] = this.ticketID;
data['TicketURL'] = this.ticketURL;
return data;
}
}

@ -76,6 +76,7 @@ class PatientAppointmentHistoryResponseModel {
num? patientTaxAmount;
String? doctorNationalityFlagURL;
bool? isClinicReBookingAllowed;
bool? isParkingAvailable;
PatientAppointmentHistoryResponseModel({
this.setupID,
@ -154,6 +155,7 @@ class PatientAppointmentHistoryResponseModel {
this.patientTaxAmount,
this.doctorNationalityFlagURL,
this.isClinicReBookingAllowed,
this.isParkingAvailable,
});
PatientAppointmentHistoryResponseModel.fromJson(Map<String, dynamic> json) {
@ -244,6 +246,7 @@ class PatientAppointmentHistoryResponseModel {
patientTaxAmount = json['PatientTaxAmount'];
doctorNationalityFlagURL = json['DoctorNationalityFlagURL'];
isClinicReBookingAllowed = json['IsClinicReBookingAllowed'];
isParkingAvailable = json['IsParkingAvailable'];
}
Map<String, dynamic> toJson() {
@ -317,6 +320,7 @@ class PatientAppointmentHistoryResponseModel {
data['SMSButtonVisable'] = this.sMSButtonVisable;
data['ServiceID'] = this.serviceID;
data['IsClinicReBookingAllowed'] = this.isClinicReBookingAllowed;
data['IsParkingAvailable'] = this.isParkingAvailable;
return data;
}
}

@ -9,6 +9,7 @@ import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.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/resp_models/appointment_parking_QR_response_model.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/rate_appointment_resp_model.dart';
import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ask_doctor_request_type_response_model.dart';
@ -92,6 +93,8 @@ abstract class MyAppointmentsRepo {
});
Future<Either<Failure, GenericApiModel<List<GetInvoicesListResponseModel>>>> getAppointmentInvoice(num appointmentNum);
Future<Either<Failure, GenericApiModel<AppointmentParkingQRResponseModel>>> getParkingQR(num appointmentNum, int projectID);
}
class MyAppointmentsRepoImp implements MyAppointmentsRepo {
@ -1183,4 +1186,40 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<AppointmentParkingQRResponseModel>>> getParkingQR(num appointmentNum, int projectID) async {
Map<String, dynamic> mapDevice = {"AppointmentNo": appointmentNum, "ProjectID": projectID};
try {
GenericApiModel<AppointmentParkingQRResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
GET_APPOINTMENT_PARKING_QR,
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
AppointmentParkingQRResponseModel appointmentParkingQRResponseModel = AppointmentParkingQRResponseModel.fromJson(response['ParkingTicketsForAppointment'][0]);
apiResponse = GenericApiModel<AppointmentParkingQRResponseModel>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: null,
data: appointmentParkingQRResponseModel,
);
} catch (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()));
}
}
}

@ -1,3 +1,6 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
@ -10,6 +13,7 @@ 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_parking_QR_response_model.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';
@ -100,6 +104,10 @@ class MyAppointmentsViewModel extends ChangeNotifier {
AppointmentRatedResponseModel? appointmentRatedResponseModel;
bool isAppointmentRatedResponseLoading = false;
AppointmentParkingQRResponseModel? appointmentParkingQRResponseModel;
Uint8List? parkingQRImageBytes;
MyAppointmentsViewModel({required this.myAppointmentsRepo, required this.errorHandlerService, required this.appState});
void onTabChange(int index) {
@ -722,6 +730,28 @@ class MyAppointmentsViewModel extends ChangeNotifier {
);
}
Future<void> getParkingQR(num appointmentNum, int projectID, {Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await myAppointmentsRepo.getParkingQR(appointmentNum, projectID);
result.fold(
(failure) async {
if (onError != null) {
onError(failure.message);
}
},
(apiResponse) {
appointmentParkingQRResponseModel = apiResponse.data!;
parkingQRImageBytes = base64Decode(
appointmentParkingQRResponseModel!.ticketBase64.contains(',') ? appointmentParkingQRResponseModel!.ticketBase64.split(',').last : appointmentParkingQRResponseModel!.ticketBase64);
// parkingQRImageBytes = appointmentParkingQRResponseModel!.ticketBase64;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
},
);
}
// Method to force refresh favorite doctors list
void refreshFavouriteDoctors() {
isFavouriteDoctorsDataFetched = false;

@ -1,8 +1,14 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:amazon_payfort/amazon_payfort.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_amazonpaymentservices/flutter_amazonpaymentservices.dart';
import 'package:hmg_patient_app_new/core/api_consts.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/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/payfort_check_payment_status_response_model.dart';
@ -12,6 +18,7 @@ import 'package:hmg_patient_app_new/features/payfort/payfort_repo.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:network_info_plus/network_info_plus.dart';
import 'package:crypto/crypto.dart';
class PayfortViewModel extends ChangeNotifier {
PayfortRepo payfortRepo;
@ -27,6 +34,8 @@ class PayfortViewModel extends ChangeNotifier {
GetTamaraInstallmentsDetailsResponseModel? getTamaraInstallmentsDetailsResponseModel;
bool isTamaraDetailsLoading = false;
SdkTokenResponse? sdkTokenResponse;
PayfortViewModel({required this.payfortRepo, required this.errorHandlerService});
setIsApplePayConfigurationLoading(bool value) {
@ -142,6 +151,100 @@ class PayfortViewModel extends ChangeNotifier {
);
}
String buildManualPayfortSignature({
required String accessCode,
required String deviceId,
required String merchantIdentifier,
required String amount,
required String currency,
required String merchantReference,
required String shaRequestPhrase, // Your secret Sandbox phrase
}) {
// 1. Map all properties in strict ALPHABETICAL order by native key names
// Notice that "amount" and "currency" must come first alphabetically
final parameters = {
"access_code": accessCode,
"device_id": deviceId,
"language": "en",
"merchant_identifier": merchantIdentifier,
"service_command": "SDK_TOKEN",
};
// 2. Concatenate into a single continuous string wrapped in your SHA Phrase
StringBuffer buffer = StringBuffer();
buffer.write(shaRequestPhrase);
parameters.forEach((key, value) {
buffer.write('$key=$value');
});
buffer.write(shaRequestPhrase);
// 3. Convert string to SHA-256 hex signature
var bytes = utf8.encode(buffer.toString());
return sha256.convert(bytes).toString();
}
Future<SdkTokenResponse?> generateSdkResponseForNormalPayment({
String? accessCode,
String? merchantIdentifier,
String? shaType,
String? shaRequestPhrase,
String? merchantReference,
String? amount,
}) async {
SdkTokenResponse? sdkTokenResponse;
try {
// String? deviceId = await _payfort.getDeviceId();
String? deviceId = await FlutterAmazonpaymentservices.getUDID;
// String? deviceId = "79B8F1C1-F8BD-40A1-9FDD-C07B8AC44D17";
/// Step 2: Generate the Signature
SdkTokenRequest tokenRequest = SdkTokenRequest(
accessCode: accessCode!,
deviceId: deviceId ?? '',
merchantIdentifier: merchantIdentifier!,
);
// String? signature = await _payfort.generateSignature(
// shaType: shaType!,
// concatenatedString: tokenRequest.toConcatenatedString(shaRequestPhrase!),
// );
String? signature = buildManualPayfortSignature(
accessCode: accessCode,
deviceId: deviceId!,
merchantIdentifier: merchantIdentifier,
amount: "10000",
currency: "SAR",
merchantReference: merchantReference!,
shaRequestPhrase: shaRequestPhrase!,
);
tokenRequest = tokenRequest.copyWith(signature: signature);
this.sdkTokenResponse = SdkTokenResponse();
/// Step 3: Generate the SDK Token
final result = await payfortRepo.generateSdkSignatureFromAPI(tokenRequest: tokenRequest);
result.fold(
(failure) async => await errorHandlerService.handleError(failure: failure),
(apiResponse) {
if (apiResponse.messageStatus == 2) {
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
} else if (apiResponse.messageStatus == 1) {
// payfortProjectDetailsRespModel = apiResponse.data!;
sdkTokenResponse = apiResponse.data;
this.sdkTokenResponse = sdkTokenResponse;
notifyListeners();
}
},
);
} catch (e) {
log("Error here: ${e.toString()}");
}
return sdkTokenResponse;
}
Future<SdkTokenResponse?> _generateSdkResponse({
String? applePayAccessCode,
String? merchantIdentifier,
@ -187,6 +290,74 @@ class PayfortViewModel extends ChangeNotifier {
return sdkTokenResponse;
}
Future<void> initiatePaymentWithCard( // var request, {
{
SucceededCallback? onSucceeded,
FailedCallback? onFailed,
String countryIsoCode = "SA",
String transactionID = "",
String amount = "0",
String fileNumber = "0",
}) async {
dynamic paymentResult;
try {
await generateSdkResponseForNormalPayment(
accessCode: payfortProjectDetailsRespModel!.accessCode,
merchantIdentifier: payfortProjectDetailsRespModel!.merchantIdentifier,
shaRequestPhrase: payfortProjectDetailsRespModel!.shaRequest,
shaType: "SHA-256",
merchantReference: transactionID,
amount: amount);
// Force a 100ms microtask buffer to isolate the UI rendering channel
await Future.delayed(const Duration(milliseconds: 350));
var request = {
"command": "PURCHASE",
"merchant_reference": transactionID, // Completely alphanumeric testing reference
"amount": Platform.isAndroid ? (num.parse(amount) * 100).toInt() : (num.parse(amount) * 100).toInt().toString(),
// "amount": "100000",
"currency": getIt.get<AppState>().getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED",
"language": "en",
"customer_email": "CustID_$fileNumber@HMG.com",
"sdk_token": sdkTokenResponse!.sdkToken,
"customer_name": "${getIt.get<AppState>().getAuthenticatedUser()!.firstName} ${getIt.get<AppState>().getAuthenticatedUser()!.lastName}",
};
LoaderBottomSheet.hideLoader();
paymentResult = await FlutterAmazonpaymentservices.normalPay(request, ApiConsts.payFortEnvironmentType, isShowResponsePage: false);
final paymentMap = Map<String, dynamic>.from(paymentResult);
String responseCode = paymentMap['response_code'] ?? "";
print("Payment Success Data: ${paymentMap.toString()}");
if (responseCode.startsWith("14") || responseCode == "20064") {
if (onSucceeded != null) {
onSucceeded(PayFortResult.fromMap(paymentMap));
}
} else if (responseCode == "13000") {
// Push user to 3D Secure Web View
} else {
// Treat as a payment rejection/technical failure
if (onFailed != null) {
onFailed(PayFortFailureResult.fromMap(paymentMap));
}
}
} on PlatformException catch (e) {
// If the error is the MethodChannel duplicate reply bug, handle it safely
if (e.message != null && e.message!.contains("Reply already submitted")) {
print("Swallowed native channel race-condition crash safely.");
return;
}
// Process real payment rejection errors here
print("Actual Payment Failure: ${e.details.toString()}");
} catch (error) {
print("Generic Error: $error");
}
}
Future<void> paymentWithApplePay({
SucceededCallback? onSucceeded,
FailedCallback? onFailed,
@ -257,7 +428,6 @@ class PayfortViewModel extends ChangeNotifier {
onError!(failure.message);
},
(apiResponse) {
log(apiResponse.data);
if (onSuccess != null) {
onSuccess(apiResponse);
}

@ -1,5 +1,5 @@
class PaytabsTransactionResponseModel {
int? tranTotal;
dynamic tranTotal;
String? transactionReference;
String? cartCurrency;
String? cartDescription;
@ -13,10 +13,10 @@ class PaytabsTransactionResponseModel {
bool? isAuthorized;
String? trace;
dynamic cartAmount;
int? merchantId;
int? profileId;
dynamic merchantId;
dynamic profileId;
bool? isProcessed;
int? serviceId;
dynamic serviceId;
PaymentInfo? paymentInfo;
bool? isSuccess;
@ -98,8 +98,8 @@ class PaytabsTransactionResponseModel {
class PaymentInfo {
String? cardScheme;
String? cardType;
int? expiryMonth;
int? expiryYear;
dynamic expiryMonth;
dynamic expiryYear;
String? paymentDescription;
String? paymentMethod;

@ -1,8 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_paytabs_bridge/BaseBillingShippingInfo.dart';
import 'package:flutter_paytabs_bridge/IOSThemeConfiguration.dart';
import 'package:flutter_paytabs_bridge/PaymentSDKNetworks.dart';
import 'package:flutter_paytabs_bridge/PaymentSdkApms.dart';
import 'package:flutter_paytabs_bridge/PaymentSdkConfigurationDetails.dart';
import 'package:flutter_paytabs_bridge/PaymentSdkLocale.dart';
import 'package:flutter_paytabs_bridge/PaymentSdkTokenFormat.dart';
import 'package:flutter_paytabs_bridge/PaymentSdkTokeniseType.dart';
import 'package:flutter_paytabs_bridge/PaymentSdkTransactionType.dart';
import 'package:flutter_paytabs_bridge/flutter_paytabs_bridge.dart';
@ -21,7 +24,7 @@ class PayTabsViewModel extends ChangeNotifier {
required this.appState,
});
setPaymentConfiguration(String paymentDescription, num amount) {
setPaymentConfiguration(String paymentDescription, num amount, {bool isAddCard = false}) {
String cartID = DateTime.now().millisecondsSinceEpoch.toString();
paymentConfiguration = PaymentSdkConfigurationDetails(
profileId: PaymentSdkDefaultConfig.profileID,
@ -62,7 +65,8 @@ class PayTabsViewModel extends ChangeNotifier {
"12626"),
alternativePaymentMethods: [],
linkBillingNameWithCardHolderName: true,
simplifyApplePayValidation: true);
simplifyApplePayValidation: false,
);
paymentConfiguration.iOSThemeConfigurations = IOSThemeConfigurations(
// logoImage: "assets/images/png/hmg_logo.png",
@ -84,17 +88,79 @@ class PayTabsViewModel extends ChangeNotifier {
placeholderColorDark: "2E3039",
buttonColor: "ED1C2B",
buttonColorDark: "ED1C2B",
// buttonFont: "Poppins",
inputsCornerRadius: 12
// buttonFont: "Poppins",
);
paymentConfiguration.tokeniseType = PaymentSdkTokeniseType.USER_OPTIONAL_DEFAULT_ON;
paymentConfiguration.tokenFormat = PaymentSdkTokenFormat.Hex32Format;
notifyListeners();
}
setAlternatePaymentConfiguration(String paymentDescription, num amount, {bool isAddCard = false}) {
String cartID = DateTime.now().millisecondsSinceEpoch.toString();
paymentConfiguration = PaymentSdkConfigurationDetails(
profileId: PaymentSdkDefaultConfig.profileID,
serverKey: PaymentSdkDefaultConfig.serverKey,
clientKey: PaymentSdkDefaultConfig.clientKey,
transactionType: PaymentSdkTransactionType.SALE,
cartId: cartID.substring((cartID.length - 5), cartID.length),
cartDescription: paymentDescription,
merchantName: PaymentSdkDefaultConfig.defaultMerchantName,
locale: appState.isArabic() ? PaymentSdkLocale.AR : PaymentSdkLocale.EN,
screentTitle: paymentDescription,
amount: amount.toDouble(),
showBillingInfo: false,
forceShippingInfo: false,
currencyCode: PaymentSdkDefaultConfig.defaultCurrency,
merchantCountryCode: PaymentSdkDefaultConfig.defaultMerchantCountryCode,
billingDetails: BillingDetails("${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" ?? "HMG Patient",
"CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com", appState.getAuthenticatedUser()!.mobileNumber ?? "0000000000", "Riyadh", "SA", "Riyadh", "Riyadh", "12626"),
shippingDetails: ShippingDetails("${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" ?? "HMG Patient",
"CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com", appState.getAuthenticatedUser()!.mobileNumber ?? "0000000000", "Riyadh", "SA", "Riyadh", "Riyadh", "12626"),
alternativePaymentMethods: [PaymentSdkAPms.TAMARA],
linkBillingNameWithCardHolderName: false,
simplifyApplePayValidation: true,
);
paymentConfiguration.tokeniseType = PaymentSdkTokeniseType.MERCHANT_MANDATORY;
paymentConfiguration.iOSThemeConfigurations = IOSThemeConfigurations(
// logoImage: "assets/images/png/hmg_logo.png",
backgroundColor: "ffffff",
backgroundColorDark: "ffffff",
secondaryColor: "191919",
secondaryColorDark: "191919",
primaryColor: "ffffff",
primaryColorDark: "ffffff",
// primaryFont: "Poppins",
titleFontColor: "2E3039",
titleFontColorDark: "2E3039",
// titleFont: "Poppins",
primaryFontColor: "2E3039",
primaryFontColorDark: "2E3039",
inputFieldBackgroundColor: "ffffff",
inputFieldBackgroundColorDark: "ffffff",
placeholderColor: "2E3039",
placeholderColorDark: "2E3039",
buttonColor: "ED1C2B",
buttonColorDark: "ED1C2B",
inputsCornerRadius: 12
// buttonFont: "Poppins",
);
paymentConfiguration.tokeniseType = PaymentSdkTokeniseType.USER_OPTIONAL_DEFAULT_ON;
paymentConfiguration.tokenFormat = PaymentSdkTokenFormat.Hex32Format;
notifyListeners();
}
startApplePayPayment({Function(PaytabsTransactionResponseModel)? onSuccess, Function(String)? onError}) {
FlutterPaytabsBridge.startApplePayPayment(paymentConfiguration, (event) {
startApplePayPayment({Function(PaytabsTransactionResponseModel)? onSuccess, Function(String)? onError}) async {
bool _callbackFired = false; // prevent duplicate callback invocations
await FlutterPaytabsBridge.startApplePayPayment(paymentConfiguration, (event) {
if (_callbackFired) return; // ignore any subsequent callback calls
final transactionDetails = event["data"] as Map<Object?, Object?>?;
if (event["status"] == "success") {
_callbackFired = true;
// Cast Map<Object?, Object?> to Map<String, dynamic>
final data = Map<String, dynamic>.from(event["data"] as Map);
paytabsTransactionResponseModel = PaytabsTransactionResponseModel.fromJson(data);
@ -110,11 +176,13 @@ class PayTabsViewModel extends ChangeNotifier {
onError!(errorMessage);
}
} else if (event["status"] == "error") {
_callbackFired = true;
final errorMessage = event["message"] ?? "An error occurred";
debugPrint("Error occurred in transaction: $errorMessage");
debugPrint("Full error event: $event");
onError!(errorMessage);
} else if (event["status"] == "event") {
// Intermediate SDK events – do NOT mark _callbackFired, just log
final eventMessage = event["message"] ?? "Event occurred";
debugPrint("Event occurred: $eventMessage");
}
@ -122,9 +190,14 @@ class PayTabsViewModel extends ChangeNotifier {
}
startCardPayment({Function(PaytabsTransactionResponseModel)? onSuccess, Function(String)? onError}) {
bool _callbackFired = false; // prevent duplicate callback invocations
FlutterPaytabsBridge.startCardPayment(paymentConfiguration, (event) {
if (_callbackFired) return; // ignore any subsequent callback calls
final transactionDetails = event["data"] as Map<Object?, Object?>?;
if (event["status"] == "success") {
_callbackFired = true;
// Cast Map<Object?, Object?> to Map<String, dynamic>
final data = Map<String, dynamic>.from(event["data"] as Map);
paytabsTransactionResponseModel = PaytabsTransactionResponseModel.fromJson(data);
@ -140,11 +213,48 @@ class PayTabsViewModel extends ChangeNotifier {
onError!(errorMessage);
}
} else if (event["status"] == "error") {
_callbackFired = true;
final errorMessage = event["message"] ?? "An error occurred";
debugPrint("Error occurred in transaction: $errorMessage");
debugPrint("Full error event: $event");
onError!(errorMessage);
} else if (event["status"] == "event") {
// Intermediate SDK events – do NOT mark _callbackFired, just log
final eventMessage = event["message"] ?? "Event occurred";
debugPrint("Event occurred: $eventMessage");
}
});
}
startAlternatePayment({Function(PaytabsTransactionResponseModel)? onSuccess, Function(String)? onError}) {
bool _callbackFired = false; // prevent duplicate callback invocations
FlutterPaytabsBridge.startAlternativePaymentMethod(paymentConfiguration, (event) {
if (_callbackFired) return; // ignore any subsequent callback calls
final transactionDetails = event["data"] as Map<Object?, Object?>?;
if (event["status"] == "success") {
_callbackFired = true;
// Cast Map<Object?, Object?> to Map<String, dynamic>
final data = Map<String, dynamic>.from(event["data"] as Map);
paytabsTransactionResponseModel = PaytabsTransactionResponseModel.fromJson(data);
if (paytabsTransactionResponseModel.isSuccess!) {
onSuccess!(paytabsTransactionResponseModel);
} else {
// Transaction was processed but failed
final reason = transactionDetails?["payResponseReturn"] ?? transactionDetails?["responseMessage"] ?? transactionDetails?["message"] ?? "Unknown error";
final responseCode = transactionDetails?["responseCode"] ?? "";
final errorMessage = responseCode.toString().isNotEmpty ? "Transaction failed (Code: $responseCode): $reason" : "Transaction failed: $reason";
onError!(errorMessage);
}
} else if (event["status"] == "error") {
_callbackFired = true;
final errorMessage = event["message"] ?? "An error occurred";
debugPrint("Error occurred in transaction: $errorMessage");
debugPrint("Full error event: $event");
onError!(errorMessage);
} else if (event["status"] == "event") {
// Intermediate SDK events – do NOT mark _callbackFired, just log
final eventMessage = event["message"] ?? "Event occurred";
debugPrint("Event occurred: $eventMessage");
}

@ -323,7 +323,7 @@ class PrescriptionsViewModel extends ChangeNotifier {
isGmsAvailable: getIt.get<AppState>().isGMSAvailable,
),
direction: AxisDirection.down),
);
) ?? false;
print("Location Selected: $result");
if (result) {
LocationViewModel locationViewModel = getIt.get<LocationViewModel>();

@ -13,9 +13,12 @@ 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:screen_brightness/screen_brightness.dart';
class ProfileSettingsViewModel extends ChangeNotifier {
static const String _darkModeKey = 'is_dark_mode';
static const String _fontSizeStepKey = 'accessibility_font_size_step';
static const String _visualModeKey = 'accessibility_visual_mode';
final CacheService _cacheService;
final ProfileSettingsRepo profileSettingsRepo;
@ -25,6 +28,13 @@ class ProfileSettingsViewModel extends ChangeNotifier {
bool _isDarkMode = false;
bool get isDarkMode => _isDarkMode;
// ── Accessibility settings ───────────────────────────────────────────
int _fontSizeStep = 2; // 0: Small, 1: Small-Medium, 2: Medium (default), 3: Medium-Large, 4: Large
int get fontSizeStep => _fontSizeStep;
String _visualMode = 'off'; // off, invert, dim, highContrast
String get visualMode => _visualMode;
// ── Update email state ───────────────────────────────────────────────
bool isUpdateEmailLoading = false;
bool isUpdateEmailSuccess = false;
@ -163,6 +173,68 @@ class ProfileSettingsViewModel extends ChangeNotifier {
notifyListeners();
}
// ── Accessibility settings ───────────────────────────────────────────
/// Load accessibility settings at app startup
void loadAccessibilitySettings() {
_fontSizeStep = _cacheService.getInt(key: _fontSizeStepKey) ?? 2;
_visualMode = _cacheService.getString(key: _visualModeKey) ?? 'off';
// Apply brightness based on saved visual mode
_applyBrightnessForMode(_visualMode);
// No notifyListeners() here — we are called before build.
}
/// Update font size step and save to cache
void setFontSizeStep(int step) {
_fontSizeStep = step;
_cacheService.saveInt(key: _fontSizeStepKey, value: step);
notifyListeners();
}
/// Update visual mode and save to cache
void setVisualMode(String mode) {
_visualMode = mode;
_cacheService.saveString(key: _visualModeKey, value: mode);
notifyListeners();
}
/// Apply screen brightness based on visual mode
void _applyBrightnessForMode(String mode) async {
try {
final screenBrightness = ScreenBrightness();
if (mode == 'dim') {
// Reduce brightness to 40% for dim mode
await screenBrightness.setScreenBrightness(0.4);
} else {
// For other modes, reset to system brightness
await screenBrightness.resetScreenBrightness();
}
} catch (e) {
print('Error applying brightness for mode: $e');
}
}
/// Get font scale multiplier based on current font size step
double getFontScaleMultiplier() {
switch (_fontSizeStep) {
case 0:
return 0.85; // Small
case 1:
return 0.95; // Small-Medium
case 2:
return 1.0; // Medium (default)
case 3:
return 1.15; // Medium-Large
case 4:
return 1.3; // Large
default:
return 1.0;
}
}
// ── Update patient info ──────────────────────────────────────────────
Future<void> updatePatientInfo({

@ -0,0 +1,147 @@
class ExplanationsResponseModel {
final ExplanationsDataDetails? dataDetails;
ExplanationsResponseModel({this.dataDetails});
factory ExplanationsResponseModel.fromJson(Map<String, dynamic> json) {
return ExplanationsResponseModel(
dataDetails: json['dataDetails'] != null
? ExplanationsDataDetails.fromJson(json['dataDetails'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'dataDetails': dataDetails?.toJson(),
};
}
}
class ExplanationsDataDetails {
final List<ExplanationEvidence>? supportingEvidence;
final List<ExplanationEvidence>? conflictingEvidence;
final List<ExplanationEvidence>? unconfirmedEvidence;
final String? message;
final List<String>? errorList;
final int? id;
final String? language;
final String? generalId;
final String? createDate;
final String? lastEditDate;
final String? createdBy;
final String? lastEditBy;
final bool? active;
final int? sortOrder;
final int? userType;
final String? userId;
ExplanationsDataDetails({
this.supportingEvidence,
this.conflictingEvidence,
this.unconfirmedEvidence,
this.message,
this.errorList,
this.id,
this.language,
this.generalId,
this.createDate,
this.lastEditDate,
this.createdBy,
this.lastEditBy,
this.active,
this.sortOrder,
this.userType,
this.userId,
});
factory ExplanationsDataDetails.fromJson(Map<String, dynamic> json) {
return ExplanationsDataDetails(
supportingEvidence: json['supporting_evidence'] != null
? (json['supporting_evidence'] as List)
.map((item) => ExplanationEvidence.fromJson(item))
.toList()
: null,
conflictingEvidence: json['conflicting_evidence'] != null
? (json['conflicting_evidence'] as List)
.map((item) => ExplanationEvidence.fromJson(item))
.toList()
: null,
unconfirmedEvidence: json['unconfirmed_evidence'] != null
? (json['unconfirmed_evidence'] as List)
.map((item) => ExplanationEvidence.fromJson(item))
.toList()
: null,
message: json['Message'],
errorList: json['ErrorList'] != null
? List<String>.from(json['ErrorList'])
: null,
id: json['Id'],
language: json['language'],
generalId: json['generalId'],
createDate: json['CreateDate'],
lastEditDate: json['LastEditDate'],
createdBy: json['CreatedBy'],
lastEditBy: json['LastEditBy'],
active: json['Active'],
sortOrder: json['SortOrder'],
userType: json['userType'],
userId: json['userId'],
);
}
Map<String, dynamic> toJson() {
return {
'supporting_evidence': supportingEvidence?.map((item) => item.toJson()).toList(),
'conflicting_evidence': conflictingEvidence?.map((item) => item.toJson()).toList(),
'unconfirmed_evidence': unconfirmedEvidence?.map((item) => item.toJson()).toList(),
'Message': message,
'ErrorList': errorList,
'Id': id,
'language': language,
'generalId': generalId,
'CreateDate': createDate,
'LastEditDate': lastEditDate,
'CreatedBy': createdBy,
'LastEditBy': lastEditBy,
'Active': active,
'SortOrder': sortOrder,
'userType': userType,
'userId': userId,
};
}
}
class ExplanationEvidence {
final String? id;
final String? name;
final String? commonName;
ExplanationEvidence({
this.id,
this.name,
this.commonName,
});
factory ExplanationEvidence.fromJson(Map<String, dynamic> json) {
return ExplanationEvidence(
id: json['id'],
name: json['name'],
commonName: json['common_name'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'common_name': commonName,
};
}
// Helper method to get display name
String getDisplayName() {
return commonName ?? name ?? '';
}
}

@ -0,0 +1,176 @@
class RationaleResponseModel {
final RationaleDataDetails? dataDetails;
RationaleResponseModel({this.dataDetails});
factory RationaleResponseModel.fromJson(Map<String, dynamic> json) {
return RationaleResponseModel(
dataDetails: json['dataDetails'] != null
? RationaleDataDetails.fromJson(json['dataDetails'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'dataDetails': dataDetails?.toJson(),
};
}
}
class RationaleDataDetails {
final String? type;
final List<RationaleObservationParam>? observationParams;
final List<RationaleConditionParam>? conditionParams;
final String? message;
final List<String>? errorList;
final int? id;
final String? language;
final String? generalId;
final String? createDate;
final String? lastEditDate;
final String? createdBy;
final String? lastEditBy;
final bool? active;
final int? sortOrder;
final int? userType;
final String? userId;
RationaleDataDetails({
this.type,
this.observationParams,
this.conditionParams,
this.message,
this.errorList,
this.id,
this.language,
this.generalId,
this.createDate,
this.lastEditDate,
this.createdBy,
this.lastEditBy,
this.active,
this.sortOrder,
this.userType,
this.userId,
});
factory RationaleDataDetails.fromJson(Map<String, dynamic> json) {
return RationaleDataDetails(
type: json['type'],
observationParams: json['observation_params'] != null
? (json['observation_params'] as List)
.map((item) => RationaleObservationParam.fromJson(item))
.toList()
: null,
conditionParams: json['condition_params'] != null
? (json['condition_params'] as List)
.map((item) => RationaleConditionParam.fromJson(item))
.toList()
: null,
message: json['Message'],
errorList: json['ErrorList'] != null
? List<String>.from(json['ErrorList'])
: null,
id: json['Id'],
language: json['language'],
generalId: json['generalId'],
createDate: json['CreateDate'],
lastEditDate: json['LastEditDate'],
createdBy: json['CreatedBy'],
lastEditBy: json['LastEditBy'],
active: json['Active'],
sortOrder: json['SortOrder'],
userType: json['userType'],
userId: json['userId'],
);
}
Map<String, dynamic> toJson() {
return {
'type': type,
'observation_params': observationParams?.map((item) => item.toJson()).toList(),
'condition_params': conditionParams?.map((item) => item.toJson()).toList(),
'Message': message,
'ErrorList': errorList,
'Id': id,
'language': language,
'generalId': generalId,
'CreateDate': createDate,
'LastEditDate': lastEditDate,
'CreatedBy': createdBy,
'LastEditBy': lastEditBy,
'Active': active,
'SortOrder': sortOrder,
'userType': userType,
'userId': userId,
};
}
}
class RationaleObservationParam {
final String? id;
final String? name;
final String? commonName;
RationaleObservationParam({
this.id,
this.name,
this.commonName,
});
factory RationaleObservationParam.fromJson(Map<String, dynamic> json) {
return RationaleObservationParam(
id: json['id'],
name: json['name'],
commonName: json['common_name'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'common_name': commonName,
};
}
// Helper method to get display name
String getDisplayName() {
return commonName ?? name ?? '';
}
}
class RationaleConditionParam {
final String? id;
final String? name;
final String? commonName;
RationaleConditionParam({
this.id,
this.name,
this.commonName,
});
factory RationaleConditionParam.fromJson(Map<String, dynamic> json) {
return RationaleConditionParam(
id: json['id'],
name: json['name'],
commonName: json['common_name'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'common_name': commonName,
};
}
// Helper method to get display name
String getDisplayName() {
return commonName ?? name ?? '';
}
}

@ -0,0 +1,151 @@
class TriageLevelResponseModel {
final TriageLevelDataDetails? dataDetails;
TriageLevelResponseModel({this.dataDetails});
factory TriageLevelResponseModel.fromJson(Map<String, dynamic> json) {
return TriageLevelResponseModel(
dataDetails: json['dataDetails'] != null
? TriageLevelDataDetails.fromJson(json['dataDetails'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'dataDetails': dataDetails?.toJson(),
};
}
}
class TriageLevelDataDetails {
final String? triageLevel;
final List<SeriousEvidence>? serious;
final String? rootCause;
final bool? teleconsultationApplicable;
final String? message;
final List<String>? errorList;
final int? id;
final String? language;
final String? generalId;
final String? createDate;
final String? lastEditDate;
final String? createdBy;
final String? lastEditBy;
final bool? active;
final int? sortOrder;
final int? userType;
final String? userId;
TriageLevelDataDetails({
this.triageLevel,
this.serious,
this.rootCause,
this.teleconsultationApplicable,
this.message,
this.errorList,
this.id,
this.language,
this.generalId,
this.createDate,
this.lastEditDate,
this.createdBy,
this.lastEditBy,
this.active,
this.sortOrder,
this.userType,
this.userId,
});
factory TriageLevelDataDetails.fromJson(Map<String, dynamic> json) {
return TriageLevelDataDetails(
triageLevel: json['triage_level'],
serious: json['serious'] != null
? (json['serious'] as List)
.map((item) => SeriousEvidence.fromJson(item))
.toList()
: null,
rootCause: json['root_cause'],
teleconsultationApplicable: json['teleconsultation_applicable'],
message: json['Message'],
errorList: json['ErrorList'] != null
? List<String>.from(json['ErrorList'])
: null,
id: json['Id'],
language: json['language'],
generalId: json['generalId'],
createDate: json['CreateDate'],
lastEditDate: json['LastEditDate'],
createdBy: json['CreatedBy'],
lastEditBy: json['LastEditBy'],
active: json['Active'],
sortOrder: json['SortOrder'],
userType: json['userType'],
userId: json['userId'],
);
}
Map<String, dynamic> toJson() {
return {
'triage_level': triageLevel,
'serious': serious?.map((item) => item.toJson()).toList(),
'root_cause': rootCause,
'teleconsultation_applicable': teleconsultationApplicable,
'Message': message,
'ErrorList': errorList,
'Id': id,
'language': language,
'generalId': generalId,
'CreateDate': createDate,
'LastEditDate': lastEditDate,
'CreatedBy': createdBy,
'LastEditBy': lastEditBy,
'Active': active,
'SortOrder': sortOrder,
'userType': userType,
'userId': userId,
};
}
}
class SeriousEvidence {
final String? id;
final String? name;
final String? commonName;
final String? seriousness;
final bool? isEmergency;
SeriousEvidence({
this.id,
this.name,
this.commonName,
this.seriousness,
this.isEmergency,
});
factory SeriousEvidence.fromJson(Map<String, dynamic> json) {
return SeriousEvidence(
id: json['id'],
name: json['name'],
commonName: json['common_name'],
seriousness: json['seriousness'],
isEmergency: json['is_emergency'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'common_name': commonName,
'seriousness': seriousness,
'is_emergency': isEmergency,
};
}
// Helper method to get display name
String getDisplayName() {
return commonName ?? name ?? '';
}
}

@ -91,18 +91,28 @@ class TriageDataDetails {
class TriageQuestion {
final int? type;
final String? text;
final String? explication;
final List<String>? instruction;
final List<TriageQuestionItem>? items;
TriageQuestion({
this.type,
this.text,
this.explication,
this.instruction,
this.items,
});
factory TriageQuestion.fromJson(Map<String, dynamic> json) {
// TESTING: Add dummy instructions to every question for testing UI
return TriageQuestion(
type: json['type'],
text: json['text'],
explication: json['explication'],
// Use dummy if null
instruction: json['instruction'] != null ? List<String>.from(json['instruction']) : [],
// Use dummy if null
items: json['items'] != null ? (json['items'] as List).map((item) => TriageQuestionItem.fromJson(item)).toList() : null,
);
}
@ -111,9 +121,18 @@ class TriageQuestion {
return {
'type': type,
'text': text,
'explication': explication,
'instruction': instruction,
'items': items?.map((item) => item.toJson()).toList(),
};
}
/// Check if instruction OR explication is available
bool get hasInstructions {
final hasInstruction = instruction != null && instruction!.isNotEmpty;
final hasExplication = explication != null && explication!.isNotEmpty;
return hasInstruction || hasExplication;
}
}
class TriageQuestionItem {

@ -8,10 +8,13 @@ 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/features/symptoms_checker/models/req_models/schedule_appointment_request_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/explanations_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/get_clinic_details_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/rationale_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/schedule_appointment_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_level_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
@ -40,7 +43,7 @@ abstract class SymptomsCheckerRepo {
Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>> getSuggestions({
required int age,
required int gender,
required List<String> evidenceIds,
required List<Map<String, dynamic>> evidenceList,
required String language,
required String userSessionToken,
required String sessionId,
@ -49,7 +52,7 @@ abstract class SymptomsCheckerRepo {
Future<Either<Failure, GenericApiModel<TriageDataDetails>>> getDiagnosisForTriage({
required int age,
required int gender,
required List<String> evidenceIds,
required List<Map<String, dynamic>> initialEvidenceList,
List<Map<String, String>>? triageEvidence,
required String language,
required String userSessionToken,
@ -66,6 +69,34 @@ abstract class SymptomsCheckerRepo {
required ScheduleAppointmentRequestModel request,
required String userSessionToken,
});
Future<Either<Failure, GenericApiModel<RationaleResponseModel>>> getRationale({
required int age,
required int gender,
required List<Map<String, dynamic>> evidence,
required String language,
required String generalId,
required String userSessionToken,
});
Future<Either<Failure, GenericApiModel<TriageLevelResponseModel>>> getTriage({
required int age,
required int gender,
required List<Map<String, dynamic>> evidence,
required String language,
required String generalId,
required String userSessionToken,
});
Future<Either<Failure, GenericApiModel<ExplanationsResponseModel>>> getExplanations({
required int age,
required int gender,
required List<Map<String, dynamic>> evidence,
required String language,
required String target,
required String generalId,
required String userSessionToken,
});
}
class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
@ -201,12 +232,20 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
required String userSessionToken,
required String sessionId,
}) async {
// Build evidence list with "initial" source for symptoms
final List<Map<String, dynamic>> evidenceList = evidenceIds.map((id) {
return {
"id": id,
"source": "initial",
};
}).toList();
final Map<String, dynamic> body = {
"age": {
"value": age,
},
"gender": gender,
"evidence": evidenceIds.map((id) => {"id": id}).toList(),
"evidence": evidenceList,
"language": language,
"generalId": sessionId,
};
@ -265,21 +304,19 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
Future<Either<Failure, GenericApiModel<TriageDataDetails>>> getDiagnosisForTriage({
required int age,
required int gender,
required List<String> evidenceIds,
required List<Map<String, dynamic>> initialEvidenceList,
List<Map<String, String>>? triageEvidence, // Additional triage-specific evidence
required String language,
required String userSessionToken,
required String sessionId,
}) async {
// Build evidence list: combine initial symptoms with triage evidence
// Build evidence list: combine initial evidence (with sources) with triage evidence
List<Map<String, dynamic>> evidenceList = [];
// Add initial evidence as simple IDs
for (var id in evidenceIds) {
evidenceList.add({"id": id});
}
// Add initial evidence (already has sources: initial, red_flags, suggest)
evidenceList.addAll(initialEvidenceList);
// Add triage evidence with proper format (id, choice_id, source)
// Add triage evidence with proper format (id, choice_id, source=triage)
if (triageEvidence != null && triageEvidence.isNotEmpty) {
evidenceList.addAll(triageEvidence);
}
@ -351,7 +388,7 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
Future<Either<Failure, GenericApiModel<RiskAndSuggestionsResponseModel>>> getSuggestions({
required int age,
required int gender,
required List<String> evidenceIds,
required List<Map<String, dynamic>> evidenceList,
required String language,
required String userSessionToken,
required String sessionId,
@ -361,7 +398,7 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
"value": age,
},
"gender": gender,
"evidence": evidenceIds.map((id) => {"id": id}).toList(),
"evidence": evidenceList,
"language": language,
"generalId": sessionId,
};
@ -529,4 +566,210 @@ class SymptomsCheckerRepoImp implements SymptomsCheckerRepo {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<RationaleResponseModel>>> getRationale({
required int age,
required int gender,
required List<Map<String, dynamic>> evidence,
required String language,
required String generalId,
required String userSessionToken,
}) async {
final Map<String, dynamic> body = {
"age": {
"value": age,
},
"gender": gender,
"evidence": evidence,
"language": language,
"generalId": generalId,
};
Map<String, String> headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer $userSessionToken',
};
try {
GenericApiModel<RationaleResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.getRationale,
apiHeaders: headers,
body: body,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
loggerService.logError("GetRationale API Failed: $error");
failure = failureType ?? ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response;
RationaleResponseModel rationaleResponse = RationaleResponseModel.fromJson(responseData);
apiResponse = GenericApiModel<RationaleResponseModel>(
messageStatus: messageStatus ?? 1,
statusCode: statusCode,
errorMessage: errorMessage,
data: rationaleResponse,
);
} catch (e, stackTrace) {
loggerService.logError("Error parsing GetRationale response: $e");
loggerService.logError("StackTrace: $stackTrace");
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e, stackTrace) {
loggerService.logError("Exception in getRationale: $e");
loggerService.logError("StackTrace: $stackTrace");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<TriageLevelResponseModel>>> getTriage({
required int age,
required int gender,
required List<Map<String, dynamic>> evidence,
required String language,
required String generalId,
required String userSessionToken,
}) async {
final Map<String, dynamic> body = {
"age": {
"value": age,
},
"gender": gender,
"evidence": evidence,
"language": language,
"generalId": generalId,
};
Map<String, String> headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer $userSessionToken',
};
try {
GenericApiModel<TriageLevelResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.getTriage,
apiHeaders: headers,
body: body,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
loggerService.logError("GetTriage API Failed: $error");
failure = failureType ?? ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response;
TriageLevelResponseModel triageResponse = TriageLevelResponseModel.fromJson(responseData);
apiResponse = GenericApiModel<TriageLevelResponseModel>(
messageStatus: messageStatus ?? 1,
statusCode: statusCode,
errorMessage: errorMessage,
data: triageResponse,
);
} catch (e, stackTrace) {
loggerService.logError("Error parsing GetTriage response: $e");
loggerService.logError("StackTrace: $stackTrace");
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e, stackTrace) {
loggerService.logError("Exception in getTriage: $e");
loggerService.logError("StackTrace: $stackTrace");
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<ExplanationsResponseModel>>> getExplanations({
required int age,
required int gender,
required List<Map<String, dynamic>> evidence,
required String language,
required String target,
required String generalId,
required String userSessionToken,
}) async {
final Map<String, dynamic> body = {
"age": {
"value": age,
},
"gender": gender,
"evidence": evidence,
"language": language,
"target": target,
"generalId": generalId,
};
Map<String, String> headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer $userSessionToken',
};
try {
GenericApiModel<ExplanationsResponseModel>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.getExplanations,
apiHeaders: headers,
body: body,
isExternal: true,
isAllowAny: true,
onFailure: (error, statusCode, {messageStatus, failureType}) {
loggerService.logError("GetExplanations API Failed: $error");
failure = failureType ?? ServerFailure(error);
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
final Map<String, dynamic> responseData = response is String ? jsonDecode(response) : response;
ExplanationsResponseModel explanationsResponse = ExplanationsResponseModel.fromJson(responseData);
apiResponse = GenericApiModel<ExplanationsResponseModel>(
messageStatus: messageStatus ?? 1,
statusCode: statusCode,
errorMessage: errorMessage,
data: explanationsResponse,
);
} catch (e, stackTrace) {
loggerService.logError("Error parsing GetExplanations response: $e");
loggerService.logError("StackTrace: $stackTrace");
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e, stackTrace) {
loggerService.logError("Exception in getExplanations: $e");
loggerService.logError("StackTrace: $stackTrace");
return Left(UnknownFailure(e.toString()));
}
}
}

@ -8,10 +8,13 @@ import 'package:hmg_patient_app_new/features/symptoms_checker/data/organ_mapping
import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/req_models/schedule_appointment_request_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/body_symptom_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/explanations_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/get_clinic_details_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/rationale_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/risk_and_suggestions_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/schedule_appointment_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/symptoms_user_details_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_level_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/resp_models/triage_response_model.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_repo.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
@ -66,6 +69,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
bool isSuggestionsLoading = false;
bool isTriageDiagnosisLoading = false;
bool isScheduleAppointmentLoading = false;
bool isRationaleLoading = false;
bool isTriageLevelLoading = false;
bool isExplanationsLoading = false;
// Flag to track if appointment is being booked via symptoms checker flow
bool isBookingFromSymptomsChecker = false;
@ -76,6 +82,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
RiskAndSuggestionsResponseModel? riskFactorsResponse;
RiskAndSuggestionsResponseModel? suggestionsResponse;
TriageDataDetails? triageDataDetails;
RationaleResponseModel? rationaleResponse;
TriageLevelResponseModel? triageLevelResponse;
ExplanationsResponseModel? explanationsResponse;
// Triage state
int? _selectedTriageChoiceIndex; // Deprecated - keeping for backward compatibility
@ -87,6 +96,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
String? _selectedSingleItemId; // Store which item was selected
int? _selectedSingleChoiceIndex; // Store which choice was selected
// Triage question history tracking
final List<TriageDataDetails> _triageQuestionHistory = []; // Stack of previous questions
final List<List<Map<String, String>>> _triageEvidenceHistory = []; // Stack of evidence states at each question
// Selected risk factors tracking
final Set<String> _selectedRiskFactorIds = {};
@ -178,6 +191,9 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
/// Get the number of triage questions answered
int get triageQuestionCount => _triageQuestionCount;
/// Check if this is the first triage question (no history)
bool get isFirstTriageQuestion => _triageQuestionHistory.isEmpty;
List<GetClinicDetailsResponseModel> clinicDetailsList = [];
/// Get choice index for a specific item
@ -745,6 +761,44 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
return evidenceIds;
}
/// Get all evidence with their sources based on selection step
/// Source values:
/// - "initial" for symptoms (used in GetRiskFactors)
/// - "red_flags" for risk factors (used in GetSuggestions)
/// - "suggest" for suggestions (used in GetDiagnosis)
List<Map<String, dynamic>> getAllEvidenceWithSources({String defaultSource = "initial"}) {
List<Map<String, dynamic>> evidenceList = [];
// Add selected symptoms with source "initial"
final selectedSymptoms = getAllSelectedSymptoms();
for (var symptom in selectedSymptoms.where((s) => s.id != null)) {
evidenceList.add({
"id": symptom.id!,
"source": "initial",
});
}
// Add selected risk factors with source "red_flags" (excluding "not_applicable")
final selectedRiskFactors = getAllSelectedRiskFactors();
for (var riskFactor in selectedRiskFactors.where((rf) => rf.id != null && rf.id != "not_applicable")) {
evidenceList.add({
"id": riskFactor.id!,
"source": "red_flags",
});
}
// Add selected suggestions with source "suggest" (excluding "not_applicable")
final selectedSuggestions = getAllSelectedSuggestions();
for (var suggestion in selectedSuggestions.where((s) => s.id != null && s.id != "not_applicable")) {
evidenceList.add({
"id": suggestion.id!,
"source": "suggest",
});
}
return evidenceList;
}
/// Fetch risk factors based on selected symptoms
Future<void> fetchSuggestions({
Function()? onSuccess,
@ -768,21 +822,30 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
return;
}
// Extract symptom IDs
List<String> evidenceIds = selectedSymptoms.where((s) => s.id != null).map((s) => s.id!).toList();
// Build evidence list with proper sources
List<Map<String, dynamic>> evidenceList = [];
// Get all selected symptoms
final selectedRisks = getAllSelectedRiskFactors();
// Add selected symptoms with source "initial"
for (var symptom in selectedSymptoms.where((s) => s.id != null)) {
evidenceList.add({
"id": symptom.id!,
"source": "initial",
});
}
if (selectedRisks.isNotEmpty) {
List<String> evidenceRisksIds = selectedRisks.where((s) => s.id != null && s.id != "not_applicable").map((s) => s.id!).toList();
evidenceIds.addAll(evidenceRisksIds);
// Get selected risk factors and add with source "red_flags"
final selectedRisks = getAllSelectedRiskFactors();
for (var riskFactor in selectedRisks.where((rf) => rf.id != null && rf.id != "not_applicable")) {
evidenceList.add({
"id": riskFactor.id!,
"source": "red_flags",
});
}
await getSuggestions(
age: _selectedAge!,
gender: _getGenderAsInt(_selectedGender),
evidenceIds: evidenceIds,
evidenceList: evidenceList,
language: appState.isArabic() ? 'ar' : 'en',
onSuccess: (response) {
if (onSuccess != null) {
@ -801,7 +864,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
Future<void> getSuggestions({
required int age,
required int gender,
required List<String> evidenceIds,
required List<Map<String, dynamic>> evidenceList,
required String language,
Function(RiskAndSuggestionsResponseModel)? onSuccess,
Function(String)? onError,
@ -812,7 +875,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
final result = await symptomsCheckerRepo.getSuggestions(
age: age,
gender: gender,
evidenceIds: evidenceIds,
evidenceList: evidenceList,
language: language,
sessionId: currentSessionId,
userSessionToken: currentSessionAuthToken,
@ -863,19 +926,27 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
Future<void> getDiagnosisForTriage({
required int age,
required int gender,
required List<String> evidenceIds,
List<Map<String, String>>? triageEvidence,
required List<Map<String, dynamic>> initialEvidenceList, // Evidence with sources (initial, red_flags, suggest)
List<Map<String, String>>? triageEvidence, // Triage-specific evidence (id, choice_id, source=triage)
required String language,
Function(TriageDataDetails)? onSuccess,
Function(String)? onError,
bool isGoingBack = false, // Flag to indicate if we're going back in history
}) async {
// Save current question and evidence to history BEFORE making new API call
// (but only if we're moving forward, not going back)
if (!isGoingBack && triageDataDetails != null) {
_triageQuestionHistory.add(triageDataDetails!);
_triageEvidenceHistory.add(List<Map<String, String>>.from(_triageEvidenceList));
}
isTriageDiagnosisLoading = true;
notifyListeners();
final result = await symptomsCheckerRepo.getDiagnosisForTriage(
age: age,
gender: gender,
evidenceIds: evidenceIds,
initialEvidenceList: initialEvidenceList,
triageEvidence: triageEvidence,
language: language,
sessionId: currentSessionId,
@ -923,10 +994,10 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
return;
}
// Get all evidence IDs
final evidenceIds = getAllEvidenceIds();
// Get all evidence with sources
final evidenceList = getAllEvidenceWithSources();
if (evidenceIds.isEmpty) {
if (evidenceList.isEmpty) {
if (onError != null) {
onError('No evidence selected');
}
@ -936,7 +1007,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
await getDiagnosisForTriage(
age: _selectedAge!,
gender: _getGenderAsInt(_selectedGender),
evidenceIds: evidenceIds,
initialEvidenceList: evidenceList,
language: appState.isArabic() ? 'ar' : 'en',
onSuccess: (response) {
if (onSuccess != null) {
@ -993,7 +1064,7 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
_triageEvidenceList.add({
"id": itemId,
"choice_id": choiceId,
"source": "triage",
"source": "suggest",
});
notifyListeners();
}
@ -1009,6 +1080,39 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
notifyListeners();
}
/// Go back to previous triage question
/// Returns true if successfully went back, false if already at first question
bool goBackToPreviousTriageQuestion() {
if (_triageQuestionHistory.isEmpty) {
return false; // Already at first question
}
// Pop the last question from history
final previousQuestion = _triageQuestionHistory.removeLast();
final previousEvidence = _triageEvidenceHistory.removeLast();
// Restore the previous question
triageDataDetails = previousQuestion;
// Restore the previous evidence state
_triageEvidenceList.clear();
_triageEvidenceList.addAll(previousEvidence);
// Decrement question count
if (_triageQuestionCount > 0) {
_triageQuestionCount--;
}
// Clear current selections
_selectedTriageChoiceIndex = null;
_selectedTriageChoicesByItemId.clear();
_selectedSingleItemId = null;
_selectedSingleChoiceIndex = null;
notifyListeners();
return true;
}
void reset() {
_currentView = BodyView.front;
_selectedOrganIds.clear();
@ -1018,6 +1122,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
_triageEvidenceList.clear();
_selectedTriageChoicesByItemId.clear();
_triageQuestionCount = 0; // Reset question count
_triageQuestionHistory.clear(); // Clear question history
_triageEvidenceHistory.clear(); // Clear evidence history
_currentZoomScale = 1.0; // Reset zoom scale
_symptomSearchQuery = ''; // Reset search query
_filteredOrganSymptomsResults.clear(); // Clear filtered results
@ -1054,6 +1160,8 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
_selectedRiskFactorIds.clear();
_selectedSuggestionsIds.clear();
_triageEvidenceList.clear();
_triageQuestionHistory.clear(); // Clear question history
_triageEvidenceHistory.clear(); // Clear evidence history
_selectedTriageChoicesByItemId.clear();
_selectedSingleItemId = null;
_selectedSingleChoiceIndex = null;
@ -1066,6 +1174,28 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
notifyListeners();
}
/// Clear selections when reaching possible conditions screen
/// This clears organs, symptoms, risk factors, suggestions, and questions
void clearSelectionsForPossibleConditions() {
_selectedOrganIds.clear();
_selectedSymptomsByOrgan.clear();
_selectedRiskFactorIds.clear();
_selectedSuggestionsIds.clear();
_triageEvidenceList.clear();
_triageQuestionHistory.clear();
_triageEvidenceHistory.clear();
_selectedTriageChoicesByItemId.clear();
_selectedSingleItemId = null;
_selectedSingleChoiceIndex = null;
_triageQuestionCount = 0;
_symptomSearchQuery = '';
_filteredOrganSymptomsResults.clear();
_isBottomSheetExpanded = false;
_tooltipTimer?.cancel();
_tooltipOrganId = null;
notifyListeners();
}
// User Info Flow Methods
/// Set current page in user info flow
@ -1170,6 +1300,34 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
};
}
// User Consent Methods
bool isTermsAccepted = false;
bool isPrivacyAccepted = false;
/// Toggle terms acceptance
void toggleTermsAcceptance() {
isTermsAccepted = !isTermsAccepted;
notifyListeners();
}
/// Toggle privacy policy acceptance
void togglePrivacyAcceptance() {
isPrivacyAccepted = !isPrivacyAccepted;
notifyListeners();
}
void setTermsAcceptance(bool value) {
isTermsAccepted = value;
notifyListeners();
}
/// Toggle privacy policy acceptance
void setPrivacyAcceptance(bool value) {
isPrivacyAccepted = value;
notifyListeners();
}
Future<void> getSymptomsUserDetails({
required String userName,
required String password,
@ -1352,6 +1510,170 @@ class SymptomsCheckerViewModel extends ChangeNotifier {
);
}
/// Get Rationale for symptoms
/// This API shows which symptoms and conditions are being considered
Future<void> fetchRationale({
required List<Map<String, dynamic>> evidence,
Function(RationaleResponseModel)? onSuccess,
Function(String)? onError,
}) async {
// Validate user info
if (_selectedAge == null || _selectedGender == null) {
if (onError != null) {
onError('User information is incomplete');
}
return;
}
isRationaleLoading = true;
notifyListeners();
final result = await symptomsCheckerRepo.getRationale(
age: _selectedAge!,
gender: _getGenderAsInt(_selectedGender),
evidence: evidence,
language: appState.isArabic() ? 'ar' : 'en',
generalId: currentSessionId,
userSessionToken: currentSessionAuthToken,
);
result.fold(
(failure) async {
isRationaleLoading = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(apiResponse) {
isRationaleLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
rationaleResponse = apiResponse.data;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse.data!);
}
} else {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? 'Failed to fetch rationale');
}
}
},
);
}
/// Get Triage Level for symptoms
/// This API determines the urgency level and serious symptoms
Future<void> fetchTriageLevel({
required List<Map<String, dynamic>> evidence,
Function(TriageLevelResponseModel)? onSuccess,
Function(String)? onError,
}) async {
// Validate user info
if (_selectedAge == null || _selectedGender == null) {
if (onError != null) {
onError('User information is incomplete');
}
return;
}
isTriageLevelLoading = true;
notifyListeners();
final result = await symptomsCheckerRepo.getTriage(
age: _selectedAge!,
gender: _getGenderAsInt(_selectedGender),
evidence: evidence,
language: appState.isArabic() ? 'ar' : 'en',
generalId: currentSessionId,
userSessionToken: currentSessionAuthToken,
);
result.fold(
(failure) async {
isTriageLevelLoading = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(apiResponse) {
isTriageLevelLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
triageLevelResponse = apiResponse.data;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse.data!);
}
} else {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? 'Failed to fetch triage level');
}
}
},
);
}
/// Get Explanations for a specific condition
/// This API explains why a condition was suggested based on evidence
Future<void> fetchExplanations({
required String targetConditionId,
required List<Map<String, dynamic>> evidence,
Function(ExplanationsResponseModel)? onSuccess,
Function(String)? onError,
}) async {
// Validate user info
if (_selectedAge == null || _selectedGender == null) {
if (onError != null) {
onError('User information is incomplete');
}
return;
}
isExplanationsLoading = true;
notifyListeners();
final result = await symptomsCheckerRepo.getExplanations(
age: _selectedAge!,
gender: _getGenderAsInt(_selectedGender),
evidence: evidence,
language: appState.isArabic() ? 'ar' : 'en',
target: targetConditionId,
generalId: currentSessionId,
userSessionToken: currentSessionAuthToken,
);
result.fold(
(failure) async {
isExplanationsLoading = false;
notifyListeners();
await errorHandlerService.handleError(failure: failure);
if (onError != null) {
onError(failure.toString());
}
},
(apiResponse) {
isExplanationsLoading = false;
if (apiResponse.messageStatus == 1 && apiResponse.data != null) {
explanationsResponse = apiResponse.data;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse.data!);
}
} else {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? 'Failed to fetch explanations');
}
}
},
);
}
@override
void dispose() {
_tooltipTimer?.cancel();

@ -78,6 +78,7 @@ class AncillaryOrderItem {
int? orderNo;
String? projectName; // Added from parent AncillaryOrderGroup
int? projectID; // Added from parent AncillaryOrderGroup
String? setupID; // Added from parent AncillaryOrderGroup
AncillaryOrderItem({
this.ancillaryProcedureListModels,
@ -94,9 +95,10 @@ class AncillaryOrderItem {
this.orderNo,
this.projectName,
this.projectID,
this.setupID,
});
factory AncillaryOrderItem.fromJson(Map<String, dynamic> json, {String? projectName, int? projectID}) => AncillaryOrderItem(
factory AncillaryOrderItem.fromJson(Map<String, dynamic> json, {String? projectName, int? projectID, String? setupID}) => AncillaryOrderItem(
ancillaryProcedureListModels: json['AncillaryProcedureListModels'],
appointmentDate: DateUtil.convertStringToDate(json['AppointmentDate']),
appointmentNo: json['AppointmentNo'] as int?,
@ -111,5 +113,6 @@ class AncillaryOrderItem {
orderNo: json['OrderNo'] as int?,
projectName: projectName,
projectID: projectID,
setupID: setupID,
);
}

@ -20,7 +20,7 @@ abstract class TodoSectionRepo {
Future<Either<Failure, dynamic>> createAdvancePayment({
required int projectID,
required double paymentAmount,
required num paymentAmount,
required String paymentReference,
required String paymentMethodName,
required int patientTypeID,
@ -85,11 +85,13 @@ class TodoSectionRepoImp implements TodoSectionRepo {
final ordersList = group['AncillaryOrderList'] as List;
final projectName = group['ProjectName'] as String?;
final projectID = group['ProjectID'] as int?;
final setupID = group['SetupID'] as String?;
// Parse each order item in the group
for (var orderJson in ordersList) {
if (orderJson is Map<String, dynamic>) {
ancillaryOrders.add(AncillaryOrderItem.fromJson(orderJson, projectName: projectName, projectID: projectID));
ancillaryOrders.add(AncillaryOrderItem.fromJson(orderJson, projectName: projectName, projectID: projectID, setupID: setupID));
}
}
}
@ -205,7 +207,7 @@ class TodoSectionRepoImp implements TodoSectionRepo {
@override
Future<Either<Failure, dynamic>> createAdvancePayment({
required int projectID,
required double paymentAmount,
required num paymentAmount,
required String paymentReference,
required String paymentMethodName,
required int patientTypeID,

@ -13,13 +13,13 @@ class TodoSectionViewModel extends ChangeNotifier {
String? notificationsCount = "0";
initializeTodoSectionViewModel() async {
// if (isAncillaryOrdersNeedReloading) {
patientAncillaryOrdersList.clear();
isAncillaryOrdersLoading = true;
if (isAncillaryOrdersNeedReloading) {
patientAncillaryOrdersList.clear();
isAncillaryOrdersLoading = true;
isAncillaryDetailsProceduresLoading = true;
notificationsCount = "0";
getPatientOnlineAncillaryOrderList();
// }
}
getPatientDashboard();
}
@ -67,9 +67,9 @@ class TodoSectionViewModel extends ChangeNotifier {
}
Future<void> getPatientOnlineAncillaryOrderList({Function(dynamic)? onSuccess, Function(String)? onError}) async {
// if (!isAncillaryOrdersNeedReloading) {
// return;
// }
if (!isAncillaryOrdersNeedReloading) {
return;
}
patientAncillaryOrdersList.clear();
isAncillaryOrdersLoading = true;
@ -158,7 +158,7 @@ class TodoSectionViewModel extends ChangeNotifier {
Future<void> createAdvancePayment({
required int projectID,
required double paymentAmount,
required num paymentAmount,
required String paymentReference,
required String paymentMethodName,
required int patientTypeID,

File diff suppressed because it is too large Load Diff

@ -231,6 +231,17 @@ abstract class LocaleKeys {
static const companyName = 'companyName';
static const receiptOn = 'receiptOn';
static const expiryDate = 'expiryDate';
static const myCards = 'myCards';
static const savedCards = 'savedCards';
static const addNewCard = 'addNewCard';
static const noSavedCards = 'noSavedCards';
static const cardEndingWith = 'cardEndingWith';
static const expiry = 'expiry';
static const defaultCard = 'defaultCard';
static const deleteCard = 'deleteCard';
static const deleteCardConfirmation = 'deleteCardConfirmation';
static const cardDeletedSuccessfully = 'cardDeletedSuccessfully';
static const addCardComingSoon = 'addCardComingSoon';
static const expiryPoints = 'expiryPoints';
static const expiryOn = 'expiryOn';
static const procedureName = 'procedureName';
@ -371,6 +382,12 @@ abstract class LocaleKeys {
static const clickHere = 'clickHere';
static const iAgreeToTheTermsAndConditions = 'iAgreeToTheTermsAndConditions';
static const iAgreeToTheTermsAndConditionsSubtitle = 'iAgreeToTheTermsAndConditionsSubtitle';
static const iAgreeAndAcceptTermsAndConditions = 'iAgreeAndAcceptTermsAndConditions';
static const termsAndConditions = 'termsAndConditions';
static const iAgreeHealthInfoCanBeUsed = 'iAgreeHealthInfoCanBeUsed';
static const privacyPolicyText = 'privacyPolicyText';
static const outPatient = 'outPatient';
static const thisInvoiceIsNotEligibleForRefund = 'thisInvoiceIsNotEligibleForRefund';
static const save = 'save';
static const userAgreement = 'userAgreement';
static const updateSuccessfully = 'updateSuccessfully';
@ -670,6 +687,15 @@ abstract class LocaleKeys {
static const offTheme = 'offTheme';
static const dimTheme = 'dimTheme';
static const bwTheme = 'bwTheme';
static const highContrastTheme = 'highContrastTheme';
static const fontSize = 'fontSize';
static const fontSizeSmall = 'fontSizeSmall';
static const fontSizeMedium = 'fontSizeMedium';
static const fontSizeLarge = 'fontSizeLarge';
static const saveChanges = 'saveChanges';
static const settingsSavedSuccessfully = 'settingsSavedSuccessfully';
static const cardHeading = 'cardHeading';
static const buttonText = 'buttonText';
static const permissions = 'permissions';
static const cameraPermission = 'cameraPermission';
static const locationPermission = 'locationPermission';
@ -1360,7 +1386,6 @@ abstract class LocaleKeys {
static const medicalFiles = 'medicalFiles';
static const addANewFamilyMember = 'addANewFamilyMember';
static const viewInvoiceDetails = 'viewInvoiceDetails';
static const outPatient = 'outPatient';
static const invoiceDetails = 'invoiceDetails';
static const sendingEmailPleaseWait = 'sendingEmailPleaseWait';
static const emailSentSuccessfullyMessage = 'emailSentSuccessfullyMessage';
@ -1426,6 +1451,9 @@ abstract class LocaleKeys {
static const symptomsSelector = 'symptomsSelector';
static const possibleSymptomsRelatedTo = 'possibleSymptomsRelatedTo';
static const suggestionAIListDescription = 'suggestionAIListDescription';
static const whyAmIBeingAskedThis = 'whyAmIBeingAskedThis';
static const weAreAskingThisQuestionToBetterUnderstand = 'weAreAskingThisQuestionToBetterUnderstand';
static const analyzingYourSymptoms = 'analyzingYourSymptoms';
static const analyzing = 'analyzing';
static const seekMedicalAdvice = 'seekMedicalAdvice';
static const monitorDescription = 'monitorDescription';
@ -1443,6 +1471,30 @@ abstract class LocaleKeys {
static const areYouSureYouWantToExitProgress = 'areYouSureYouWantToExitProgress';
static const noQuestionAvailable = 'noQuestionAvailable';
static const possibleSymptom = 'possibleSymptom';
static const triageLevelEmergencyAmbulance = 'triageLevelEmergencyAmbulance';
static const triageLevelEmergency = 'triageLevelEmergency';
static const triageLevelConsultation24 = 'triageLevelConsultation24';
static const triageLevelConsultation = 'triageLevelConsultation';
static const triageLevelSelfCare = 'triageLevelSelfCare';
static const triageLevelEmergencyAmbulanceDesc = 'triageLevelEmergencyAmbulanceDesc';
static const triageLevelEmergencyDesc = 'triageLevelEmergencyDesc';
static const triageLevelConsultation24Desc = 'triageLevelConsultation24Desc';
static const triageLevelConsultationDesc = 'triageLevelConsultationDesc';
static const triageLevelSelfCareDesc = 'triageLevelSelfCareDesc';
static const rootCauseEmergencyEvidencePresent = 'rootCauseEmergencyEvidencePresent';
static const rootCauseSeriousEvidencePresent = 'rootCauseSeriousEvidencePresent';
static const rootCauseEmergencyConditionLikely = 'rootCauseEmergencyConditionLikely';
static const rootCauseEmergencyConditionPossible = 'rootCauseEmergencyConditionPossible';
static const rootCauseConsultationConditionLikely = 'rootCauseConsultationConditionLikely';
static const rootCauseSelfCareSufficient = 'rootCauseSelfCareSufficient';
static const rootCauseDiagnosisUnknown = 'rootCauseDiagnosisUnknown';
static const teleconsultationAvailable = 'teleconsultationAvailable';
static const seriousSymptoms = 'seriousSymptoms';
static const howTo = 'howTo';
static const explainWhy = 'explainWhy';
static const whyThisCondition = 'whyThisCondition';
static const supportingEvidence = 'supportingEvidence';
static const explanationDescription = 'explanationDescription';
static const symptomsCheckerFindingScore = 'symptomsCheckerFindingScore';
static const notSet = 'notSet';
static const years = 'years';
@ -1535,7 +1587,6 @@ abstract class LocaleKeys {
static const waterConsumption = 'waterConsumption';
static const selectNumberOfReminders = 'selectNumberOfReminders';
static const h2oSettings = 'h2oSettings';
static const settingsSavedSuccessfully = 'settingsSavedSuccessfully';
static const yourName = 'yourName';
static const ageYears = 'ageYears';
static const numberOfRemindersInADay = 'numberOfRemindersInADay';
@ -1804,6 +1855,7 @@ abstract class LocaleKeys {
static const watchUsOnYoutube = 'watchUsOnYoutube';
static const connectOnLinkedin = 'connectOnLinkedin';
static const riskFactorPageTitle = 'riskFactorPageTitle';
static const doAnyOfTheseApply = 'doAnyOfTheseApply';
static const bloodDonationService = 'bloodDonationService';
static const bookAppointmentService = 'bookAppointmentService';
static const pinchToZoom = 'pinchToZoom';
@ -1868,13 +1920,19 @@ abstract class LocaleKeys {
static const refundReason6 = 'refundReason6';
static const refundReason7 = 'refundReason7';
static const selectProcedures = 'selectProcedures';
static const selectProcedures = 'selectProcedures';
static const beforeUsingSymptomsChecker = 'beforeUsingSymptomsChecker';
static const symptomsCheckerNotDiagnosis = 'symptomsCheckerNotDiagnosis';
static const symptomsCheckerNotEmergency = 'symptomsCheckerNotEmergency';
static const yourDataIsSafe = 'yourDataIsSafe';
static const redeem = 'redeem';
static const points = 'points';
static const transactions = 'transactions';
static const pharmacy = 'pharmacy';
static const visitsOrders = 'visitsOrders';
static const earned = 'earned';
static const getParkingQR = 'getParkingQR';
static const cannotOpenThisFile = 'cannotOpenThisFile';
static const thisInvoiceIsNotEligibleForRefund = 'thisInvoiceIsNotEligibleForRefund';
static const refundDetails = 'refundDetails';
static const refund = 'refund';
static const requestNo = 'requestNo';
@ -1893,5 +1951,7 @@ abstract class LocaleKeys {
static const cashRefundInstructions = 'cashRefundInstructions';
static const cashRefundApprovalInstructions = 'cashRefundApprovalInstructions';
static const packageRefundValidationMessage = 'packageRefundValidationMessage';
static const parkingQR = 'parkingQR';
static const parkingQRDesc = 'parkingQRDesc';
}

@ -96,7 +96,7 @@ Future<void> callAppStateInitializations() async {
PlatformDispatcher.instance.onError = (error, stack) {
if (!kDebugMode) {
FirebaseCrashlytics.instance.recordError(error, stack,
fatal: true,
fatal: false,
printDetails: true,
reason:
"${appState.isAuthenticated ? "Authenticated User ID: ${appState.getAuthenticatedUser()!.patientId} - ${appState.getAuthenticatedUser()!.mobileNumber}" : "Unauthenticated User"} - Uncaught asynchronous error");
@ -134,6 +134,9 @@ Future<void> callInitializations() async {
// Restore persisted dark-mode preference before the first frame.
getIt.get<ProfileSettingsViewModel>().loadDarkMode();
// Restore persisted accessibility settings before the first frame.
getIt.get<ProfileSettingsViewModel>().loadAccessibilitySettings();
}
void main() async {
@ -282,6 +285,55 @@ void main() async {
class MyApp extends StatelessWidget {
const MyApp({super.key});
/// Apply visual mode filter to the entire app
Widget _applyVisualMode(String visualMode, Widget child) {
switch (visualMode) {
case 'invert':
// Invert colors app-wide (red becomes cyan for colorblind users)
return ColorFiltered(
colorFilter: const ColorFilter.matrix(<double>[
-1, 0, 0, 0, 255, // Red channel inverted
0, -1, 0, 0, 255, // Green channel inverted
0, 0, -1, 0, 255, // Blue channel inverted
0, 0, 0, 1, 0, // Alpha channel unchanged
]),
child: child,
);
case 'dim':
// Apply dim effect with opacity overlay
return Stack(
children: [
child,
Positioned.fill(
child: IgnorePointer(
child: Container(
color: Colors.black.withValues(alpha: 0.3), // 30% black overlay for dimming
),
),
),
],
);
case 'highContrast':
// Apply desaturation for high contrast (removes colors, leaves black and white)
return ColorFiltered(
colorFilter: const ColorFilter.matrix(<double>[
0.2126, 0.7152, 0.0722, 0, 0, // Red channel (grayscale)
0.2126, 0.7152, 0.0722, 0, 0, // Green channel (grayscale)
0.2126, 0.7152, 0.0722, 0, 0, // Blue channel (grayscale)
0, 0, 0, 1, 0, // Alpha channel unchanged
]),
child: child,
);
case 'off':
default:
// No filter applied
return child;
}
}
@override
Widget build(BuildContext context) {
return SafeArea(
@ -300,13 +352,16 @@ class MyApp extends StatelessWidget {
// title: 'Dr. AlHabib',
title: 'Dr. AlHabib Beta',
builder: (context, mchild) {
return MediaQuery(
Widget child = MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.noScaling,
textScaler: TextScaler.linear(profileVm.getFontScaleMultiplier()),
alwaysUse24HourFormat: true,
),
child: mchild!,
);
// Apply visual mode filters app-wide
return _applyVisualMode(profileVm.visualMode, child);
},
showSemanticsDebugger: false,
debugShowCheckedModeBanner: false,

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
@ -85,6 +86,7 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
@override
void initState() {
Clarity.setCurrentScreenName('Appointment Details Page');
super.initState();
scheduleMicrotask(() async {
@ -284,7 +286,7 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
AppointmentDoctorCard(
// renderWidgetForERDisplay: ((widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ||
renderWidgetForERDisplay: ((widget.patientAppointmentHistoryResponseModel.isExecludeDoctor ?? false) ||
!Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID)),
!Utils.isClinicAllowedForRebook(widget.patientAppointmentHistoryResponseModel.clinicID ?? 0)),
patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel,
onAskDoctorTap: () async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingDoctorAvailability.tr(context: context));
@ -570,11 +572,12 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
clipBehavior: Clip.hardEdge,
borderRadius: BorderRadius.circular(24.r),
// Todo: what is this???? Api Key??? 😲
child: Image.network(
"https://maps.googleapis.com/maps/api/staticmap?center=${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&zoom=14&size=${(MediaQuery.of(context).size.width * 1.5).toInt()}x${(MediaQuery.of(context).size.height * 0.35).toInt()}&maptype=roadmap&markers=color:red%7C${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&key=${ApiKeyConstants.googleMapsApiKey}",
child: Utils.buildImgWithNetwork(
url: "https://maps.googleapis.com/maps/api/staticmap?center=${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&zoom=14&size=${(MediaQuery.of(context).size.width * 1.5).toInt()}x${(MediaQuery.of(context).size.height * 0.35).toInt()}&maptype=roadmap&markers=color:red%7C${widget.patientAppointmentHistoryResponseModel.latitude},${widget.patientAppointmentHistoryResponseModel.longitude}&key=${ApiKeyConstants.googleMapsApiKey}",
iconColor: AppColors.transparent,
fit: BoxFit.cover,
width: double.infinity,
),
height: 180.h),
),
),
Positioned(
@ -874,6 +877,73 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
}
})
: SizedBox.shrink(),
(widget.patientAppointmentHistoryResponseModel.isParkingAvailable ?? false)
? MedicalFileCard(
label: LocaleKeys.getParkingQR.tr(context: context),
textColor: AppColors.blackColor,
backgroundColor: AppColors.whiteColor,
svgIcon: AppAssets.appointment_parking_icon,
isLargeText: true,
iconSize: 36.w,
).onPress(() {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context));
myAppointmentsViewModel.getParkingQR(
widget.patientAppointmentHistoryResponseModel.appointmentNo,
widget.patientAppointmentHistoryResponseModel.projectID,
onSuccess: (val) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.parkingQR.tr(context: context),
context,
child: Column(
children: [
LocaleKeys.parkingQRDesc.tr(context: context).toText16(),
SizedBox(height: 16.h),
Image.memory(
myAppointmentsViewModel.parkingQRImageBytes!,
width: 156.w,
height: 156.h,
fit: BoxFit.cover,
gaplessPlayback: true, // Prevents blink during image rebuild
),
SizedBox(height: 16.h),
CustomButton(
text: LocaleKeys.done.tr(context: context),
onPressed: () {
openDoctorScheduleCalendar();
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: Colors.white,
fontSize: 16.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 50.h,
icon: AppAssets.checkmark_icon,
iconColor: Colors.white,
iconSize: 18.h,
).paddingSymmetrical(0.h, 0.h),
],
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
})
: SizedBox.shrink(),
],
);
}),
@ -1242,6 +1312,7 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
case 50:
// return LocaleKeys.confirmLiveCare.tr(context: context);
case 90:
Clarity.setCurrentScreenName('Appointment CheckIn Page');
showCommonBottomSheetWithoutHeight(context,
title: LocaleKeys.onlineCheckIn.tr(context: context),
child: isAppointmentWithin4Hours
@ -1251,9 +1322,9 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
)
: Utils.getOnlineCheckInWidget(
loadingText: LocaleKeys.onlineCheckIn24HoursOnly.tr(context: context),
),
callBackFunc: () {},
isFullScreen: false);
), callBackFunc: () {
Clarity.setCurrentScreenName('Appointment Details Page');
}, isFullScreen: false);
default:
// return "No Action".needTranslation;
}

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
@ -13,6 +14,7 @@ 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/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/features/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/features/payfort/models/apple_pay_request_insert_model.dart';
@ -59,6 +61,7 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
@override
void initState() {
scheduleMicrotask(() {
Clarity.setCurrentScreenName('Appointment Payment Page');
payfortViewModel.initPayfortViewModel();
payfortViewModel.setIsApplePayConfigurationLoading(false);
myAppointmentsViewModel.getPatientShareAppointment(
@ -112,8 +115,7 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.mada, width: 72.h, height: 25.h)
.toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading),
Utils.buildImgWithAssets(icon: AppAssets.mada, width: 80.h, height: 30.h, fit: BoxFit.contain).toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading),
SizedBox(height: 16.h),
LocaleKeys.mada.tr(context: context).toText16(isBold: true).toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading),
],
@ -132,7 +134,7 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() {
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "MADA";
if (appState.isPaytabsEnabled) {
paytabsViewModel.setPaymentConfiguration(
@ -212,7 +214,100 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
);
});
} else {
openPaymentURL("mada");
if (appState.isPayfortHostedPageEnabled) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
transID = Utils.getAppointmentTransID(
widget.patientAppointmentHistoryResponseModel.projectID,
widget.patientAppointmentHistoryResponseModel.clinicID,
widget.patientAppointmentHistoryResponseModel.appointmentNo,
);
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: widget.patientAppointmentHistoryResponseModel.projectID, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
await myAppointmentsViewModel.createAdvancePayment(
paymentMethodName: selectedPaymentMethod,
projectID: widget.patientAppointmentHistoryResponseModel.projectID,
clinicID: widget.patientAppointmentHistoryResponseModel.clinicID,
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(),
payedAmount: num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString()),
paymentReference: paymentResult.fortId!,
patientID: appState.getAuthenticatedUser()!.patientId.toString(),
patientType: appState.getAuthenticatedUser()!.patientType!,
onSuccess: (value) async {
print(value);
await myAppointmentsViewModel.addAdvanceNumberRequest(
advanceNumber: Utils.isVidaPlusProject(widget.patientAppointmentHistoryResponseModel.projectID)
? value.data['OnlineCheckInAppointments'][0]['AdvanceNumber_VP'].toString()
: value.data['OnlineCheckInAppointments'][0]['AdvanceNumber'].toString(),
paymentReference: paymentResult.fortId!,
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(),
onSuccess: (value) async {
if (widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment!) {
//TODO: Implement LiveCare Check-In API Call
await myAppointmentsViewModel.insertLiveCareVIDARequest(
clientRequestID: transID,
patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel,
onSuccess: (apiResponse) {
Future.delayed(Duration(milliseconds: 500), () {
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
LoaderBottomSheet.hideLoader();
Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
page: LandingNavigation(),
),
(r) => false);
});
},
onError: (error) {});
} else {
await myAppointmentsViewModel.generateAppointmentQR(
clinicID: widget.patientAppointmentHistoryResponseModel.clinicID,
projectID: widget.patientAppointmentHistoryResponseModel.projectID,
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(),
isFollowUp: myAppointmentsViewModel.patientAppointmentShareResponseModel!.isFollowup!,
onSuccess: (apiResponse) {
Future.delayed(Duration(milliseconds: 500), () {
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
LoaderBottomSheet.hideLoader();
Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
page: LandingNavigation(),
),
(r) => false);
});
});
}
});
});
},
onFailed: (err) {
print("Payment Failed Data: ${err.toString()}");
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
} else {
openPaymentURL("mada");
}
}
}),
SizedBox(height: 16.h),
@ -230,9 +325,9 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
children: [
Row(
children: [
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
Utils.buildImgWithAssets(icon: AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
Utils.buildImgWithAssets(icon: AppAssets.mastercard, width: 40.h, height: 40.h, fit: BoxFit.contain),
],
).toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading),
SizedBox(height: 16.h),
@ -255,7 +350,7 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() {
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "VISA";
if (appState.isPaytabsEnabled) {
paytabsViewModel.setPaymentConfiguration(
@ -335,7 +430,110 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
);
});
} else {
openPaymentURL("visa");
if (appState.isPayfortHostedPageEnabled) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
transID = Utils.getAppointmentTransID(
widget.patientAppointmentHistoryResponseModel.projectID,
widget.patientAppointmentHistoryResponseModel.clinicID,
widget.patientAppointmentHistoryResponseModel.appointmentNo,
);
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: widget.patientAppointmentHistoryResponseModel.projectID, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
await myAppointmentsViewModel.createAdvancePayment(
paymentMethodName: selectedPaymentMethod,
projectID: widget.patientAppointmentHistoryResponseModel.projectID,
clinicID: widget.patientAppointmentHistoryResponseModel.clinicID,
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(),
payedAmount: num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString()),
paymentReference: paymentResult.fortId!,
patientID: appState.getAuthenticatedUser()!.patientId.toString(),
patientType: appState.getAuthenticatedUser()!.patientType!,
onSuccess: (value) async {
print(value);
await myAppointmentsViewModel.addAdvanceNumberRequest(
advanceNumber: Utils.isVidaPlusProject(widget.patientAppointmentHistoryResponseModel.projectID)
? value.data['OnlineCheckInAppointments'][0]['AdvanceNumber_VP'].toString()
: value.data['OnlineCheckInAppointments'][0]['AdvanceNumber'].toString(),
paymentReference: paymentResult.fortId!,
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(),
onSuccess: (value) async {
if (widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment!) {
//TODO: Implement LiveCare Check-In API Call
await myAppointmentsViewModel.insertLiveCareVIDARequest(
clientRequestID: transID,
patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel,
onSuccess: (apiResponse) {
Future.delayed(Duration(milliseconds: 500), () {
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
getIt.get<HabibWalletViewModel>().setIsWalletAmountToBeLoaded(true);
getIt.get<HabibWalletViewModel>().initHabibWalletProvider();
getIt.get<HabibWalletViewModel>().getPatientBalanceAmount();
LoaderBottomSheet.hideLoader();
Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
page: LandingNavigation(),
),
(r) => false);
});
},
onError: (error) {});
} else {
await myAppointmentsViewModel.generateAppointmentQR(
clinicID: widget.patientAppointmentHistoryResponseModel.clinicID,
projectID: widget.patientAppointmentHistoryResponseModel.projectID,
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(),
isFollowUp: myAppointmentsViewModel.patientAppointmentShareResponseModel!.isFollowup!,
onSuccess: (apiResponse) {
Future.delayed(Duration(milliseconds: 500), () {
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
getIt.get<HabibWalletViewModel>().setIsWalletAmountToBeLoaded(true);
getIt.get<HabibWalletViewModel>().initHabibWalletProvider();
getIt.get<HabibWalletViewModel>().getPatientBalanceAmount();
LoaderBottomSheet.hideLoader();
Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
page: LandingNavigation(),
),
(r) => false);
});
});
}
});
});
},
onFailed: (err) {
print("Payment Failed Data: ${err.toString()}");
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
} else {
openPaymentURL("visa");
}
}
}),
SizedBox(height: 16.h),
@ -352,7 +550,7 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.tamaraEng, width: 72.h, height: 25.h)
Utils.buildImgWithAssets(icon: AppAssets.tamaraEng, width: 72.h, height: 25.h)
.toShimmer2(isShow: myAppointmentsVM.isAppointmentPatientShareLoading),
SizedBox(height: 16.h),
LocaleKeys.tamara.tr(context: context)
@ -376,7 +574,108 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() {
selectedPaymentMethod = "TAMARA";
openPaymentURL("tamara");
if (appState.isPaytabsEnabled) {
paytabsViewModel.setAlternatePaymentConfiguration(
"Appointment Payment", num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString()));
paytabsViewModel.startAlternatePayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
tamaraOrderID = transactionData.cartID!;
transID = transactionData.transactionReference!;
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
await payfortViewModel.updateTamaraRequestStatus(responseMessage: "success", status: "14", clientRequestID: transID, tamaraOrderID: tamaraOrderID);
await payfortViewModel.markAppointmentAsTamaraPaid(
projectID: widget.patientAppointmentHistoryResponseModel.projectID, appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo);
await myAppointmentsViewModel.createAdvancePayment(
paymentMethodName: selectedPaymentMethod,
projectID: widget.patientAppointmentHistoryResponseModel.projectID,
clinicID: widget.patientAppointmentHistoryResponseModel.clinicID,
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(),
payedAmount: num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString()),
paymentReference: transactionData.transactionReference!,
patientID: appState.getAuthenticatedUser()!.patientId.toString(),
patientType: appState.getAuthenticatedUser()!.patientType!,
onSuccess: (value) async {
print(value);
await myAppointmentsViewModel.addAdvanceNumberRequest(
advanceNumber: "Tamara-Advance-0000",
paymentReference: transactionData.transactionReference!,
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(),
onSuccess: (value) async {
if (widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment!) {
//TODO: Implement LiveCare Check-In API Call
await myAppointmentsViewModel.insertLiveCareVIDARequest(
clientRequestID: transID,
patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel,
onSuccess: (apiResponse) {
Future.delayed(Duration(milliseconds: 500), () {
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
LoaderBottomSheet.hideLoader();
getIt.get<HabibWalletViewModel>().setIsWalletAmountToBeLoaded(true);
getIt.get<HabibWalletViewModel>().initHabibWalletProvider();
getIt.get<HabibWalletViewModel>().getPatientBalanceAmount();
Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
page: LandingNavigation(),
),
(r) => false);
});
},
onError: (error) {});
} else {
await myAppointmentsViewModel.generateAppointmentQR(
clinicID: widget.patientAppointmentHistoryResponseModel.clinicID,
projectID: widget.patientAppointmentHistoryResponseModel.projectID,
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(),
isFollowUp: myAppointmentsViewModel.patientAppointmentShareResponseModel!.isFollowup!,
onSuccess: (apiResponse) {
Future.delayed(Duration(milliseconds: 500), () {
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
getIt.get<HabibWalletViewModel>().setIsWalletAmountToBeLoaded(true);
getIt.get<HabibWalletViewModel>().initHabibWalletProvider();
getIt.get<HabibWalletViewModel>().getPatientBalanceAmount();
LoaderBottomSheet.hideLoader();
Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
page: LandingNavigation(),
),
(r) => false);
});
});
}
});
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err.toString()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
}, onError: (err) async {
await payfortViewModel.updateTamaraRequestStatus(responseMessage: "Failed", status: "00", clientRequestID: transID, tamaraOrderID: tamaraOrderID);
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err.toString()),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
} else {
openPaymentURL("tamara");
}
})
: SizedBox.shrink(),
],
@ -443,21 +742,24 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.amountBeforeTax.tr(context: context).toText14(isBold: true),
Utils.getPaymentAmountWithSymbol((myAppointmentsVM.patientAppointmentShareResponseModel!.patientShare ?? 0).toString().toText16(isBold: true), AppColors.blackColor,
13,
isSaudiCurrency: true),
ClarityUnmask(
child: Utils.getPaymentAmountWithSymbol(
(myAppointmentsVM.patientAppointmentShareResponseModel!.patientShare ?? 0).toString().toText16(isBold: true), AppColors.blackColor, 13,
isSaudiCurrency: true),
),
],
).paddingSymmetrical(24.h, 0.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"VAT 15%".toText14(isBold: true, color: AppColors.greyTextColor),
Utils.getPaymentAmountWithSymbol(
(myAppointmentsVM.patientAppointmentShareResponseModel!.patientTaxAmount ?? 0).toString()
.toText14(isBold: true, color: AppColors.greyTextColor),
AppColors.greyTextColor,
13,
isSaudiCurrency: true),
ClarityUnmask(
child: Utils.getPaymentAmountWithSymbol(
(myAppointmentsVM.patientAppointmentShareResponseModel!.patientTaxAmount ?? 0).toString().toText14(isBold: true, color: AppColors.greyTextColor),
AppColors.greyTextColor,
13,
isSaudiCurrency: true),
),
],
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 17.h),
@ -465,10 +767,11 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"".toText14(isBold: true),
Utils.getPaymentAmountWithSymbol(
(myAppointmentsVM.patientAppointmentShareResponseModel!.patientShareWithTax ?? 0).toString().toText24(isBold: true), AppColors.blackColor,
17,
isSaudiCurrency: true),
ClarityUnmask(
child: Utils.getPaymentAmountWithSymbol(
(myAppointmentsVM.patientAppointmentShareResponseModel!.patientShareWithTax ?? 0).toString().toText24(isBold: true), AppColors.blackColor, 17,
isSaudiCurrency: true),
),
],
).paddingSymmetrical(24.h, 0.h),
Platform.isIOS
@ -566,6 +869,11 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
getIt.get<HabibWalletViewModel>().setIsWalletAmountToBeLoaded(true);
getIt.get<HabibWalletViewModel>().initHabibWalletProvider();
getIt.get<HabibWalletViewModel>().getPatientBalanceAmount();
LoaderBottomSheet.hideLoader();
Navigator.pushAndRemoveUntil(
context,
@ -587,6 +895,11 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
getIt.get<HabibWalletViewModel>().setIsWalletAmountToBeLoaded(true);
getIt.get<HabibWalletViewModel>().initHabibWalletProvider();
getIt.get<HabibWalletViewModel>().getPatientBalanceAmount();
LoaderBottomSheet.hideLoader();
Navigator.pushAndRemoveUntil(
context,
@ -654,6 +967,11 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
getIt.get<HabibWalletViewModel>().setIsWalletAmountToBeLoaded(true);
getIt.get<HabibWalletViewModel>().initHabibWalletProvider();
getIt.get<HabibWalletViewModel>().getPatientBalanceAmount();
LoaderBottomSheet.hideLoader();
Navigator.pushAndRemoveUntil(
context,
@ -675,6 +993,11 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
getIt.get<HabibWalletViewModel>().setIsWalletAmountToBeLoaded(true);
getIt.get<HabibWalletViewModel>().initHabibWalletProvider();
getIt.get<HabibWalletViewModel>().getPatientBalanceAmount();
LoaderBottomSheet.hideLoader();
Navigator.pushAndRemoveUntil(
context,
@ -795,6 +1118,7 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
//TODO: Need to pass dynamic params to the Apple Pay instead of static values
await payfortViewModel.applePayRequestInsert(applePayInsertRequest: applePayInsertRequest).then((value) {
if (appState.isPaytabsEnabled) {
selectedPaymentMethod = "VISA";
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration(
"Appointment Payment",
@ -830,6 +1154,11 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
getIt.get<HabibWalletViewModel>().setIsWalletAmountToBeLoaded(true);
getIt.get<HabibWalletViewModel>().initHabibWalletProvider();
getIt.get<HabibWalletViewModel>().getPatientBalanceAmount();
LoaderBottomSheet.hideLoader();
Navigator.pushAndRemoveUntil(
context,
@ -851,6 +1180,11 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
getIt.get<HabibWalletViewModel>().setIsWalletAmountToBeLoaded(true);
getIt.get<HabibWalletViewModel>().initHabibWalletProvider();
getIt.get<HabibWalletViewModel>().getPatientBalanceAmount();
LoaderBottomSheet.hideLoader();
Navigator.pushAndRemoveUntil(
context,

@ -139,8 +139,9 @@ class _MyDoctorsPageState extends State<MyDoctorsPage> {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,
@ -261,8 +262,9 @@ class _MyDoctorsPageState extends State<MyDoctorsPage> {
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
(doctor?.doctorImageURL ?? doctor?.doctorImage ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png"),
Utils.buildImgWithNetwork(
url: (doctor?.doctorImageURL ?? doctor?.doctorImage ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png"),
iconColor: AppColors.transparent,
width: 24.w,
height: 24.h,
fit: BoxFit.cover,

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
@ -227,10 +228,11 @@ class _AppointmentCardState extends State<AppointmentCard> {
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
widget.isLoading
Utils.buildImgWithNetwork(
url: widget.isLoading
? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png'
: widget.patientAppointmentHistoryResponseModel.doctorImageURL!,
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,
@ -280,8 +282,9 @@ class _AppointmentCardState extends State<AppointmentCard> {
SizedBox(width: 12.w),
(widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null &&
widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL!.isNotEmpty)
? Image.network(
widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png",
? Utils.buildImgWithNetwork(
url: widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png",
iconColor: AppColors.transparent,
width: 20.h,
height: 15.h,
fit: BoxFit.cover,
@ -309,16 +312,18 @@ class _AppointmentCardState extends State<AppointmentCard> {
? '${(widget.patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital").substring(0, 15)}...'
: widget.patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital")
.toShimmer2(isShow: widget.isLoading),
Directionality(
textDirection: ui.TextDirection.ltr,
child: AppCustomChipWidget(
labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w),
icon: AppAssets.appointment_calendar_icon,
richText: widget.isLoading
? 'Cardiology'.toText10().toShimmer2(isShow: widget.isLoading)
: "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false)}"
.toText10(isEnglishOnly: true, isBold: true),
).toShimmer2(isShow: widget.isLoading),
ClarityUnmask(
child: Directionality(
textDirection: ui.TextDirection.ltr,
child: AppCustomChipWidget(
labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w),
icon: AppAssets.appointment_calendar_icon,
richText: widget.isLoading
? 'Cardiology'.toText10().toShimmer2(isShow: widget.isLoading)
: "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang(DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate), false)}"
.toText10(isEnglishOnly: true, isBold: true),
).toShimmer2(isShow: widget.isLoading),
),
),
// AppCustomChipWidget(
@ -639,7 +644,9 @@ class _AppointmentCardState extends State<AppointmentCard> {
),
);
} else {
if (!AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) {
if (!AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel) &&
widget.patientAppointmentHistoryResponseModel.projectID != null &&
widget.patientAppointmentHistoryResponseModel.clinicID != null) {
widget.bookAppointmentsViewModel.getAppointmentNearestGate(
projectID: widget.patientAppointmentHistoryResponseModel.projectID, clinicID: widget.patientAppointmentHistoryResponseModel.clinicID);
}

@ -1,5 +1,6 @@
import 'dart:ui' as ui;
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
@ -58,8 +59,9 @@ class AppointmentDoctorCard extends StatelessWidget {
children: [
Column(
children: [
Image.network(
patientAppointmentHistoryResponseModel.doctorImageURL!,
Utils.buildImgWithNetwork(
url: patientAppointmentHistoryResponseModel.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png",
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,
@ -98,21 +100,24 @@ class AppointmentDoctorCard extends StatelessWidget {
children: [
Row(
children: [
patientAppointmentHistoryResponseModel.doctorNameObj!.toText16(
isBold: true,
isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? ""),
textOverflow: TextOverflow.ellipsis,
Expanded(
child: patientAppointmentHistoryResponseModel.doctorNameObj!.toText16(
isBold: true,
isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? ""),
textOverflow: TextOverflow.ellipsis,
maxlines: 2
),
),
SizedBox(width: 12.w),
(patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null &&
patientAppointmentHistoryResponseModel.doctorNationalityFlagURL!.isNotEmpty)
? Image.network(
patientAppointmentHistoryResponseModel.doctorNationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png",
width: 20.h,
height: 15.h,
fit: BoxFit.cover,
)
: SizedBox.shrink(),
// (patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null &&
// patientAppointmentHistoryResponseModel.doctorNationalityFlagURL!.isNotEmpty)
// ? Image.network(
// patientAppointmentHistoryResponseModel.doctorNationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png",
// width: 20.h,
// height: 15.h,
// fit: BoxFit.cover,
// )
// : SizedBox.shrink(),
],
),
SizedBox(height: 8.h),
@ -131,17 +136,18 @@ class AppointmentDoctorCard extends StatelessWidget {
labelText: patientAppointmentHistoryResponseModel.projectName ?? "Habib Hospital",
labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w),
),
Directionality(
textDirection: ui.TextDirection.ltr,
child: AppCustomChipWidget(
labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w),
icon: AppAssets.doctor_calendar_icon,
richText:
"${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang(
DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate),
false,
)}"
.toText10(isBold: true),
ClarityUnmask(
child: Directionality(
textDirection: ui.TextDirection.ltr,
child: AppCustomChipWidget(
labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w),
icon: AppAssets.doctor_calendar_icon,
richText: "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)} ${DateUtil.formatDateToTimeLang(
DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate),
false,
)}"
.toText10(isBold: true),
),
),
),
AppCustomChipWidget(
@ -213,7 +219,7 @@ class AppointmentDoctorCard extends StatelessWidget {
AppointmentType.isArrived(patientAppointmentHistoryResponseModel),
),
),
if (timerWidget != null) timerWidget ?? SizedBox()
if (!AppointmentType.isArrived(patientAppointmentHistoryResponseModel) && timerWidget != null) timerWidget ?? SizedBox()
],
),
),

@ -10,6 +10,9 @@ import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'dart:ui' as ui;
import '../../../core/utils/utils.dart';
import '../../../theme/colors.dart';
class BuildDoctorRowAppointmentRating extends StatelessWidget {
final PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel;
@ -22,8 +25,9 @@ class BuildDoctorRowAppointmentRating extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
patientAppointmentHistoryResponseModel.doctorImageURL!,
Utils.buildImgWithNetwork(
url: patientAppointmentHistoryResponseModel.doctorImageURL!,
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -86,8 +86,9 @@ class _DoctorResponsePageState extends State<DoctorResponsePage> {
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png",
iconColor: AppColors.blackBgColor,
width: 50.w,
height: 50.h,
fit: BoxFit.cover,
@ -133,7 +134,7 @@ class _DoctorResponsePageState extends State<DoctorResponsePage> {
).onPress(() {
showCommonBottomSheetWithoutHeight(
context,
title: askDoctorVM.doctorResponsesList[index].requestTypeDescription!,
title: askDoctorVM.doctorResponsesList[index].requestTypeDescription ?? "",
child: DoctorResponseTransactions(doctorResponseModel: askDoctorVM.doctorResponsesList[index],),
callBackFunc: () {},
isFullScreen: false,

@ -87,8 +87,9 @@ class AskDoctorAppointmentCard extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
askDoctorAppointmentHistoryList.doctorImageURL!,
Utils.buildImgWithNetwork(
url: askDoctorAppointmentHistoryList.doctorImageURL!,
iconColor: AppColors.blackBgColor,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -28,8 +28,9 @@ class DoctorResponseTransactions extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png",
iconColor: AppColors.blackBgColor,
width: 50.w,
height: 50.h,
fit: BoxFit.cover,
@ -85,7 +86,11 @@ class DoctorResponseTransactions extends StatelessWidget {
SizedBox(height: 16.h),
Row(
children: [
Image.asset(getIt.get<AppState>().getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg, width: 42.w, height: 42.h),
Utils.buildImgWithAssets(
icon: getIt.get<AppState>().getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg,
width: 42.w,
height: 42.h,
),
SizedBox(width: 8.w),
Container(
padding: EdgeInsets.all(8.w),
@ -93,7 +98,7 @@ class DoctorResponseTransactions extends StatelessWidget {
color: AppColors.infoColor.withAlpha(20),
borderRadius: BorderRadius.circular(8.r),
),
child: doctorResponseModel.requestTypeDescription!.toText12(color: AppColors.infoColor, isBold: true),
child: (doctorResponseModel.requestTypeDescription ?? "").toText12(color: AppColors.infoColor, isBold: true),
),
],
),
@ -116,10 +121,16 @@ class DoctorResponseTransactions extends StatelessWidget {
borderRadius: BorderRadius.circular(8.r),
),
child: doctorResponseModel.transactions![index].doctorResponse!.toText12(color: AppColors.textColor, isBold: true),
),
SizedBox(width: 8.w),
Image.network("https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png", fit: BoxFit.cover, width: 42.w, height: 42.h).circle(100.r),
],
),
SizedBox(width: 8.w),
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png",
iconColor: AppColors.blackBgColor,
fit: BoxFit.cover,
width: 42.w,
height: 42.h,
).circle(100.r),
],
),
],
);

@ -1,3 +1,4 @@
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
@ -32,6 +33,7 @@ class LoginScreenState extends State<LoginScreen> {
@override
void initState() {
Clarity.setCurrentScreenName('Login Screen');
super.initState();
_nationalIdFocusNode = FocusNode();
@ -97,7 +99,7 @@ class LoginScreenState extends State<LoginScreen> {
hintText: "xxxxxxxxx",
controller: authVm.nationalIdController,
focusNode: _nationalIdFocusNode,
keyboardType: TextInputType.text,
keyboardType: TextInputType.number,
isEnable: true,
prefix: null,
autoFocus: true,

@ -1,6 +1,8 @@
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/api_consts.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';
@ -30,6 +32,7 @@ class _RegisterNew extends State<RegisterNew> {
@override
void initState() {
Clarity.setCurrentScreenName('Registration Step 1 Screen');
super.initState();
_nationalIdFocusNode = FocusNode();
_dobFocusNode = FocusNode();
@ -51,7 +54,7 @@ class _RegisterNew extends State<RegisterNew> {
// Clear errors when leaving register screen
// Use post frame callback to avoid calling notifyListeners during dispose
WidgetsBinding.instance.addPostFrameCallback((_) {
final authVm = context.read<AuthenticationViewModel>();
final authVm = getIt.get<AuthenticationViewModel>();
authVm.clearNationalIdError();
authVm.clearDobError();
authVm.clearPhoneNumberError();
@ -95,7 +98,13 @@ class _RegisterNew extends State<RegisterNew> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.showLottie(context: context, assetPath: 'assets/animations/lottie/register.json', width: 200.w, height: 200.h, fit: BoxFit.cover, repeat: true),
Utils.showLottie(
context: context,
assetPath: 'assets/animations/lottie/register.json',
width: 200.w,
height: 200.h,
fit: BoxFit.cover,
repeat: true),
SizedBox(height: 16.h),
LocaleKeys.prepareToElevate.tr(context: context).toText32(isBold: true),
SizedBox(height: 24.h),
@ -134,8 +143,10 @@ class _RegisterNew extends State<RegisterNew> {
padding: EdgeInsets.symmetric(vertical: 8.h),
leadingIcon: AppAssets.student_card,
fontFamily: "Poppins",
hasError: false, // Don't show individual field border
errorMessage: authVm.nationalIdError, // Show error message if exists
hasError: false,
// Don't show individual field border
errorMessage: authVm.nationalIdError,
// Show error message if exists
onChange: (value) {
// Clear error when user starts typing
authVm.clearNationalIdError();
@ -162,8 +173,9 @@ class _RegisterNew extends State<RegisterNew> {
authVm.clearDobError();
},
fontFamily: "Poppins",
hasError: false, // Don't show individual field border
errorMessage: authVm.dobError, // Show error message if exists
hasError: false,
// Don't show individual field border
errorMessage: authVm.dobError, // Show error message if exists
).withVerticalPadding(8),
],
),
@ -202,7 +214,7 @@ class _RegisterNew extends State<RegisterNew> {
onTap: () {
// Navigate to terms and conditions page
Utils.openWebView(
url: 'https://hmg.com/en/Pages/Terms.aspx',
url: AppUrls.hmgTermsUrlEn,
);
},
child: Text(

@ -1,3 +1,4 @@
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
@ -34,6 +35,7 @@ class _RegisterNew extends State<RegisterNewStep2> {
@override
void initState() {
Clarity.setCurrentScreenName('Registration Step 2 Screen');
super.initState();
authVM = context.read<AuthenticationViewModel>();
insuranceVM = context.read<InsuranceViewModel>();

@ -1,5 +1,6 @@
import 'dart:ui' as ui;
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
@ -37,6 +38,7 @@ class _SavedLogin extends State<SavedLogin> {
@override
void initState() {
Clarity.setCurrentScreenName('Last Login Screen');
authVm = context.read<AuthenticationViewModel>();
appState = getIt.get<AppState>();
loginType = LoginTypeExtension.fromValue(appState.getSelectDeviceByImeiRespModelElement!.logInType!)!;
@ -58,7 +60,6 @@ class _SavedLogin extends State<SavedLogin> {
if (loginType == LoginTypeEnum.fingerprint || loginType == LoginTypeEnum.face) {
authVm.loginWithFingerPrintFace(() {});
}
super.initState();
}

@ -1,5 +1,6 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/api_consts.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';
@ -15,18 +16,13 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/blood_donation/widgets/select_blood_group_widget.dart';
import 'package:hmg_patient_app_new/presentation/blood_donation/widgets/select_city_widget.dart';
import 'package:hmg_patient_app_new/presentation/blood_donation/widgets/select_gender_widget.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/select_clinic_page.dart';
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.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';
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:provider/provider.dart';
import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart';
class BloodDonationPage extends StatefulWidget {
BloodDonationPage({super.key});
@ -60,7 +56,8 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
onPressed: () {
if (bloodDonationVM.isUserAuthanticated()) {
bloodDonationVM.fetchHospitalsList().then((value) {
showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.selectHospital.tr(context: context), isDismissible: false, child: Consumer<BloodDonationViewModel>(builder: (_, data, __) {
showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.selectHospital.tr(context: context), isDismissible: false,
child: Consumer<BloodDonationViewModel>(builder: (_, data, __) {
return HospitalBottomSheetBodySelection(
isHideTitle: true,
onUserHospitalSelection: (BdGetProjectsHaveBdClinic userChoice) {
@ -77,7 +74,8 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Lottie.asset(AppAnimations.errorAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
Lottie.asset(AppAnimations.errorAnimation,
repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
SizedBox(height: 8.h),
(LocaleKeys.loginToUseService.tr()).toText16(color: AppColors.blackColor),
SizedBox(height: 16.h),
@ -131,7 +129,8 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
padding: EdgeInsets.all(24.w),
child: SingleChildScrollView(
child: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false),
decoration:
RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false),
child: Padding(
padding: EdgeInsets.all(16.h),
child: Column(
@ -150,7 +149,8 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
LocaleKeys.city.tr().toText16(color: AppColors.textColor, isBold: true),
(appState.isArabic()
? (bloodDonationVM.selectedCity?.descriptionN ?? LocaleKeys.selectBloodDonationCity.tr())
: bloodDonationVM.selectedCity?.description ?? LocaleKeys.selectBloodDonationCity.tr(context: context))
: bloodDonationVM.selectedCity?.description ??
LocaleKeys.selectBloodDonationCity.tr(context: context))
.toText14(color: AppColors.greyTextColor, isBold: true),
],
),
@ -160,7 +160,10 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
],
).onPress(() async {
showCommonBottomSheetWithoutHeight(context,
title: LocaleKeys.selectCity.tr(context: context), isDismissible: true, child: SelectCityWidget(bloodDonationViewModel: bloodDonationVM), callBackFunc: () {});
title: LocaleKeys.selectCity.tr(context: context),
isDismissible: true,
child: SelectCityWidget(bloodDonationViewModel: bloodDonationVM),
callBackFunc: () {});
}),
SizedBox(height: 16.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h),
@ -254,17 +257,17 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
SizedBox(width: 12.w),
Expanded(
child: LocaleKeys.aboutBloodDonation.tr(context: context).toText16(
color: AppColors.primaryRedColor,
isBold: true,
),
color: AppColors.primaryRedColor,
isBold: true,
),
),
],
),
SizedBox(height: 12.h),
LocaleKeys.bloodDonationDescription.tr(context: context).toText14(
color: AppColors.greyTextColor,
isBold: true,
),
color: AppColors.greyTextColor,
isBold: true,
),
SizedBox(height: 12.h),
Row(
children: [
@ -279,9 +282,9 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
SizedBox(width: 8.w),
Expanded(
child: LocaleKeys.bloodDonationBenefit1.tr(context: context).toText13(
color: AppColors.textColor,
isBold: true,
),
color: AppColors.textColor,
isBold: true,
),
),
],
),
@ -299,9 +302,9 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
SizedBox(width: 8.w),
Expanded(
child: LocaleKeys.bloodDonationBenefit2.tr(context: context).toText13(
color: AppColors.textColor,
isBold: true,
),
color: AppColors.textColor,
isBold: true,
),
),
],
),
@ -319,9 +322,9 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
SizedBox(width: 8.w),
Expanded(
child: LocaleKeys.bloodDonationBenefit3.tr(context: context).toText13(
color: AppColors.textColor,
isBold: true,
),
color: AppColors.textColor,
isBold: true,
),
),
],
),
@ -376,10 +379,10 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
// Navigator.of(context).pushNamed('/terms');
appState.isArabic()
? Utils.openWebView(
url: 'https://hmg.com/ar/Pages/Terms.aspx',
url: AppUrls.hmgTermsUrlAr,
)
: Utils.openWebView(
url: 'https://hmg.com/en/Pages/Terms.aspx',
url: AppUrls.hmgTermsUrlEn,
);
},
child: Text(
@ -419,7 +422,8 @@ class _BloodDonationPageState extends State<BloodDonationPage> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Lottie.asset(AppAnimations.errorAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
Lottie.asset(AppAnimations.errorAnimation,
repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill),
SizedBox(height: 8.h),
(LocaleKeys.loginToUseService.tr()).toText16(color: AppColors.blackColor),
SizedBox(height: 16.h),

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
@ -57,6 +58,7 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
@override
void initState() {
Clarity.setCurrentScreenName('Book Appointment Page');
authVM = context.read<AuthenticationViewModel>();
scheduleMicrotask(() {
// bookAppointmentsViewModel.selectedTabIndex = 0;
@ -132,8 +134,9 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 64.h,
height: 64.h,
fit: BoxFit.cover,
@ -191,8 +194,9 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!,
Utils.buildImgWithNetwork(
url: myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!,
iconColor: AppColors.transparent,
width: 64.h,
height: 64.h,
fit: BoxFit.cover,
@ -383,8 +387,9 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl!,
Utils.buildImgWithNetwork(
url: myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl!,
iconColor: AppColors.transparent,
width: 64.h,
height: 64.h,
fit: BoxFit.cover,

@ -83,8 +83,9 @@ class DoctorProfilePage extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
bookAppointmentsViewModel.doctorsProfileResponseModel.doctorImageURL!,
Utils.buildImgWithNetwork(
url: bookAppointmentsViewModel.doctorsProfileResponseModel.doctorImageURL!,
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,
@ -109,8 +110,9 @@ class DoctorProfilePage extends StatelessWidget {
),
],
),
Image.network(
bookAppointmentsViewModel.doctorsProfileResponseModel.nationalityFlagURL!,
Utils.buildImgWithNetwork(
url: bookAppointmentsViewModel.doctorsProfileResponseModel.nationalityFlagURL!,
iconColor: AppColors.transparent,
width: 32.h,
height: 32.h,
fit: BoxFit.cover,

@ -66,8 +66,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
padding: EdgeInsets.all(16.h),
child: Row(
children: [
Image.asset(
appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg,
Utils.buildImgWithAssets(
icon: appState.getAuthenticatedUser()?.gender == 1 ? AppAssets.maleImg : AppAssets.femaleImg,
width: 52.h,
height: 52.h,
),
@ -86,8 +86,8 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget {
AppCustomChipWidget(
richText: Row(
children: [
"${appState.getAuthenticatedUser()!.age} ".toText10(color: AppColors.blackColor, isEnglishOnly: true),
LocaleKeys.yearsOld.tr(context: context).toText10(color: AppColors.blackColor),
"${appState.getAuthenticatedUser()!.age} ".toText10(color: AppColors.blackColor, isBold: true),
LocaleKeys.yearsOld.tr(context: context).toText10(color: AppColors.blackColor, isBold: true),
],
),
),

@ -106,7 +106,7 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.mada, width: 72.h, height: 25.h),
Utils.buildImgWithAssets(icon: AppAssets.mada, width: 72.h, height: 25.h, fit: BoxFit.contain),
SizedBox(height: 16.h),
LocaleKeys.mada.tr(context: context).toText16(isBold: true),
],
@ -169,7 +169,66 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
);
});
} else {
openPaymentURL("mada");
if (appState.isPayfortHostedPageEnabled) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
transID = Utils.getAppointmentTransID(
immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceID!,
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
DateTime.now().millisecondsSinceEpoch,
);
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.liveCareAppointment.getIdFromServiceEnum(),
projectId: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total ?? "0.0",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
await immediateLiveCareViewModel.addNewCallRequestForImmediateLiveCare(transID);
await immediateLiveCareViewModel.getPatientLiveCareHistory();
LoaderBottomSheet.hideLoader();
if (immediateLiveCareViewModel.patientHasPendingLiveCareRequest) {
Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
page: LandingNavigation(),
),
(r) => false);
Navigator.of(context).push(
CustomPageRoute(
page: ImmediateLiveCarePendingRequestPage(),
),
);
} else {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Unknown error occurred..."),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
},
onFailed: (err) {
print("Payment Failed Data: ${err.toString()}");
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
} else {
openPaymentURL("mada");
}
}
}),
SizedBox(height: 16.h),
@ -187,9 +246,9 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
children: [
Row(
children: [
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
Utils.buildImgWithAssets(icon: AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
Utils.buildImgWithAssets(icon: AppAssets.mastercard, width: 40.h, height: 40.h, fit: BoxFit.contain),
],
),
SizedBox(height: 16.h),
@ -210,7 +269,7 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() {
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "VISA";
if (appState.isPaytabsEnabled) {
paytabsViewModel.setPaymentConfiguration(
@ -254,7 +313,66 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
);
});
} else {
openPaymentURL("visa");
if (appState.isPayfortHostedPageEnabled) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
transID = Utils.getAppointmentTransID(
immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceID!,
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
DateTime.now().millisecondsSinceEpoch,
);
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.liveCareAppointment.getIdFromServiceEnum(),
projectId: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total ?? "0.0",
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
await immediateLiveCareViewModel.addNewCallRequestForImmediateLiveCare(transID);
await immediateLiveCareViewModel.getPatientLiveCareHistory();
LoaderBottomSheet.hideLoader();
if (immediateLiveCareViewModel.patientHasPendingLiveCareRequest) {
Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
page: LandingNavigation(),
),
(r) => false);
Navigator.of(context).push(
CustomPageRoute(
page: ImmediateLiveCarePendingRequestPage(),
),
);
} else {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: "Unknown error occurred..."),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
},
onFailed: (err) {
print("Payment Failed Data: ${err.toString()}");
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
} else {
openPaymentURL("visa");
}
}
}),
SizedBox(height: 16.h),
@ -271,7 +389,7 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.tamaraEng, width: 72.h, height: 25.h),
Utils.buildImgWithAssets(icon: AppAssets.tamaraEng, width: 72.h, height: 25.h),
SizedBox(height: 16.h),
LocaleKeys.tamara.tr(context: context).toText16(isBold: true),
],
@ -584,7 +702,7 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
ApplePayInsertRequest applePayInsertRequest = ApplePayInsertRequest();
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12, integrationId: 2);
serviceId: ServiceTypeEnum.liveCareAppointment.getIdFromServiceEnum(), projectId: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12, integrationId: 2);
applePayInsertRequest.clientRequestID = transID;
applePayInsertRequest.clinicID = immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceID!;
@ -632,6 +750,7 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
//TODO: Need to pass dynamic params to the Apple Pay instead of static values
await payfortViewModel.applePayRequestInsert(applePayInsertRequest: applePayInsertRequest).then((value) {
if (appState.isPaytabsEnabled) {
selectedPaymentMethod = "VISA";
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration("Advance Payment", num.parse((immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total ?? "0.0")));
paytabsViewModel.startApplePayPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {

@ -197,7 +197,7 @@ class _ImmediateLiveCarePendingRequestPageState extends State<ImmediateLiveCareP
CustomButton(
text: LocaleKeys.callLiveCareSupport.tr(),
onPressed: () async {
launchUrl(Uri.parse("tel://" + "011 525 9553"));
launchUrl(Uri.parse("tel://" + "966 11 525 9553"));
},
backgroundColor: AppColors.lightRedButtonColor,
borderColor: AppColors.lightRedButtonColor,

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:developer';
import 'dart:typed_data';
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
@ -48,6 +49,12 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
Uint8List? _cachedImageBytes;
String? _cachedImageDataHash;
@override
void initState() {
Clarity.setCurrentScreenName('Appointment Review Page');
super.initState();
}
@override
Widget build(BuildContext context) {
bookAppointmentsViewModel = Provider.of<BookAppointmentsViewModel>(context, listen: false);
@ -84,9 +91,10 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
children: [
Row(
children: [
Image.network(
bookAppointmentsViewModel.selectedDoctor.doctorImageURL ??
Utils.buildImgWithNetwork(
url: bookAppointmentsViewModel.selectedDoctor.doctorImageURL ??
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown.png",
iconColor: AppColors.transparent,
width: 50.h,
height: 50.h,
fit: BoxFit.cover,
@ -104,9 +112,10 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
SizedBox(width: 12.w),
(bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL != null &&
bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL!.isNotEmpty)
? Image.network(
bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL ??
? Utils.buildImgWithNetwork(
url: bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL ??
"https://hmgwebservices.com/Images/flag/SAU.png",
iconColor: AppColors.transparent,
width: 20.h,
height: 15.h,
fit: BoxFit.cover,
@ -334,12 +343,10 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
// Show default image (no image data or user has no uploaded image)
print('📷 Showing default avatar for user $currentPatientId');
return Image.asset(
defaultImage,
key: ValueKey('default_$currentPatientId'),
return Utils.buildImgWithAssets(
icon: defaultImage,
width: 52.w,
height: 52.h,
gaplessPlayback: true,
);
}

@ -1,3 +1,4 @@
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
@ -39,6 +40,7 @@ class _SearchDoctorByNameState extends State<SearchDoctorByName> {
@override
void initState() {
Clarity.setCurrentScreenName('Search By Doctor Name Page');
_scrollController = ScrollController();
super.initState();
}

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
@ -59,6 +60,7 @@ class _SelectClinicPageState extends State<SelectClinicPage> {
@override
void initState() {
Clarity.setCurrentScreenName('Select Clinic Page');
scheduleMicrotask(() {
bookAppointmentsViewModel.getClinics();
});

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
@ -44,6 +45,7 @@ class _SelectDoctorPageState extends State<SelectDoctorPage> {
@override
void initState() {
Clarity.setCurrentScreenName('Select Doctor Page');
_scrollController = ScrollController();
scheduleMicrotask(() {
bookAppointmentsViewModel.setIsNearestAppointmentSelected(true);

@ -1,11 +1,13 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart';
import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart';
import 'package:hmg_patient_app_new/features/immediate_livecare/models/resp_models/get_livecare_immediate_clinics_response_model.dart';
@ -114,11 +116,11 @@ class SelectLivecareClinicPage extends StatelessWidget {
),
),
),
Column(
children: [
CustomButton(
text: LocaleKeys.yesPleasImInAHurry.tr(context: context),
onPressed: () async {
getIt.get<AppState>().isAuthenticated
? Column(children: [
CustomButton(
text: LocaleKeys.yesPleasImInAHurry.tr(context: context),
onPressed: () async {
Navigator.pop(context);
GetLiveCareClinicListResponseModel liveCareClinic = GetLiveCareClinicListResponseModel(
iD: selectedClinic.liveCareClinicID,
@ -177,8 +179,24 @@ class SelectLivecareClinicPage extends StatelessWidget {
height: 50.h,
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 24.h),
],
),
])
: CustomButton(
text: LocaleKeys.loginToUseServiceLiveCare.tr(),
onPressed: () async {
await getIt.get<AuthenticationViewModel>().onLoginPressed();
},
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
isBold: true,
borderRadius: 12.r,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 50.h,
icon: AppAssets.login1,
iconColor: AppColors.whiteColor,
iconSize: 24.h,
).paddingSymmetrical(24.h, 120.h),
],
),
);

@ -116,7 +116,7 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.mada, width: 72.h, height: 25.h),
Utils.buildImgWithAssets(icon: AppAssets.mada, width: 72.h, height: 25.h, fit: BoxFit.contain),
SizedBox(height: 16.h),
LocaleKeys.mada.tr(context: context).toText16(isBold: true),
],
@ -135,7 +135,7 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() {
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "MADA";
if (appState.isPaytabsEnabled) {
paytabsViewModel.setPaymentConfiguration(
@ -179,7 +179,66 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
);
});
} else {
openPaymentURL("mada");
if (appState.isPayfortHostedPageEnabled) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
transID = Utils.getAppointmentTransID(
bookAppointmentsViewModel.selectedDoctor.projectID!,
bookAppointmentsViewModel.selectedDoctor.clinicID!,
bookAppointmentsViewModel.selectedDoctor.doctorID!,
);
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: bookAppointmentsViewModel.selectedDoctor.projectID!, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.hideLoader();
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.bookingWaitingAppointment.tr(context: context));
await bookAppointmentsViewModel.insertSpecificAppointmentForWalkIn(onSuccess: (val) async {
String appointmentNo = val.data['AppointmentNo'].toString();
await myAppointmentsViewModel.createAdvancePayment(
paymentMethodName: selectedPaymentMethod,
projectID: bookAppointmentsViewModel.waitingAppointmentProjectID,
clinicID: bookAppointmentsViewModel.selectedDoctor.clinicID!,
appointmentNo: appointmentNo,
payedAmount: payfortViewModel.payfortCheckPaymentStatusResponseModel!.amount!,
paymentReference: payfortViewModel.payfortCheckPaymentStatusResponseModel!.fortId!,
patientID: appState.getAuthenticatedUser()!.patientId.toString(),
patientType: appState.getAuthenticatedUser()!.patientType!,
onSuccess: (value) async {
print(value);
await myAppointmentsViewModel.addAdvanceNumberRequest(
advanceNumber: Utils.isVidaPlusProject(bookAppointmentsViewModel.waitingAppointmentProjectID)
? value.data['OnlineCheckInAppointments'][0]['AdvanceNumber_VP'].toString()
: value.data['OnlineCheckInAppointments'][0]['AdvanceNumber'].toString(),
paymentReference: payfortViewModel.payfortCheckPaymentStatusResponseModel!.fortId!,
appointmentNo: appointmentNo,
onSuccess: (value) async {
LoaderBottomSheet.hideLoader();
sendCheckInRequest(bookAppointmentsViewModel.waitingAppointmentNFCCode, appointmentNo, context);
});
});
});
},
onFailed: (err) {
print("Payment Failed Data: ${err.toString()}");
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
} else {
openPaymentURL("mada");
}
}
}),
SizedBox(height: 16.h),
@ -197,9 +256,9 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
children: [
Row(
children: [
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
Utils.buildImgWithAssets(icon: AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
Utils.buildImgWithAssets(icon: AppAssets.mastercard, width: 40.h, height: 40.h, fit: BoxFit.contain),
],
),
SizedBox(height: 16.h),
@ -220,7 +279,7 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() {
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "VISA";
if (appState.isPaytabsEnabled) {
paytabsViewModel.setPaymentConfiguration(
@ -264,7 +323,66 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
);
});
} else {
openPaymentURL("visa");
if (appState.isPayfortHostedPageEnabled) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
transID = Utils.getAppointmentTransID(
bookAppointmentsViewModel.selectedDoctor.projectID!,
bookAppointmentsViewModel.selectedDoctor.clinicID!,
bookAppointmentsViewModel.selectedDoctor.doctorID!,
);
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum(), projectId: bookAppointmentsViewModel.selectedDoctor.projectID!, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.hideLoader();
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.bookingWaitingAppointment.tr(context: context));
await bookAppointmentsViewModel.insertSpecificAppointmentForWalkIn(onSuccess: (val) async {
String appointmentNo = val.data['AppointmentNo'].toString();
await myAppointmentsViewModel.createAdvancePayment(
paymentMethodName: selectedPaymentMethod,
projectID: bookAppointmentsViewModel.waitingAppointmentProjectID,
clinicID: bookAppointmentsViewModel.selectedDoctor.clinicID!,
appointmentNo: appointmentNo,
payedAmount: payfortViewModel.payfortCheckPaymentStatusResponseModel!.amount!,
paymentReference: payfortViewModel.payfortCheckPaymentStatusResponseModel!.fortId!,
patientID: appState.getAuthenticatedUser()!.patientId.toString(),
patientType: appState.getAuthenticatedUser()!.patientType!,
onSuccess: (value) async {
print(value);
await myAppointmentsViewModel.addAdvanceNumberRequest(
advanceNumber: Utils.isVidaPlusProject(bookAppointmentsViewModel.waitingAppointmentProjectID)
? value.data['OnlineCheckInAppointments'][0]['AdvanceNumber_VP'].toString()
: value.data['OnlineCheckInAppointments'][0]['AdvanceNumber'].toString(),
paymentReference: payfortViewModel.payfortCheckPaymentStatusResponseModel!.fortId!,
appointmentNo: appointmentNo,
onSuccess: (value) async {
LoaderBottomSheet.hideLoader();
sendCheckInRequest(bookAppointmentsViewModel.waitingAppointmentNFCCode, appointmentNo, context);
});
});
});
},
onFailed: (err) {
print("Payment Failed Data: ${err.toString()}");
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
} else {
openPaymentURL("visa");
}
}
}),
SizedBox(height: 16.h),
@ -281,7 +399,7 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.tamaraEng, width: 72.h, height: 25.h),
Utils.buildImgWithAssets(icon: AppAssets.tamaraEng, width: 72.h, height: 25.h),
SizedBox(height: 16.h),
LocaleKeys.tamara.tr(context: context).toText16(isBold: true),
],
@ -680,6 +798,7 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
//TODO: Need to pass dynamic params to the Apple Pay instead of static values
await payfortViewModel.applePayRequestInsert(applePayInsertRequest: applePayInsertRequest).then((value) {
if (appState.isPaytabsEnabled) {
selectedPaymentMethod = "VISA";
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration(
"Appointment Payment",

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
@ -59,6 +60,7 @@ class _AppointmentCalendarState extends State<AppointmentCalendar> {
@override
void initState() {
Clarity.setCurrentScreenName('Select Doctor Free Slot Page');
_calendarController = CalendarController();
scheduleMicrotask(() {
_events = {
@ -98,54 +100,56 @@ class _AppointmentCalendarState extends State<AppointmentCalendar> {
child: Localizations.override(
context: context,
locale: isArabic ? const Locale('ar', 'SA') : const Locale('en'),
child: SfCalendar(
controller: _calendarController,
minDate: DateTime.now(),
showNavigationArrow: true,
// headerHeight: 60.h,
headerStyle: CalendarHeaderStyle(
backgroundColor: AppColors.transparent,
textAlign: isArabic ? TextAlign.end : TextAlign.start,
textStyle: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, letterSpacing: -0.46, color: AppColors.primaryRedColor),
),
viewHeaderStyle: ViewHeaderStyle(
// backgroundColor: AppColors.scaffoldBgColor,
dayTextStyle: TextStyle(fontSize: 14.f, fontWeight: FontWeight.w600, letterSpacing: -0.46, color: AppColors.textColor),
),
view: CalendarView.month,
todayHighlightColor: Colors.transparent,
todayTextStyle: TextStyle(color: AppColors.textColor, fontWeight: FontWeight.bold),
selectionDecoration: ShapeDecoration(
color: AppColors.transparent,
shape: SmoothRectangleBorder(
borderRadius: BorderRadius.circular(10.r),
smoothness: 1,
side: BorderSide(color: AppColors.primaryRedColor, width: 1.5),
child: ClarityUnmask(
child: SfCalendar(
controller: _calendarController,
minDate: DateTime.now(),
showNavigationArrow: true,
// headerHeight: 60.h,
headerStyle: CalendarHeaderStyle(
backgroundColor: AppColors.transparent,
textAlign: isArabic ? TextAlign.end : TextAlign.start,
textStyle: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, letterSpacing: -0.46, color: AppColors.primaryRedColor),
),
),
cellBorderColor: AppColors.transparent,
dataSource: MeetingDataSource(_getDataSource()),
monthCellBuilder: (context, details) => Padding(
padding: EdgeInsets.all(12.h),
child: details.date.day.toString().toText14(
isCenter: true,
color: details.date == _calendarController.selectedDate ? AppColors.primaryRedColor : AppColors.textColor,
isEnglishOnly: true,
),
),
monthViewSettings: MonthViewSettings(
dayFormat: "EEE",
appointmentDisplayMode: MonthAppointmentDisplayMode.indicator,
showTrailingAndLeadingDates: false,
appointmentDisplayCount: 1,
monthCellStyle: MonthCellStyle(
textStyle: TextStyle(fontSize: 19.f),
viewHeaderStyle: ViewHeaderStyle(
// backgroundColor: AppColors.scaffoldBgColor,
dayTextStyle: TextStyle(fontSize: 14.f, fontWeight: FontWeight.w600, letterSpacing: -0.46, color: AppColors.textColor),
),
view: CalendarView.month,
todayHighlightColor: Colors.transparent,
todayTextStyle: TextStyle(color: AppColors.textColor, fontWeight: FontWeight.bold),
selectionDecoration: ShapeDecoration(
color: AppColors.transparent,
shape: SmoothRectangleBorder(
borderRadius: BorderRadius.circular(10.r),
smoothness: 1,
side: BorderSide(color: AppColors.primaryRedColor, width: 1.5),
),
),
cellBorderColor: AppColors.transparent,
dataSource: MeetingDataSource(_getDataSource()),
monthCellBuilder: (context, details) => Padding(
padding: EdgeInsets.all(12.h),
child: details.date.day.toString().toText14(
isCenter: true,
color: details.date == _calendarController.selectedDate ? AppColors.primaryRedColor : AppColors.textColor,
isEnglishOnly: true,
),
),
monthViewSettings: MonthViewSettings(
dayFormat: "EEE",
appointmentDisplayMode: MonthAppointmentDisplayMode.indicator,
showTrailingAndLeadingDates: false,
appointmentDisplayCount: 1,
monthCellStyle: MonthCellStyle(
textStyle: TextStyle(fontSize: 19.f),
),
),
onTap: (CalendarTapDetails details) {
_calendarController.selectedDate = details.date;
_onDaySelected(details.date!);
},
),
onTap: (CalendarTapDetails details) {
_calendarController.selectedDate = details.date;
_onDaySelected(details.date!);
},
),
),
),
@ -153,35 +157,37 @@ class _AppointmentCalendarState extends State<AppointmentCalendar> {
SizedBox(height: 10.h),
Transform.translate(
offset: const Offset(0.0, -10.0),
child: selectedDateDisplay.toText16(isBold: true, isEnglishOnly: !isArabic),
child: ClarityUnmask(child: selectedDateDisplay.toText16(isBold: true, isEnglishOnly: !isArabic)),
),
//TODO: Add Next Day Span here
dayEvents.isNotEmpty
? ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.15,
minHeight: 0,
),
child: Directionality(
textDirection: isArabic ? ui.TextDirection.rtl : ui.TextDirection.ltr,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Wrap(
direction: Axis.horizontal,
alignment: WrapAlignment.start,
spacing: 6.h,
runSpacing: 6.h,
children: List.generate(
dayEvents.length,
(index) => TimeSlotChip(
label: dayEvents[index].isoTime!,
isSelected: index == selectedButtonIndex,
onTap: () {
setState(() {
selectedButtonIndex = index;
selectedTime = dayEvents[index].isoTime!;
});
},
? ClarityUnmask(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.15,
minHeight: 0,
),
child: Directionality(
textDirection: isArabic ? ui.TextDirection.rtl : ui.TextDirection.ltr,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Wrap(
direction: Axis.horizontal,
alignment: WrapAlignment.start,
spacing: 6.h,
runSpacing: 6.h,
children: List.generate(
dayEvents.length,
(index) => TimeSlotChip(
label: dayEvents[index].isoTime!,
isSelected: index == selectedButtonIndex,
onTap: () {
setState(() {
selectedButtonIndex = index;
selectedTime = dayEvents[index].isoTime!;
});
},
),
),
),
),

@ -49,6 +49,7 @@ class ClinicCard extends StatelessWidget {
width: 24.w,
height: 24.h,
fit: BoxFit.contain,
errorAsset: "assets/images/clinicIcons/1.svg"
// Note: Add error handling in the Utils.buildSvgWithAssets if possible
// or use a try-catch wrapper
),

@ -52,10 +52,11 @@ class DoctorCard extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
isLoading
Utils.buildImgWithNetwork(
url: isLoading
? "https://hmgwebservices.com/Images/MobileImages/OALAY/1439.png"
: doctorsListResponseModel.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/OALAY/1439.png",
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,
@ -120,15 +121,16 @@ class DoctorCard extends StatelessWidget {
.toString()
.toText12(isBold: true, color: AppColors.textColor, maxLine: 2, isEnglishOnly: true)
.toShimmer2(isShow: isLoading),
),
SizedBox(width: 6.w),
Image.network(
isLoading ? "https://hmgwebservices.com/Images/flag/SYR.png" : doctorsListResponseModel.nationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SYR.png",
width: 20.h,
height: 15.h,
fit: BoxFit.fill,
).toShimmer2(isShow: isLoading),
],
),
SizedBox(width: 6.w),
Utils.buildImgWithNetwork(
url: isLoading ? "https://hmgwebservices.com/Images/flag/SYR.png" : doctorsListResponseModel.nationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SYR.png",
iconColor: AppColors.transparent,
width: 20.h,
height: 15.h,
fit: BoxFit.fill,
).toShimmer2(isShow: isLoading),
],
),
SizedBox(height: 6.h),
Wrap(

@ -301,7 +301,7 @@ class _MyChildVaccinePageState extends State<MyChildVaccinePage> {
),
child: Row(
children: [
Image.asset(children[index]['image'], width: 24.h, height: 24.h),
Utils.buildImgWithAssets(icon: children[index]['image'], width: 24.h, height: 24.h),
SizedBox(width: 8.w),
Text(
"${children[index]['name']} (${children[index]['gender']})",

@ -282,8 +282,8 @@ class _ComprehensiveCheckupPageState extends State<ComprehensiveCheckupPage> {
final String imagePath = appStateLocal.isArabic() ? AppAssets.comprehensiveCheckupAr : AppAssets.comprehensiveCheckupEn;
return Stack(
children: [
Image.asset(
imagePath,
Utils.buildImgWithAssets(
icon: imagePath,
width: double.infinity,
fit: BoxFit.cover,
).paddingAll(16.w),

@ -259,8 +259,9 @@ class FeedbackPage extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
contactUsViewModel.patientFeedbackSelectedAppointment!.doctorImageURL!,
Utils.buildImgWithNetwork(
url: contactUsViewModel.patientFeedbackSelectedAppointment!.doctorImageURL!,
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -16,6 +16,8 @@ import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart';
import 'package:provider/provider.dart';
import '../../core/utils/utils.dart';
class FindUsPage extends StatelessWidget {
FindUsPage({super.key});
@ -120,8 +122,9 @@ class FindUsPage extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -70,8 +70,9 @@ class LiveChatPage extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -78,8 +78,9 @@ class FindUsItemCard extends StatelessWidget {
: SizedBox.shrink(),
Row(
children: [
Image.network(
getHMGLocationsModel.projectImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
Utils.buildImgWithNetwork(
url: getHMGLocationsModel.projectImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 40.h,
height: 40.h,
fit: BoxFit.cover,

@ -211,7 +211,7 @@ class _CovidPaymentScreenState extends State<CovidPaymentScreen> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.mada, width: 72.h, height: 25.h),
Utils.buildImgWithAssets(icon: AppAssets.mada, width: 72.h, height: 25.h),
SizedBox(height: 16.h),
LocaleKeys.mada.tr(context: context).toText16(isBold: true),
],
@ -252,9 +252,9 @@ class _CovidPaymentScreenState extends State<CovidPaymentScreen> {
children: [
Row(
children: [
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
Utils.buildImgWithAssets(icon: AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
Utils.buildImgWithAssets(icon: AppAssets.mastercard, width: 40.h, height: 40.h),
],
),
SizedBox(height: 16.h),
@ -296,21 +296,11 @@ class _CovidPaymentScreenState extends State<CovidPaymentScreen> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(
AppAssets.tamaraEng,
Utils.buildImgWithAssets(
icon: AppAssets.tamaraEng,
width: 72.h,
height: 25.h,
fit: BoxFit.contain,
// If PNG fails to load for any reason, log and fallback to SVG asset
errorBuilder: (context, error, stackTrace) {
debugPrint('Failed to load Tamara PNG asset: $error');
return Utils.buildSvgWithAssets(
icon: AppAssets.tamara,
width: 72.h,
height: 25.h,
fit: BoxFit.contain,
);
},
),
SizedBox(height: 16.h),
LocaleKeys.tamara.tr(context: context).toText16(isBold: true),

@ -102,7 +102,7 @@ class _ErOnlineCheckinPaymentPageState extends State<ErOnlineCheckinPaymentPage>
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.mada, width: 72.h, height: 25.h),
Utils.buildImgWithAssets(icon: AppAssets.mada, width: 72.h, height: 25.h),
SizedBox(height: 16.h),
LocaleKeys.mada.tr(context: context).toText16(isBold: true),
],
@ -139,6 +139,7 @@ class _ErOnlineCheckinPaymentPageState extends State<ErOnlineCheckinPaymentPage>
paymentReference: transactionData.transactionReference!,
appointmentNo: "0",
onSuccess: (val) {
emergencyServicesViewModel.setIsAdvanceERBalanceNeedToBeLoaded(true);
LoaderBottomSheet.hideLoader();
if (emergencyServicesViewModel.isERBookAppointment) {
showCommonBottomSheetWithoutHeight(
@ -187,9 +188,9 @@ class _ErOnlineCheckinPaymentPageState extends State<ErOnlineCheckinPaymentPage>
children: [
Row(
children: [
Image.asset(AppAssets.visa, width: 50.h, height: 50.h),
Utils.buildImgWithAssets(icon: AppAssets.visa, width: 50.h, height: 50.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
Utils.buildImgWithAssets(icon: AppAssets.mastercard, width: 40.h, height: 40.h),
],
),
SizedBox(height: 16.h),
@ -228,6 +229,7 @@ class _ErOnlineCheckinPaymentPageState extends State<ErOnlineCheckinPaymentPage>
paymentReference: transactionData.transactionReference!,
appointmentNo: "0",
onSuccess: (val) {
emergencyServicesViewModel.setIsAdvanceERBalanceNeedToBeLoaded(true);
LoaderBottomSheet.hideLoader();
if (emergencyServicesViewModel.isERBookAppointment) {
showCommonBottomSheetWithoutHeight(
@ -275,7 +277,7 @@ class _ErOnlineCheckinPaymentPageState extends State<ErOnlineCheckinPaymentPage>
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.tamaraEng, width: 72.h, height: 25.h),
Utils.buildImgWithAssets(icon: AppAssets.tamaraEng, width: 72.h, height: 25.h),
SizedBox(height: 16.h),
LocaleKeys.tamara.tr(context: context).toText16(isBold: true),
],
@ -492,6 +494,7 @@ class _ErOnlineCheckinPaymentPageState extends State<ErOnlineCheckinPaymentPage>
//TODO: Need to pass dynamic params to the Apple Pay instead of static values
await payfortViewModel.applePayRequestInsert(applePayInsertRequest: applePayInsertRequest).then((value) {
if (appState.isPaytabsEnabled) {
selectedPaymentMethod = "VISA";
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration(
"ER Online Check-In Payment",
@ -509,6 +512,7 @@ class _ErOnlineCheckinPaymentPageState extends State<ErOnlineCheckinPaymentPage>
paymentReference: transactionData.transactionReference!,
appointmentNo: "0",
onSuccess: (val) {
emergencyServicesViewModel.setIsAdvanceERBalanceNeedToBeLoaded(true);
LoaderBottomSheet.hideLoader();
if (emergencyServicesViewModel.isERBookAppointment) {
showCommonBottomSheetWithoutHeight(
@ -589,6 +593,7 @@ class _ErOnlineCheckinPaymentPageState extends State<ErOnlineCheckinPaymentPage>
paymentReference: payfortViewModel.payfortCheckPaymentStatusResponseModel!.fortId!,
appointmentNo: "0",
onSuccess: (val) {
emergencyServicesViewModel.setIsAdvanceERBalanceNeedToBeLoaded(true);
LoaderBottomSheet.hideLoader();
if (emergencyServicesViewModel.isERBookAppointment) {
showCommonBottomSheetWithoutHeight(

@ -59,7 +59,7 @@ class _RechargeWalletPageState extends State<RechargeWalletPage> {
@override
void dispose() {
// Clear errors before disposing
habibWalletVM.clearAllRechargeErrors();
// habibWalletVM.clearAllRechargeErrors();
_amountFocusNode.dispose();
amountTextController.dispose();

@ -2,8 +2,12 @@ import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:amazon_payfort/amazon_payfort.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_amazonpaymentservices/flutter_amazonpaymentservices.dart';
import 'package:hmg_patient_app_new/core/api_consts.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/cache_consts.dart';
@ -26,6 +30,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/in_app_browser/InAppBrowser.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:network_info_plus/network_info_plus.dart';
import 'package:provider/provider.dart';
class WalletPaymentConfirmPage extends StatefulWidget {
@ -85,7 +90,7 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(AppAssets.mada, width: 72.h, height: 25.h).toShimmer2(isShow: false),
Utils.buildImgWithAssets(icon: AppAssets.mada, width: 72.h, height: 25.h, fit: BoxFit.contain).toShimmer2(isShow: false),
SizedBox(height: 16.h),
LocaleKeys.mada.tr(context: context).toText16(isBold: true).toShimmer2(isShow: false),
],
@ -104,7 +109,7 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() {
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "MADA";
if (appState.isPaytabsEnabled) {
paytabsViewModel.setPaymentConfiguration("Advance Payment", habibWalletVM.walletRechargeAmount);
@ -127,6 +132,7 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(getIt.get<NavigationService>().navigatorKey.currentContext!, child: Utils.getSuccessWidget(loadingText: "Payment Successful!"),
callBackFunc: () {
habibWalletVM.setIsWalletAmountToBeLoaded(true);
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
@ -164,7 +170,80 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
);
});
} else {
openPaymentURL("mada");
if (appState.isPayfortHostedPageEnabled) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
transID = Utils.getAdvancePaymentTransID(habibWalletVM.selectedHospital!.iD!, int.parse(habibWalletVM.fileNumber));
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), projectId: habibWalletVM.selectedHospital!.iD!, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
fileNumber: habibWalletVM.fileNumber.toString(),
amount: habibWalletVM.walletRechargeAmount.toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader();
await habibWalletVM.HISCreateAdvancePayment(
paymentMethodName: selectedPaymentMethod,
paidAmount: habibWalletVM.walletRechargeAmount,
paymentReference: paymentResult.fortId!,
patientID: habibWalletVM.fileNumber,
projectID: habibWalletVM.selectedHospital!.iD!,
depositorName: habibWalletVM.depositorName,
onSuccess: (value) async {
await habibWalletVM.addAdvanceNumberRequest(
advanceNumber: Utils.isVidaPlusProject(habibWalletVM.selectedHospital!.iD)
? value.data['OnlineCheckInAppointments'][0]['AdvanceNumber_VP'].toString()
: value.data['OnlineCheckInAppointments'][0]['AdvanceNumber'].toString(),
paymentReference: paymentResult.fortId!,
onSuccess: (value) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(getIt.get<NavigationService>().navigatorKey.currentContext!,
child: Utils.getSuccessWidget(loadingText: "Payment Successful!"), callBackFunc: () {
habibWalletVM.setIsWalletAmountToBeLoaded(true);
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
}, isFullScreen: false, isCloseButtonVisible: true, isAutoDismiss: true);
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
},
onFailed: (err) {
print("Payment Failed Data: ${err.toString()}");
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
} else {
openPaymentURL("mada");
}
}
}),
SizedBox(height: 16.h),
@ -182,9 +261,9 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
children: [
Row(
children: [
Image.asset(AppAssets.visa, width: 40.h, height: 40.h),
Utils.buildImgWithAssets(icon: AppAssets.visa, width: 40.h, height: 40.h),
SizedBox(width: 8.h),
Image.asset(AppAssets.mastercard, width: 40.h, height: 40.h),
Utils.buildImgWithAssets(icon: AppAssets.mastercard, width: 40.h, height: 40.h, fit: BoxFit.contain),
],
).toShimmer2(isShow: false),
SizedBox(height: 16.h),
@ -205,7 +284,7 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
),
],
).paddingSymmetrical(16.h, 16.h),
).paddingSymmetrical(24.h, 0.h).onPress(() {
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "VISA";
if (appState.isPaytabsEnabled) {
paytabsViewModel.setPaymentConfiguration("Advance Payment", habibWalletVM.walletRechargeAmount);
@ -228,6 +307,7 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(getIt.get<NavigationService>().navigatorKey.currentContext!, child: Utils.getSuccessWidget(loadingText: "Payment Successful!"),
callBackFunc: () {
habibWalletVM.setIsWalletAmountToBeLoaded(true);
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
@ -265,7 +345,80 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
);
});
} else {
openPaymentURL("visa");
if (appState.isPayfortHostedPageEnabled) {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingPaymentInformation.tr(context: context));
transID = Utils.getAdvancePaymentTransID(habibWalletVM.selectedHospital!.iD!, int.parse(habibWalletVM.fileNumber));
await payfortViewModel.getPayfortConfigurations(
serviceId: ServiceTypeEnum.advancePayment.getIdFromServiceEnum(), projectId: habibWalletVM.selectedHospital!.iD!, integrationId: 1);
payfortViewModel.initiatePaymentWithCard(
transactionID: transID,
fileNumber: habibWalletVM.fileNumber.toString(),
amount: habibWalletVM.walletRechargeAmount.toString(),
onSucceeded: (paymentResult) async {
print("Payment Success Data: ${paymentResult.toString()}");
LoaderBottomSheet.showLoader();
await habibWalletVM.HISCreateAdvancePayment(
paymentMethodName: selectedPaymentMethod,
paidAmount: habibWalletVM.walletRechargeAmount,
paymentReference: paymentResult.fortId!,
patientID: habibWalletVM.fileNumber,
projectID: habibWalletVM.selectedHospital!.iD!,
depositorName: habibWalletVM.depositorName,
onSuccess: (value) async {
await habibWalletVM.addAdvanceNumberRequest(
advanceNumber: Utils.isVidaPlusProject(habibWalletVM.selectedHospital!.iD)
? value.data['OnlineCheckInAppointments'][0]['AdvanceNumber_VP'].toString()
: value.data['OnlineCheckInAppointments'][0]['AdvanceNumber'].toString(),
paymentReference: paymentResult.fortId!,
onSuccess: (value) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(getIt.get<NavigationService>().navigatorKey.currentContext!,
child: Utils.getSuccessWidget(loadingText: "Payment Successful!"), callBackFunc: () {
habibWalletVM.setIsWalletAmountToBeLoaded(true);
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
}, isFullScreen: false, isCloseButtonVisible: true, isAutoDismiss: true);
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
},
onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
},
onFailed: (err) {
print("Payment Failed Data: ${err.toString()}");
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
} else {
openPaymentURL("visa");
}
}
}),
],
@ -389,6 +542,7 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
//TODO: Need to pass dynamic params to the Apple Pay instead of static values
await payfortViewModel.applePayRequestInsert(applePayInsertRequest: applePayInsertRequest).then((value) {
if(appState.isPaytabsEnabled) {
selectedPaymentMethod = "VISA";
LoaderBottomSheet.hideLoader();
paytabsViewModel.setPaymentConfiguration("Advance Payment", habibWalletVM.walletRechargeAmount);
paytabsViewModel.startApplePayPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
@ -410,8 +564,9 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(getIt.get<NavigationService>().navigatorKey.currentContext!, child: Utils.getSuccessWidget(loadingText: "Payment Successful!"),
callBackFunc: () {
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
habibWalletVM.setIsWalletAmountToBeLoaded(true);
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
}, isFullScreen: false, isCloseButtonVisible: true, isAutoDismiss: true);
@ -451,7 +606,7 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
customerName: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}",
// customerEmail: projectViewModel.authenticatedUserObject.user.emailAddress,
customerEmail: "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com",
orderDescription: "Appointment Payment",
orderDescription: "Advance Payment",
orderAmount: double.parse(habibWalletVM.walletRechargeAmount.toString()),
merchantReference: transID,
merchantIdentifier: payfortViewModel.payfortProjectDetailsRespModel!.merchantIdentifier,
@ -502,6 +657,7 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(getIt.get<NavigationService>().navigatorKey.currentContext!, child: Utils.getSuccessWidget(loadingText: "Payment Successful!"),
callBackFunc: () {
habibWalletVM.setIsWalletAmountToBeLoaded(true);
habibWalletVM.initHabibWalletProvider();
habibWalletVM.getPatientBalanceAmount();
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).pop();
@ -621,4 +777,28 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
"",
"3");
}
Future<void> runSafePayment(Map<String, dynamic> requestParam, {String sdkToken = ""}) async {
dynamic result;
try {
// Force a 100ms microtask buffer to isolate the UI rendering channel
await Future.delayed(const Duration(milliseconds: 350));
result = await FlutterAmazonpaymentservices.normalPay(requestParam, ApiConsts.payFortEnvironmentType, isShowResponsePage: true);
print("Payment Success Data: ${result.toString()}");
} on PlatformException catch (e) {
// If the error is the MethodChannel duplicate reply bug, handle it safely
if (e.message != null && e.message!.contains("Reply already submitted")) {
print("Swallowed native channel race-condition crash safely.");
return;
}
// Process real payment rejection errors here
print("Actual Payment Failure: ${e.details.toString()}");
} catch (error) {
print("Generic Error: $error");
}
}
}

@ -1,6 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/api_consts.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';
@ -820,7 +821,7 @@ class _ServicesPageState extends State<ServicesPage> {
),
).onPress(() {
Utils.openWebView(
url: 'https://hmg.com/en/Pages/Terms.aspx',
url: AppUrls.hmgTermsUrlEn,
);
}),
),
@ -845,7 +846,7 @@ class _ServicesPageState extends State<ServicesPage> {
),
).onPress(() {
Utils.openWebView(
url: 'https://hmg.com/en/Pages/Privacy.aspx',
url: AppUrls.hmgPrivacyUrlEn,
);
}),
)

@ -18,7 +18,7 @@ import 'package:lottie/lottie.dart';
class AppUpdatePage extends StatelessWidget {
AppUpdatePage({super.key, required this.appUpdateMessage});
String appUpdateMessage;
final String appUpdateMessage;
@override
Widget build(BuildContext context) {
@ -72,21 +72,7 @@ class AppUpdatePage extends StatelessWidget {
openAppUpdateLink() {
if (Platform.isAndroid) {
// _launchURL("https://play.google.com/store/apps/details?id=com.ejada.hmg");
InAppUpdate.checkForUpdate().then((info) {
print("checkForUpdate!!!");
print(info.toString());
if (info.immediateUpdateAllowed) {
print("Immediate Allowed!!!");
InAppUpdate.performImmediateUpdate().then((value) {}).catchError((e) => print(e.toString()));
}
}).catchError((e) {
print(e.toString());
Utils.openWebView(
// url: "https://play.google.com/store/apps/details?id=com.ejada.hmg",
url: "https://play.google.com/store/apps/details?id=com.cloudsolutions.HMGPatientApp",
);
});
_handleAndroidAppUpdate();
}
if (Platform.isIOS) {
Utils.openWebView(
@ -95,4 +81,43 @@ class AppUpdatePage extends StatelessWidget {
);
}
}
Future<void> _handleAndroidAppUpdate() async {
const fallbackUrl = "https://play.google.com/store/apps/details?id=com.cloudsolutions.HMGPatientApp";
try {
final info = await InAppUpdate.checkForUpdate();
print("InAppUpdate.checkForUpdate -> $info");
if (info != null) {
final immediateAllowed = info.immediateUpdateAllowed ?? false;
final flexibleAllowed = info.flexibleUpdateAllowed ?? false;
print("immediateAllowed: $immediateAllowed, flexibleAllowed: $flexibleAllowed");
if (immediateAllowed) {
print("Performing immediate update...");
await InAppUpdate.performImmediateUpdate();
return;
}
if (flexibleAllowed) {
print("Starting flexible update...");
await InAppUpdate.startFlexibleUpdate();
try {
await InAppUpdate.completeFlexibleUpdate();
} catch (e) {
print("Error completing flexible update: $e");
}
return;
}
}
print("No in-app update allowed/available; opening Play Store page");
Utils.openWebView(url: fallbackUrl);
} catch (e, st) {
print("InAppUpdate.checkForUpdate threw an exception: $e");
print(st);
Utils.openWebView(url: fallbackUrl);
}
}
}

@ -304,7 +304,7 @@ class LakumWalletDetails extends StatelessWidget {
Widget _buildTransactionItem(PointsDetails transaction) {
// Determine if points were gained or used
bool isGained = transaction.operationType?.toLowerCase() == 'gain' || transaction.operationType?.toLowerCase() == 'credit' || (transaction.points ?? 0) > 0;
bool isGained = transaction.subTransactionType == 1 || transaction.operationType?.toLowerCase() == 'cr' || (transaction.points ?? 0) > 0;
Color transactionColor = isGained ? AppColors.habibPharmacyColor : AppColors.primaryRedColor;
String pointsText = "${isGained ? '+' : '-'}${transaction.points?.abs() ?? 0}";
@ -345,7 +345,7 @@ class LakumWalletDetails extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(transaction.subTransactionTypeDescription ?? transaction.operationType ?? "Transaction").toText14(
(isGained ? LocaleKeys.earned.tr() : LocaleKeys.consumed.tr()).toText14(
isBold: true,
maxlines: 2,
),

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:developer';
import 'dart:ui' as ui;
import 'package:clarity_flutter/clarity_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
@ -36,11 +37,13 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/appointments/appointment_queue_page.dart';
import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart';
import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_card.dart';
import 'package:hmg_patient_app_new/presentation/authentication/login.dart';
import 'package:hmg_patient_app_new/presentation/authentication/quick_login.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart';
import 'package:hmg_patient_app_new/presentation/contact_us/contact_us.dart';
import 'package:hmg_patient_app_new/presentation/hmg_services/services_page.dart';
import 'package:hmg_patient_app_new/presentation/home/app_update_page.dart';
import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dart';
import 'package:hmg_patient_app_new/presentation/home/widgets/habib_wallet_card.dart';
import 'package:hmg_patient_app_new/presentation/home/widgets/lakum_wallet_card.dart';
@ -59,6 +62,7 @@ import 'package:hmg_patient_app_new/presentation/todo_section/todo_page.dart';
import 'package:hmg_patient_app_new/presentation/todo_section/widgets/ancillary_orders_list.dart';
import 'package:hmg_patient_app_new/routes/app_routes.dart';
import 'package:hmg_patient_app_new/services/cache_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/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
@ -111,6 +115,7 @@ class _LandingPageState extends State<LandingPage> {
@override
void initState() {
Clarity.setCurrentScreenName('Landing Page');
authVM = context.read<AuthenticationViewModel>();
habibWalletVM = context.read<HabibWalletViewModel>();
appointmentRatingViewModel = context.read<AppointmentRatingViewModel>();
@ -156,9 +161,13 @@ class _LandingPageState extends State<LandingPage> {
appointmentRatingViewModel.getLastRatingAppointment(
onSuccess: (response) {
if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) {
final lastAppointment = appointmentRatingViewModel.appointmentRatedList.last;
final appointmentNo = lastAppointment.appointmentNo;
final projectID = lastAppointment.projectID;
if (appointmentNo == null || projectID == null) return;
appointmentRatingViewModel.getAppointmentDetails(
appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!,
appointmentRatingViewModel.appointmentRatedList.last.projectID!,
appointmentNo,
projectID,
onSuccess: ((response) {
appointmentRatingViewModel.setClinicOrDoctor(false);
appointmentRatingViewModel.setTitle(LocaleKeys.rateDoctor.tr(context: context));
@ -208,11 +217,31 @@ class _LandingPageState extends State<LandingPage> {
habibWalletVM.getLakumAccountInformation();
// Refresh Ancillary Orders
todoSectionViewModel.setIsAncillaryOrdersNeedReloading(true);
todoSectionViewModel.initializeTodoSectionViewModel();
// Refresh Immediate LiveCare Data
immediateLiveCareViewModel.initImmediateLiveCare();
immediateLiveCareViewModel.getPatientLiveCareHistory();
// appointmentRatingViewModel.getLastRatingAppointment(
// onSuccess: (response) {
// if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) {
// appointmentRatingViewModel.getAppointmentDetails(
// appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!,
// appointmentRatingViewModel.appointmentRatedList.last.projectID!,
// onSuccess: ((response) {
// appointmentRatingViewModel.setClinicOrDoctor(false);
// appointmentRatingViewModel.setTitle(LocaleKeys.rateDoctor.tr(context: context));
// appointmentRatingViewModel.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr(context: context));
// openLastRating();
// appState.setRatedVisible(true);
// }),
// );
// }
// },
// );
}
},
child: SingleChildScrollView(
@ -263,6 +292,16 @@ class _LandingPageState extends State<LandingPage> {
text: LocaleKeys.loginOrRegister.tr(context: context),
onPressed: () async {
await authVM.onLoginPressed();
// Navigator.of(context).push(
// CustomPageRoute(
// // page: NotificationsListPage(),
// page: AppUpdatePage(
// appUpdateMessage:
// "A new update for Dr.Alhabib app is now available with new features and services.\r\nFor technical support:\r\neservices.hmg@drsulaimanalhabib.com"),
// ),
// );
// ...existing commented code...
},
backgroundColor: AppColors.secondaryLightRedColor,
@ -1218,7 +1257,7 @@ class _LandingPageState extends State<LandingPage> {
},
backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.20),
textColor: AppColors.alertColor,
borderColor: AppColors.infoBannerBgColor,
borderColor: AppColors.warningColorYellow.withValues(alpha: 0.01),
fontSize: 12.f,
fontWeight: FontWeight.w600,
borderRadius: 12.r,
@ -1368,6 +1407,7 @@ class _LandingPageState extends State<LandingPage> {
orderNo: order.orderNo ?? 0,
projectID: order.projectID ?? 0,
projectName: order.projectName ?? "",
setupID: order.setupID ?? "",
),
),
);
@ -1377,7 +1417,7 @@ class _LandingPageState extends State<LandingPage> {
void showQuickLogin(BuildContext context) {
showCommonBottomSheetWithoutHeight(
context,
getIt<NavigationService>().navigatorKey.currentContext!,
// title: "",
isCloseButtonVisible: false,
child: StatefulBuilder(

@ -13,6 +13,8 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.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 '../../core/utils/utils.dart';
class ServiceInfoPage extends StatelessWidget {
final String serviceName;
final String serviceHeader;
@ -37,8 +39,8 @@ class ServiceInfoPage extends StatelessWidget {
borderRadius: BorderRadius.circular(24.r),
child: Transform.flip(
flipX: getIt.get<AppState>().isArabic(),
child: Image.asset(
serviceImage,
child: Utils.buildImgWithAssets(
icon: serviceImage,
fit: BoxFit.fitHeight,
// height: 480.h,
// width: 520.w,

@ -56,8 +56,8 @@ class LargeServiceCard extends StatelessWidget {
topLeft: Radius.circular(24.r),
topRight: Radius.circular(24.r),
),
child: Image.asset(
serviceCardData.largeCardIcon,
child: Utils.buildImgWithAssets(
icon: serviceCardData.largeCardIcon,
fit: BoxFit.cover,
width: double.infinity,
height: isFoldable ? 190.h : (isTablet ? 200.h : 180.h),
@ -72,7 +72,7 @@ class LargeServiceCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
isPNG
? Image.asset(serviceCardData.icon, width: 35.h, height: 35.h)
? Utils.buildImgWithAssets(icon: serviceCardData.icon, width: 35.h, height: 35.h)
: Container(
height: 48.h,
width: 48.h,
@ -204,7 +204,7 @@ class FadedLargeServiceCard extends StatelessWidget {
borderRadius: BorderRadius.circular(24.r),
child: Transform.flip(
flipX: getIt.get<AppState>().isArabic(),
child: Image.asset(serviceCardData.largeCardIcon, fit: BoxFit.cover, width: 520.w, height: 250.h)),
child: Utils.buildImgWithAssets(icon: serviceCardData.largeCardIcon, fit: BoxFit.cover, width: 520.w, height: 250.h)),
),
Positioned(
top: 0,
@ -234,7 +234,7 @@ class FadedLargeServiceCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
isPNG
? Image.asset(serviceCardData.icon, width: 32.h, height: 32.h).circle(100.h)
? Utils.buildImgWithAssets(icon: serviceCardData.icon, width: 32.h, height: 32.h).circle(100.h)
: Container(
height: 32.h,
width: 32.h,

@ -63,7 +63,7 @@ class WelcomeWidget extends StatelessWidget {
),
imageWidget ??
(imageUrl != null
? Image.asset(imageUrl!, width: 40, height: 40)
? Utils.buildImgWithAssets(icon: imageUrl!, width: 40, height: 40)
: SizedBox(width: 40, height: 40)),
Column(
crossAxisAlignment: CrossAxisAlignment.start,

@ -6,6 +6,7 @@ import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart';
@ -82,8 +83,9 @@ class InsuranceApprovalDetailsPage extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
insuranceApprovalResponseModel.doctorImageURL!,
Utils.buildImgWithNetwork(
url: insuranceApprovalResponseModel.doctorImageURL!,
iconColor: AppColors.blackBgColor,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,
@ -152,7 +154,7 @@ class InsuranceApprovalDetailsPage extends StatelessWidget {
child: insuranceApprovalResponseModel.apporvalDetails != null ? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.approvalDetails.toText16(isBold: true),
LocaleKeys.approvalDetails.tr().toText16(isBold: true),
ListView.separated(
padding: EdgeInsets.only(top: 16.h),
shrinkWrap: true,

@ -106,8 +106,9 @@ class InsuranceApprovalCard extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
isLoading ? "https://hmgwebservices.com/Images/MobileImages/OALAY/1439.png" : insuranceApprovalResponseModel.doctorImageURL!,
Utils.buildImgWithNetwork(
url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/OALAY/1439.png" : insuranceApprovalResponseModel.doctorImageURL!,
iconColor: AppColors.blackBgColor,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -55,8 +55,9 @@ class LabResultItemView extends StatelessWidget {
SizedBox(height: 8.h),
Row(
children: [
Image.network(
isLoading ? "" : labOrder!.doctorImageURL!,
Utils.buildImgWithNetwork(
url: isLoading ? "" : labOrder!.doctorImageURL!,
iconColor: AppColors.transparent,
width: 24.h,
height: 24.h,
fit: BoxFit.cover,

@ -1,9 +1,9 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:hmg_patient_app_new/core/app_assets.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';
import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart';
import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_order_response_by_ai_model.dart';
@ -37,7 +37,13 @@ class AiAnalysisWidget extends StatelessWidget {
Container(
padding: EdgeInsets.all(8.r),
decoration: BoxDecoration(gradient: AppColors.aiLinearGradient, borderRadius: BorderRadius.circular(8.r)),
child: SvgPicture.asset(AppAssets.aiOverView, colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), height: 18.r, width: 18.r),
child: Utils.buildSvgWithAssets(
icon: AppAssets.aiOverView,
iconColor: Colors.white,
height: 18.r,
width: 18.r,
applyThemeColor: false,
),
),
],
),

@ -908,8 +908,9 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
child: FadeInAnimation(
child: Row(
children: [
Image.network(
prescriptionVM.patientPrescriptionOrders[index].doctorImageURL!,
Utils.buildImgWithNetwork(
url: prescriptionVM.patientPrescriptionOrders[index].doctorImageURL!,
iconColor: AppColors.transparent,
width: 40.h,
height: 40.h,
fit: BoxFit.cover,
@ -1059,8 +1060,9 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
? Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 64.h,
height: 64.h,
fit: BoxFit.cover,
@ -1106,8 +1108,9 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.network(
myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!,
Utils.buildImgWithNetwork(
url: myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!,
iconColor: AppColors.transparent,
width: 64.h,
height: 64.h,
fit: BoxFit.cover,

@ -139,8 +139,9 @@ class _PatientSickleavesListPageState extends State<PatientSickleavesListPage> {
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
model.patientSickLeaveList[index].doctorImageURL!,
Utils.buildImgWithNetwork(
url: model.patientSickLeaveList[index].doctorImageURL!,
iconColor: AppColors.transparent,
width: 24.h,
height: 24.h,
fit: BoxFit.fill,

@ -83,8 +83,9 @@ class _VaccineListPageState extends State<VaccineListPage> {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
Utils.buildImgWithNetwork(
url: "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
fit: BoxFit.cover,

@ -163,9 +163,10 @@ class _MedicalFileAppointmentCardState extends State<MedicalFileAppointmentCard>
children: [
Row(
children: [
Image.network(
widget.patientAppointmentHistoryResponseModel.doctorImageURL ??
Utils.buildImgWithNetwork(
url: widget.patientAppointmentHistoryResponseModel.doctorImageURL ??
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
iconColor: AppColors.transparent,
width: 30.h,
height: 30.h,
fit: BoxFit.fill,

@ -56,8 +56,9 @@ class PatientSickLeaveCard extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientSickLeavesResponseModel.doctorImageURL!,
Utils.buildImgWithNetwork(
url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientSickLeavesResponseModel.doctorImageURL!,
iconColor: AppColors.transparent,
width: 30.h,
height: 30.h,
fit: BoxFit.cover,

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save