Merge branch 'refs/heads/master' into faiz_dev

# Conflicts:
#	lib/core/api/api_client.dart
#	lib/core/api_consts.dart
#	lib/presentation/contact_us/feedback_page.dart
faiz_dev
FaizHashmi 2 months ago
commit b75e72636a

@ -36,7 +36,7 @@ android {
applicationId = "com.cloudsolutions.HMGPatientApp"
// minSdk = 24
minSdk = 29
targetSdk = 35
targetSdk = 36
compileSdk = 36
// targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode

@ -0,0 +1,26 @@
<resources>
<color name="payment_sdk_primary_color">#ffffff</color>
<color name="payment_sdk_secondary_color">#ED1C2B</color>
<color name="payment_sdk_status_bar_color">#444647</color>
<color name="payment_sdk_primary_font_color">#2E3039</color>
<color name="payment_sdk_secondary_font_color">#2E3039</color>
<color name="payment_sdk_hint_font_color">#a5a5a5</color>
<color name="payment_sdk_stroke_color">#e1e1e1</color>
<color name="payment_sdk_button_text_color">#FFF</color>
<color name="payment_sdk_title_text_color">#1e1e1e</color>
<color name="payment_sdk_button_background_color">#ED1C2B</color>
<color name="payment_sdk_background_color">#F9FAFD</color>
<color name="payment_sdk_blue_F2FAFD">#F2FAFD</color>
<color name="payment_sdk_error_text_color">#EC2213</color>
<color name="payment_sdk_back_black_dim">#4D6E6E6E</color>
<color name="payment_sdk_input_field_background_color">#FFFFFFFF</color>
<color name="payment_sdk_enabled_switch_track_color">#00000000</color>
<color name="payment_sdk_enabled_switch_handle_color">#3db39e</color>
<color name="payment_sdk_disabled_switch_track_color">#00000000</color>
<color name="payment_sdk_disabled_switch_handle_color">#c7c7c7</color>
<color name="payment_sdk_switch_stroke_color">#4c4c4c</color>
<color name="payment_sdk_amount_font_color">#4c4c4c</color>
<color name="payment_sdk_original_amount_font_color">#a5a5a5</color>
<color name="payment_sdk_billing_header_background_color">#ED1C2B</color>
<color name="payment_sdk_billing_text_color">#FFF</color>
</resources>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="120" height="120" fill="#EFF1F3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.2503 38.4816C33.2603 37.0472 34.4199 35.8864 35.8543 35.875H83.1463C84.5848 35.875 85.7503 37.0431 85.7503 38.4816V80.5184C85.7403 81.9528 84.5807 83.1136 83.1463 83.125H35.8543C34.4158 83.1236 33.2503 81.957 33.2503 80.5184V38.4816ZM80.5006 41.1251H38.5006V77.8751L62.8921 53.4783C63.9172 52.4536 65.5788 52.4536 66.6039 53.4783L80.5006 67.4013V41.1251ZM43.75 51.6249C43.75 54.5244 46.1005 56.8749 49 56.8749C51.8995 56.8749 54.25 54.5244 54.25 51.6249C54.25 48.7254 51.8995 46.3749 49 46.3749C46.1005 46.3749 43.75 48.7254 43.75 51.6249Z" fill="#687787"/>
</svg>

After

Width:  |  Height:  |  Size: 892 B

@ -0,0 +1,12 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.9384 3.88371e-06C3.28428 -5.8582e-05 2.69656 -0.000114493 2.22085 0.0638424C1.70149 0.133668 1.16868 0.295802 0.732241 0.732241C0.295802 1.16868 0.133668 1.70149 0.0638424 2.22085C-0.000114493 2.69656 -5.8582e-05 3.28428 3.88371e-06 3.9384L3.88371e-06 4.06161C-5.8582e-05 4.71574 -0.000114493 5.30346 0.0638424 5.77916C0.133668 6.29852 0.295802 6.83134 0.732241 7.26777C1.16868 7.70421 1.70149 7.86635 2.22085 7.93617C2.69656 8.00013 3.28427 8.00007 3.9384 8.00001H4.06162C4.71574 8.00007 5.30346 8.00013 5.77916 7.93617C6.29852 7.86635 6.83134 7.70421 7.26777 7.26777C7.70421 6.83134 7.86635 6.29852 7.93617 5.77916C8.00013 5.30346 8.00007 4.71574 8.00001 4.06162V3.9384C8.00007 3.28427 8.00013 2.69656 7.93617 2.22085C7.86635 1.70149 7.70421 1.16868 7.26777 0.732241C6.83134 0.295802 6.29852 0.133668 5.77916 0.0638424C5.30346 -0.000114493 4.71574 -5.8582e-05 4.06161 3.88371e-06L3.9384 3.88371e-06Z" fill="#8F9AA3"/>
<path d="M3.9384 12C3.28428 11.9999 2.69655 11.9999 2.22085 12.0638C1.70149 12.1337 1.16868 12.2958 0.732241 12.7322C0.295802 13.1687 0.133668 13.7015 0.0638424 14.2209C-0.000114493 14.6966 -5.8582e-05 15.2843 3.88371e-06 15.9384L3.88371e-06 16.0616C-5.8582e-05 16.7157 -0.000114493 17.3035 0.0638424 17.7792C0.133668 18.2985 0.295802 18.8313 0.732241 19.2678C1.16868 19.7042 1.70149 19.8663 2.22085 19.9362C2.69656 20.0001 3.28427 20.0001 3.9384 20H4.06162C4.71574 20.0001 5.30346 20.0001 5.77916 19.9362C6.29852 19.8663 6.83134 19.7042 7.26777 19.2678C7.70421 18.8313 7.86635 18.2985 7.93617 17.7792C8.00013 17.3035 8.00007 16.7157 8.00001 16.0616V15.9384C8.00007 15.2843 8.00013 14.6966 7.93617 14.2209C7.86635 13.7015 7.70421 13.1687 7.26777 12.7322C6.83134 12.2958 6.29852 12.1337 5.77916 12.0638C5.30346 11.9999 4.71575 11.9999 4.06163 12H3.9384Z" fill="#8F9AA3"/>
<path d="M0 10C0 9.44771 0.447715 9 1 9L7 9C7.55228 9 8 9.44772 8 10C8 10.5523 7.55228 11 7 11L1 11C0.447715 11 0 10.5523 0 10Z" fill="#8F9AA3"/>
<path d="M11 1C11 0.447715 10.5523 0 10 0C9.44771 0 9 0.447715 9 1L9 6C9 6.55228 9.44771 7 10 7C10.5523 7 11 6.55228 11 6L11 1Z" fill="#8F9AA3"/>
<path d="M15.9384 3.88371e-06L16.0616 3.88371e-06C16.7157 -5.8582e-05 17.3035 -0.000114493 17.7792 0.0638424C18.2985 0.133668 18.8313 0.295802 19.2678 0.732241C19.7042 1.16868 19.8663 1.70149 19.9362 2.22085C20.0001 2.69656 20.0001 3.28427 20 3.9384V4.06162C20.0001 4.71574 20.0001 5.30346 19.9362 5.77916C19.8663 6.29852 19.7042 6.83134 19.2678 7.26777C18.8313 7.70421 18.2985 7.86635 17.7792 7.93617C17.3035 8.00013 16.7157 8.00007 16.0616 8.00001H15.9384C15.2843 8.00007 14.6966 8.00013 14.2209 7.93617C13.7015 7.86635 13.1687 7.70421 12.7322 7.26777C12.2958 6.83134 12.1337 6.29852 12.0638 5.77916C11.9999 5.30346 11.9999 4.71575 12 4.06163V3.9384C11.9999 3.28428 11.9999 2.69655 12.0638 2.22085C12.1337 1.70149 12.2958 1.16868 12.7322 0.732241C13.1687 0.295802 13.7015 0.133668 14.2209 0.0638424C14.6966 -0.000114493 15.2843 -5.8582e-05 15.9384 3.88371e-06Z" fill="#8F9AA3"/>
<path d="M12.9384 9C12.2843 8.99994 11.6966 8.99989 11.2209 9.06384C10.7015 9.13367 10.1687 9.2958 9.73224 9.73224C9.2958 10.1687 9.13367 10.7015 9.06384 11.2209C8.99989 11.6966 8.99994 12.2843 9 12.9384L9.00001 13C9.00001 13.5523 9.44772 14 10 14C10.5523 14 11 13.5523 11 13C11 12.2646 11.0021 11.8137 11.046 11.4873C11.0846 11.2005 11.1394 11.1526 11.1459 11.1469L11.1469 11.1459C11.1526 11.1394 11.2005 11.0846 11.4873 11.046C11.8137 11.0021 12.2646 11 13 11L19 11C19.5523 11 20 10.5523 20 10C20 9.44772 19.5523 9.00001 19 9.00001L12.9384 9Z" fill="#8F9AA3"/>
<path d="M16 14C16.7354 14 17.1862 14.0021 17.5125 14.0461C17.799 14.0846 17.8468 14.1395 17.8526 14.146L17.8536 14.1471C17.8604 14.1531 17.9154 14.2016 17.954 14.4891C17.9979 14.8162 18 15.268 18 16.0043C18 16.7407 17.9979 17.1925 17.954 17.5196C17.9154 17.8071 17.8604 17.8556 17.8536 17.8616L17.8522 17.863C17.8442 17.8712 17.8181 17.8979 17.7034 17.9272C17.5612 17.9636 17.3416 17.9892 16.9698 18.0005C16.4178 18.0171 15.9838 18.4781 16.0005 19.0302C16.0171 19.5822 16.4781 20.0162 17.0302 19.9996C17.7921 19.9766 18.64 19.9045 19.2683 19.2753C19.7045 18.8385 19.8664 18.3054 19.9362 17.7857C20.0001 17.3095 20.0001 16.7211 20 16.066V15.9428C20.0001 15.2876 20.0001 14.6992 19.9362 14.223C19.8664 13.7033 19.7045 13.1702 19.2683 12.7334C18.8319 12.2964 18.299 12.1339 17.7794 12.0639C17.3036 11.9999 16.7157 11.9999 16.0617 12L16 12C15.4477 12 15 12.4477 15 13C15 13.5523 15.4477 14 16 14Z" fill="#8F9AA3"/>
<path d="M14 13C14 12.4477 13.5523 12 13 12C12.4477 12 12 12.4477 12 13V14.5C12 15.394 12.2542 16.2178 13.0565 16.6656C13.3946 16.8543 13.7549 16.928 14.0621 16.9627C14.3698 16.9975 14.6995 17 15 17C15 17.5523 15.4477 18 16 18C16.5523 18 17 17.5523 17 17C17 15.8954 16.1046 15 15 15C14.6924 15 14.468 14.9958 14.2866 14.9753C14.152 14.9601 14.0822 14.9397 14.051 14.9279C14.0333 14.8814 14 14.7576 14 14.5V13Z" fill="#8F9AA3"/>
<path d="M11 15.7692C11 15.217 10.5523 14.7692 10 14.7692C9.44772 14.7692 9.00001 15.217 9.00001 15.7692L9.00001 18.5385C9.00001 19.0908 9.44772 19.5385 10 19.5385C10.5523 19.5385 11 19.0908 11 18.5385L11 15.7692Z" fill="#8F9AA3"/>
<path d="M13 18C12.4477 18 12 18.4477 12 19C12 19.5523 12.4477 20 13 20H14C14.5523 20 15 19.5523 15 19C15 18.4477 14.5523 18 14 18H13Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

@ -123,7 +123,7 @@
"forgotFileNoTitle": "نسيت الملف الطبي",
"enterNationalId": "يرجى إدخال رقم الهاتف المحمول ورقم الهوية",
"profileInfo": "يرجى إدخال معلومات الملف الشخصي",
"submit": "بحث",
"submit": "تأكيد",
"forgotDesc": "أدخل رقم الهاتف المحمول لاستلام رقم الملف الطبي عبر الرسائل القصيرة",
"dob": "تاريخ الميلاد",
"hijriDate": "التاريخ الهجري",
@ -1390,7 +1390,6 @@
"medicalFiles": "الملفات الطبية",
"addANewFamilyMember": "اضافة",
"viewInvoiceDetails": "عرض تفاصيل الفاتورة",
"outPatient": "مريض خارجي",
"invoiceDetails": "تفاصيل الفاتورة",
"sendingEmailPleaseWait": "جاري إرسال ال<D8A7><D984>ريد الإلكتروني، يرجى الانتظار...",
"emailSentSuccessfullyMessage": "تم إرسال البريد الإلكتروني بنجاح.",
@ -1950,7 +1949,7 @@
"transactions": "المعاملات",
"pharmacy": "الصيدلية",
"visitsOrders": "الزيارات/الطلبات",
"earned": "حصل",
"earned": "مكتسب",
"getParkingQR": "الحصول على رمز الاستجابة السريعة لوقوف السيارات",
"cannotOpenThisFile": "لا يمكن فتح هذا الملف",
"thisInvoiceIsNotEligibleForRefund": "هذه الفاتورة غير مؤهلة للاسترداد",
@ -1977,11 +1976,15 @@
"pharmacyRefundPhysicalVisitNotice": "تتطلب استردادات الصيدلية زيارة شخصية لتسليم الأدوية ومعالجة الاسترداد.",
"parkingQR": "رمز الاستجابة السريعة لوقوف السيارات",
"parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:",
"livecareUpdateInsuranceSent": "تم استلام طلب تحديث بيانات التأمين الخاص بك، ويجري العمل عليه حالياً. سنقوم بإشعارك فور اكتمال التحديث.",
"livecareUpdateInsurance": "سيتم تحويل طلب تحديث بيانات التأمين إلى فريق LiveCare المختص لاستكمال الإجراءات. هل تود المتابعة؟",
"schedule": "جدول",
"doctorSchedule": "جدول الطبيب",
"weeklyShift": "الفترات الأسبوعية",
"dayOff": "إجازة",
"notAvailable": "غير متاح"
"livecareUpdateInsuranceSent": "تم استلام طلب تحديث بيانات التأمين الخاص بك، ويجري العمل عليه حالياً. سنقوم بإشعارك فور اكتمال التحديث.",
"livecareUpdateInsurance": "سيتم تحويل طلب تحديث بيانات التأمين إلى فريق LiveCare المختص لاستكمال الإجراءات. هل تود المتابعة؟",
"schedule": "جدول",
"doctorSchedule": "جدول الطبيب",
"weeklyShift": "الفترات الأسبوعية",
"dayOff": "إجازة",
"notAvailable": "غير متاح",
"hide": "إخفاء",
"viewPatientQR": "عرض رمز الاستجابة السريعة للمريض",
"patientQR": "رمز الاستجابة السريعة للمريض",
"forText": "عن"
}

@ -21,7 +21,7 @@
"mySchedule": "My Schedule",
"logout": "Logout",
"respirationRate": "Respiration Rate",
"bookAppo": "New Appointment",
"bookAppo": "Book Appointment",
"searchBy": "Search By:",
"clinic": "Clinic",
"byClinic": "By Clinic",
@ -1967,13 +1967,17 @@
"pharmacyRefundPhysicalVisitNotice": "Pharmacy refunds require a physical visit to hand over medications and process the refund.",
"parkingQR": "Parking QR Code",
"parkingQRDesc": "Scan the below QR Code at the exit parking gate:",
"livecareUpdateInsuranceSent": "Your insurance update request is being processed. You will be notified once the update is completed.",
"livecareUpdateInsurance": "Please note that your insurance update request will be sent to the our LiveCare team for update. Would you like to proceed?",
"schedule": "Schedule",
"doctorSchedule": "Doctor Schedule",
"weeklyShift": "Weekly Shift",
"dayOff": "Off",
"notAvailable": "Not Available"
"livecareUpdateInsuranceSent": "Your insurance update request is being processed. You will be notified once the update is completed.",
"livecareUpdateInsurance": "Please note that your insurance update request will be sent to the our LiveCare team for update. Would you like to proceed?",
"schedule": "Schedule",
"doctorSchedule": "Doctor Schedule",
"weeklyShift": "Weekly Shift",
"dayOff": "Off",
"notAvailable": "Not Available",
"hide": "Hide",
"viewPatientQR": "View Patient QR Code",
"patientQR": "Patient QR Code",
"forText": "For"
}

@ -8,8 +8,8 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
1587365701FF0D2AAAFED0D8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A83C76CFF9D3F92EDE3B22B4 /* Pods_Runner.framework */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
37D27FAF2C38AC83B852680C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A83C76CFF9D3F92EDE3B22B4 /* Pods_Runner.framework */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
478CFA942E638C8E0064F3D7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 478CFA932E638C8E0064F3D7 /* GoogleService-Info.plist */; };
61243B492EC5FA3700D46FA0 /* PenguinModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61243B422EC5FA3700D46FA0 /* PenguinModel.swift */; };
@ -100,7 +100,7 @@
76B1D2512F91BF8000DBBD1B /* Penguin.xcframework in Frameworks */,
76B1D2552F91BF8000DBBD1B /* PenNavUI.xcframework in Frameworks */,
76B1D2532F91BF8000DBBD1B /* PenguinINRenderer.xcframework in Frameworks */,
DE1EF78253E64BE86845D2CC /* Pods_Runner.framework in Frameworks */,
37D27FAF2C38AC83B852680C /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -339,10 +339,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@ -356,10 +360,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@ -519,7 +527,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 3A359E86ZF;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -529,7 +537,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.0.5;
MARKETING_VERSION = 0.0.12;
PRODUCT_BUNDLE_IDENTIFIER = com.cloudsolutions.HMGPatientApp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -706,7 +714,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 3A359E86ZF;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -716,7 +724,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.0.5;
MARKETING_VERSION = 0.0.12;
PRODUCT_BUNDLE_IDENTIFIER = com.cloudsolutions.HMGPatientApp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -736,7 +744,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 3A359E86ZF;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -746,7 +754,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.0.5;
MARKETING_VERSION = 0.0.12;
PRODUCT_BUNDLE_IDENTIFIER = com.cloudsolutions.HMGPatientApp;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

@ -86,7 +86,7 @@
<key>NSMicrophoneUsageDescription</key>
<string>This app requires microphone access to enable virtual consultation between patient &amp; doctor</string>
<key>NSMotionUsageDescription</key>
<string>This app requires motion detection access to function properly.</string>
<string>This app requires access to motion detection to count your daily steps.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app requires photo library access to select image as document &amp; upload it.</string>
<key>NSPhotoLibraryAddUsageDescription</key>

@ -3,7 +3,7 @@ import 'package:flutter_amazonpaymentservices/environment_type.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
class ApiConsts {
static const maxSmallScreen = 660;
static const maxSmallScreen = 660;
static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat;
@ -303,7 +303,7 @@ class ApiConsts {
static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations";
// ************ static values for Api ****************
static final double appVersionID = 21.5;
static final double appVersionID = 21.8;
static final int appChannelId = 3;
static final String appIpAddress = "10.20.10.20";
@ -919,8 +919,6 @@ var AUTO_GENERATE_INVOICE_TAMARA = 'Services/PayFort_Serv.svc/REST/Tamara_Getinf
var GET_ONESIGNAL_VOIP_TOKEN = 'https://onesignal.com/api/v1/players';
var CANCEL_PHARMA_LIVECARE_REQUEST = 'https://vcallapi.hmg.com/api/PharmaLiveCare/SendPaymentStatus';
var INSERT_FREE_SLOTS_LOGS = 'Services/Doctors.svc/Rest/InsertDoctorFreeSlotsLogs';
var GET_NATIONALITY = 'Services/Lists.svc/REST/GetNationality';

@ -400,6 +400,8 @@ class AppAssets {
static const String habibLogoOnly = '$svgBasePath/habib logo_only.svg';
static const String descriptionIcon = '$svgBasePath/description_icon.svg';
static const String selectImageIcon = '$svgBasePath/select_image_icon.svg';
static const String patientQRCodeIcon = '$svgBasePath/patient_qr_code_icon.svg';
static const String noImageFound = '$svgBasePath/no_image_found.svg';
// PNGS //
static const String hmgLogo = '$pngBasePath/hmg_logo.png';

@ -84,6 +84,7 @@ import 'package:hmg_patient_app_new/services/logger_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/services/notification_service.dart';
import 'package:hmg_patient_app_new/services/permission_service.dart';
import 'package:hmg_patient_app_new/core/services/turnstile_service.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart';
import 'package:local_auth/local_auth.dart';
@ -155,6 +156,7 @@ class AppDependencies {
);
getIt.registerLazySingleton<PermissionService>(() => PermissionService());
getIt.registerLazySingleton<TurnstileService>(() => TurnstileService(getIt<LoggerService>()));
// Repositories
getIt.registerLazySingleton<CommonRepo>(() => CommonRepoImp(loggerService: getIt()));
@ -255,7 +257,8 @@ class AppDependencies {
dialogService: getIt(),
appState: getIt(),
errorHandlerService: getIt(),
localAuthService: getIt()),
localAuthService: getIt(),
turnstileService: getIt()),
);
getIt.registerLazySingleton<ProfileSettingsViewModel>(() => ProfileSettingsViewModel(
@ -366,7 +369,11 @@ class AppDependencies {
errorHandlerService: getIt(),
));
getIt.registerLazySingleton<PayTabsViewModel>(() => PayTabsViewModel(appState: getIt()));
getIt.registerLazySingleton<PayTabsViewModel>(() => PayTabsViewModel(
appState: getIt(),
payfortRepo: getIt(),
errorHandlerService: getIt(),
));
getIt.registerLazySingleton<ClarityService>(
() => ClarityService(appState: getIt.get<AppState>()),

@ -0,0 +1,60 @@
import 'dart:async';
import 'package:cloudflare_turnstile/cloudflare_turnstile.dart';
import 'package:flutter/foundation.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
/// Service to handle Cloudflare Turnstile captcha verification
class TurnstileService {
final LoggerService _loggerService;
// Replace with your Cloudflare Turnstile site key
static const String _siteKey = '0x4AAAAAAAkmbvEy63pcxBBt';
TurnstileService(this._loggerService);
/// Get Turnstile token using invisible mode
/// Returns the token on success, null on failure
Future<String?> getTurnstileToken() async {
// Initialize an instance of invisible Cloudflare Turnstile with your site key
final turnstile = CloudflareTurnstile.invisible(
siteKey: _siteKey,
onTimeout: () {},
options: TurnstileOptions(
retryInterval: const Duration(milliseconds: 2000),
),
);
try {
// Get the Turnstile token
final token = await turnstile.getToken();
_loggerService.logInfo('Turnstile token retrieved successfully');
return token; // Return the token upon success
} on TurnstileException catch (e) {
// Handle Turnstile failure
_loggerService.logError('Turnstile challenge failed: ${e.message}');
if (kDebugMode) {
print('Challenge failed: ${e.message}');
}
} catch (e) {
// Handle any other exceptions
_loggerService.logError('Unexpected error during Turnstile challenge: $e');
if (kDebugMode) {
print('Unexpected error: $e');
}
} finally {
// Ensure the Turnstile instance is properly disposed of
turnstile.dispose();
}
// Return null if the token couldn't be generated
return null;
}
/// Get the site key for Turnstile widget
String getSiteKey() => _siteKey;
/// Validate if Turnstile is properly configured
bool isConfigured() {
return _siteKey.isNotEmpty && _siteKey != '0x4AAAAAAAkmbvEy63pcxBBt';
}
}

@ -18,7 +18,8 @@ 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));
// ), isUtc: true).add(Duration(hours: 3));
), isUtc: true).toLocal();
}
static DateTime convertStringToDateSaudiTimezone(String date, int projectId) {

@ -19,7 +19,12 @@ import 'package:hmg_patient_app_new/core/api/api_client.dart';
import 'package:hmg_patient_app_new/core/api_consts.dart';
import 'package:hmg_patient_app_new/core/cache_consts.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/features/notifications/models/resp_models/notification_response_model.dart';
import 'package:hmg_patient_app_new/presentation/notifications/notification_details_page.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:uuid/uuid.dart';
@ -523,6 +528,26 @@ class PushNotificationHandler {
_incomingCall(remoteMessage.data);
// showCallkitIncoming();
} else {
NotificationResponseModel notification = NotificationResponseModel();
notification.createdOn = DateUtil.convertDateToString(DateTime.now());
notification.isSentOn = DateUtil.convertDateToString(DateTime.now());
notification.messageTypeData = remoteMessage.data['picture'];
notification.messageType = notification.messageTypeData != null && notification.messageTypeData!.isNotEmpty ? "image" : "";
notification.message = remoteMessage.data['message'];
notification.notificationType = remoteMessage.data["NotificationType"].toString();
if (remoteMessage.data["NotificationType"] == "2") {
notification.videoURL = remoteMessage.data["VideoUrl"];
}
Navigator.of(getIt.get<NavigationService>().navigatorKey.currentContext!).push(
CustomPageRoute(
page: NotificationDetailsPage(
notification: notification,
),
),
);
// GetNotificationsResponseModel notification = new GetNotificationsResponseModel();
//
// notification.createdOn = DateUtil.convertDateToString(DateTime.now());

@ -27,7 +27,7 @@ class RequestUtils {
}) {
bool fileNo = false;
if (nationId.isNotEmpty) {
final numericRegex = RegExp(r'^[0-9]+$');
final numericRegex = RegExp(r'^[0-9]+$');
fileNo = nationId.length < 10 && numericRegex.hasMatch(nationId);
//fileNo = nationId.length < 10 && nationId.isNumericOnly() ;
if (fileNo) {
@ -40,14 +40,23 @@ class RequestUtils {
if (zipCode == "0") {
request.patientMobileNumberOthers = phoneNumber;
} else {
request.patientMobileNumber = int.parse(phoneNumber);
// Remove any non-numeric characters before parsing
final numericPhone = phoneNumber.replaceAll(RegExp(r'[^0-9]'), '');
final parsedPhone = int.tryParse(numericPhone);
if (parsedPhone != null) {
request.patientMobileNumber = parsedPhone;
} else {
// If parsing fails, use as string in Others field
request.patientMobileNumberOthers = phoneNumber;
}
}
}
request.oTPSendType = otpTypeEnum.toInt(); // could map OTPTypeEnum if needed
request.zipCode = zipCode; // or countryCode if defined elsewhere
if (isForRegister) {
request.patientIdentificationID = int.parse(nationId);
final parsedNationId = int.tryParse(nationId.replaceAll(RegExp(r'[^0-9]'), ''));
request.patientIdentificationID = parsedNationId ?? 0;
request.searchType = 1;
request.isHijri = calenderType.toInt;
request.patientID = patientId;
@ -56,7 +65,7 @@ class RequestUtils {
request.isDentalAllowedBackend = false;
} else {
if (fileNo) {
request.patientID = patientId ?? int.parse(nationId);
request.patientID = patientId ?? (int.tryParse(nationId.replaceAll(RegExp(r'[^0-9]'), '')) ?? 0);
request.patientIdentificationID = request.nationalID;
request.searchType = 2;
} else {
@ -71,15 +80,15 @@ class RequestUtils {
static dynamic getCommonRequestWelcome(
{required String phoneNumber,
required OTPTypeEnum otpTypeEnum,
required String? deviceToken,
required bool patientOutSA,
required String? loginTokenID,
RegistrationDataModelPayload? registeredData,
int? patientId,
required String nationIdText,
required String countryCode,
required int loginType}) {
required OTPTypeEnum otpTypeEnum,
required String? deviceToken,
required bool patientOutSA,
required String? loginTokenID,
RegistrationDataModelPayload? registeredData,
int? patientId,
required String nationIdText,
required String countryCode,
required int loginType}) {
bool fileNo = false;
if (nationIdText.isNotEmpty) {
final numericRegex = RegExp(r'^[0-9]+$');
@ -91,8 +100,17 @@ class RequestUtils {
request.patientMobileNumberOthers = phoneNumber;
request.mobileNo = phoneNumber;
} else {
request.patientMobileNumber = int.parse(phoneNumber);
request.mobileNo = '0$phoneNumber';
// Remove any non-numeric characters before parsing
final numericPhone = phoneNumber.replaceAll(RegExp(r'[^0-9]'), '');
final parsedPhone = int.tryParse(numericPhone);
if (parsedPhone != null) {
request.patientMobileNumber = parsedPhone;
request.mobileNo = '0$numericPhone';
} else {
// If parsing fails, use as string in Others field
request.patientMobileNumberOthers = phoneNumber;
request.mobileNo = phoneNumber;
}
}
request.deviceToken = deviceToken;
request.projectOutSA = patientOutSA;
@ -106,8 +124,8 @@ class RequestUtils {
request.searchType = registeredData.searchType != null
? registeredData.searchType
: fileNo
? 1
: 2;
? 1
: 2;
request.patientID = registeredData.patientId ?? 0;
request.patientIdentificationID = request.nationalID = (registeredData.patientIdentificationId ?? 0);
request.dob = registeredData.dob;
@ -115,7 +133,8 @@ class RequestUtils {
log("nationIdText: ${nationIdText}");
} else {
if (fileNo) {
request.patientID = patientId ?? int.parse(nationIdText);
final numericNationId = nationIdText.replaceAll(RegExp(r'[^0-9]'), '');
request.patientID = patientId ?? (int.tryParse(numericNationId) ?? 0);
request.patientIdentificationID = request.nationalID = '0';
request.searchType = 2;
//TODO: Issue HEre is Not Login
@ -155,8 +174,17 @@ class RequestUtils {
request.patientMobileNumberOthers = mobileNumber;
request.mobileNo = mobileNumber;
} else {
request.patientMobileNumber = int.parse(mobileNumber);
request.mobileNo = '0$mobileNumber';
// Remove any non-numeric characters before parsing
final numericMobile = mobileNumber.replaceAll(RegExp(r'[^0-9]'), '');
final parsedMobile = int.tryParse(numericMobile);
if (parsedMobile != null) {
request.patientMobileNumber = parsedMobile;
request.mobileNo = '0$numericMobile';
} else {
// If parsing fails, use as string in Others field
request.patientMobileNumberOthers = mobileNumber;
request.mobileNo = mobileNumber;
}
}
}
request.projectOutSA = patientOutSA;
@ -240,16 +268,16 @@ class RequestUtils {
"Patientobject": {
"TempValue": true,
"PatientIdentificationType": (isDubai
? appState.getUserRegistrationPayload.patientIdentificationId?.toString().substring(0, 1)
: appState.getNHICUserData.idNumber!.substring(0, 1)) ==
"1"
? appState.getUserRegistrationPayload.patientIdentificationId?.toString().substring(0, 1)
: appState.getNHICUserData.idNumber!.substring(0, 1)) ==
"1"
? 1
: 2,
"PatientIdentificationNo":
isDubai ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : appState.getNHICUserData.idNumber.toString(),
isDubai ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : appState.getNHICUserData.idNumber.toString(),
"MobileNumber": appState.getUserRegistrationPayload.patientMobileNumber ?? 0,
"PatientOutSA": (appState.getUserRegistrationPayload.zipCode == CountryEnum.saudiArabia.countryCode ||
appState.getUserRegistrationPayload.zipCode == '+966')
appState.getUserRegistrationPayload.zipCode == '+966')
? 0
: 1,
"FirstNameN": isDubai ? "..." : appState.getNHICUserData.firstNameAr,
@ -266,31 +294,31 @@ class RequestUtils {
"DateofBirthN": date,
"EmailAddress": emailAddress,
"SourceType": (appState.getUserRegistrationPayload.zipCode == CountryEnum.saudiArabia.countryCode ||
appState.getUserRegistrationPayload.zipCode == '+966')
appState.getUserRegistrationPayload.zipCode == '+966')
? "1"
: "2",
"PreferredLanguage": appState.getLanguageCode() == "ar" ? (isDubai ? "1" : 1) : (isDubai ? "2" : 2),
"Marital": isDubai
? (maritalStatus == MaritalStatusTypeEnum.single
? '0'
: maritalStatus == MaritalStatusTypeEnum.married
? '1'
: '2')
? '0'
: maritalStatus == MaritalStatusTypeEnum.married
? '1'
: '2')
: (appState.getNHICUserData.maritalStatusCode == 'U'
? '0'
: appState.getNHICUserData.maritalStatusCode == 'M'
? '1'
: '2'),
? '0'
: appState.getNHICUserData.maritalStatusCode == 'M'
? '1'
: '2'),
},
"PatientIdentificationID":
isDubai ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : appState.getNHICUserData.idNumber.toString(),
isDubai ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : appState.getNHICUserData.idNumber.toString(),
"PatientMobileNumber": appState.getUserRegistrationPayload.patientMobileNumber.toString()[0] == '0'
? appState.getUserRegistrationPayload.patientMobileNumber
: '0${appState.getUserRegistrationPayload.patientMobileNumber}',
"DOB": dob,
"IsHijri": appState.getUserRegistrationPayload.isHijri,
"PatientOutSA": (appState.getUserRegistrationPayload.zipCode == CountryEnum.saudiArabia.countryCode ||
appState.getUserRegistrationPayload.zipCode == '+966')
appState.getUserRegistrationPayload.zipCode == '+966')
? 0
: 1,
"isDentalAllowedBackend": appState.getUserRegistrationPayload.isDentalAllowedBackend,
@ -314,7 +342,9 @@ class RequestUtils {
request.sharedPatientId = 0;
request.sharedPatientIdentificationId = nationalIDorFile;
} else if (loginType == 2) {
request.sharedPatientId = int.parse(nationalIDorFile);
// Remove any non-numeric characters before parsing
final numericId = nationalIDorFile.replaceAll(RegExp(r'[^0-9]'), '');
request.sharedPatientId = int.tryParse(numericId) ?? 0;
request.sharedPatientIdentificationId = '';
}
request.searchType = loginType;
@ -325,4 +355,4 @@ class RequestUtils {
request.isDentalAllowedBackend = false;
return request;
}
}
}

@ -851,7 +851,7 @@ class Utils {
/// Widget to build an image from network - automatically preserves colors in visual modes
static Widget buildImgWithNetwork({
required String url,
required String? url,
required Color iconColor,
bool isDisabled = false,
double? width,
@ -859,6 +859,13 @@ class Utils {
BoxFit fit = BoxFit.cover,
ImageErrorWidgetBuilder? errorBuilder,
}) {
if (url == null || url.isEmpty) {
return Utils.buildSvgWithAssets(
width: width ?? 24.w,
height: height ?? 24.h,
icon: AppAssets.noImageFound,
);
}
final iconH = height ?? 24.h;
final iconW = width ?? 24.w;
return PreserveImageColors(
@ -870,7 +877,7 @@ class Utils {
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 Utils.buildSvgWithAssets(width: iconW, height: iconH, icon: AppAssets.noImageFound);
},
),
);

@ -147,6 +147,9 @@ class AuthenticationRepoImp implements AuthenticationRepo {
bool isRegister = false,
bool isFormFamilyFile = false,
}) async {
int isOutKsa = (sendActivationCodeReq.zipCode == '966' || sendActivationCodeReq.zipCode == '+966') ? 0 : 1;
sendActivationCodeReq.patientOutSA = isOutKsa;
sendActivationCodeReq.isDentalAllowedBackend = false;

@ -3,6 +3,7 @@ import 'dart:developer';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:get_it/get_it.dart';
@ -42,6 +43,7 @@ import 'package:hmg_patient_app_new/services/dialog_service.dart';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
import 'package:hmg_patient_app_new/services/localauth_service.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/core/services/turnstile_service.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
@ -60,6 +62,7 @@ class AuthenticationViewModel extends ChangeNotifier {
final DialogService _dialogService;
final NavigationService _navigationService;
final LocalAuthService _localAuthService;
final TurnstileService _turnstileService;
AuthenticationViewModel({
required AppState appState,
@ -69,12 +72,14 @@ class AuthenticationViewModel extends ChangeNotifier {
required NavigationService navigationService,
required CacheService cacheService,
required LocalAuthService localAuthService,
required TurnstileService turnstileService,
}) : _navigationService = navigationService,
_dialogService = dialogService,
_errorHandlerService = errorHandlerService,
_appState = appState,
_authenticationRepo = authenticationRepo,
_localAuthService = localAuthService {}
_localAuthService = localAuthService,
_turnstileService = turnstileService {}
final TextEditingController nationalIdController = TextEditingController(),
phoneNumberController = TextEditingController(),
dobController = TextEditingController(),
@ -974,6 +979,17 @@ class AuthenticationViewModel extends ChangeNotifier {
// TODO: GET APP SMS SIGNATURE HERE
request.sMSSignature = await getSignature();
// if(!kDebugMode) {
// // Get Turnstile token if configured
// final turnstileToken = await _turnstileService.getTurnstileToken();
// if (turnstileToken != null) {
// request.turnstileToken = turnstileToken;
// } else {
// request.turnstileToken = "";
// }
// }
if (isNeedLoading) {
LoaderBottomSheet.showLoader();
}
@ -985,7 +1001,10 @@ class AuthenticationViewModel extends ChangeNotifier {
sendActivationCodeReq: request, isRegister: checkIsUserComingForRegister(request: payload), languageID: 'er', isFormFamilyFile: isFormFamilyFile);
resultEither.fold(
(failure) async => await _errorHandlerService.handleError(failure: failure),
(failure) async {
LoaderBottomSheet.hideLoader();
await _errorHandlerService.handleError(failure: failure);
},
(apiResponse) async {
if (isFormFamilyFile) {
if (apiResponse.data['PatientShareRequestID'] != null && apiResponse.data['PatientShareRequestID'] == 0 && apiResponse.messageStatus == 1) {

@ -30,6 +30,7 @@ class SendActivationRequest {
int? status;
int? familyRegionID;
bool? isPatientExcluded;
String? turnstileToken;
SendActivationRequest(
{this.patientMobileNumber,
@ -62,7 +63,8 @@ class SendActivationRequest {
this.responseID,
this.status,
this.familyRegionID,
this.isPatientExcluded});
this.isPatientExcluded,
this.turnstileToken});
SendActivationRequest.fromJson(Map<String, dynamic> json) {
patientMobileNumber = json['PatientMobileNumber'];
@ -96,6 +98,7 @@ class SendActivationRequest {
status = json['Status'];
familyRegionID = json['FamilyRegionID'];
isPatientExcluded = json['IsPatientExcluded'];
turnstileToken = json['TurnstileToken'];
}
Map<String, dynamic> toJson() {
@ -135,6 +138,9 @@ class SendActivationRequest {
data['Status'] = status;
data['FamilyRegionID'] = familyRegionID;
data['IsPatientExcluded'] = isPatientExcluded;
if (turnstileToken != null) {
data['TurnstileToken'] = turnstileToken;
}
return data;
}
}

@ -404,6 +404,7 @@ class BookAppointmentsViewModel extends ChangeNotifier {
isWaitingAppointmentSelected = false;
isPatientRescheduleAppointment = false;
bodyTypes = [maleLaserCategory, femaleLaserCategory];
selectedChiefComplaintID = 0;
// getLocation();
notifyListeners();
}
@ -420,14 +421,14 @@ class BookAppointmentsViewModel extends ChangeNotifier {
setIsNearestAppointmentSelected(bool isNearestAppointmentSelected) {
this.isNearestAppointmentSelected = isNearestAppointmentSelected;
if (isNearestAppointmentSelected) {
for (var group in doctorsListGrouped) {
group.sort((a, b) {
var aSlot = a.nearestFreeSlot;
var bSlot = b.nearestFreeSlot;
if (aSlot == null || bSlot == null) return 0;
return DateUtil.convertStringToDate(aSlot).compareTo(DateUtil.convertStringToDate(bSlot));
});
}
// for (var group in doctorsListGrouped) {
// group.sort((a, b) {
// var aSlot = a.nearestFreeSlot;
// var bSlot = b.nearestFreeSlot;
// if (aSlot == null || bSlot == null) return 0;
// return DateUtil.convertStringToDate(aSlot).compareTo(DateUtil.convertStringToDate(bSlot));
// });
// }
} else {
// for (var group in doctorsListGrouped) {
// group.sort((a, b) {
@ -675,7 +676,6 @@ class BookAppointmentsViewModel extends ChangeNotifier {
(failure) async {
isDoctorsListLoading = false;
if (onError != null) onError(LocaleKeys.noDoctorFound.tr());
notifyListeners();
},
(apiResponse) {
@ -690,7 +690,7 @@ class BookAppointmentsViewModel extends ChangeNotifier {
clearSearchFilters();
getFiltersFromDoctorList();
_groupDoctorsList();
setIsNearestAppointmentSelected(isNearest);
// setIsNearestAppointmentSelected(isNearest);
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);

@ -12,8 +12,22 @@ import 'package:hmg_patient_app_new/features/contact_us/models/resp_models/get_p
import 'package:hmg_patient_app_new/features/contact_us/models/resp_models/get_status_coc_response_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/services/error_handler_service.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:permission_handler/permission_handler.dart';
/// Class to store feedback attachment data
class FeedbackAttachment {
final String base64String;
final File file;
final String fileName;
FeedbackAttachment({
required this.base64String,
required this.file,
required this.fileName,
});
}
class ContactUsViewModel extends ChangeNotifier {
ContactUsRepo contactUsRepo;
ErrorHandlerService errorHandlerService;
@ -34,7 +48,7 @@ class ContactUsViewModel extends ChangeNotifier {
String? chatRequestID;
List<String> feedbackAttachmentList = [];
List<FeedbackAttachment> feedbackAttachmentList = [];
List<COCItem> cocItemsList = [];
bool isCOCItemsListLoading = false;
@ -167,13 +181,20 @@ class ContactUsViewModel extends ChangeNotifier {
notifyListeners();
}
addFeedbackAttachment(String attachmentPath) {
feedbackAttachmentList.add(attachmentPath);
addFeedbackAttachment(String base64String, File file) {
// Extract filename from file path
final fileName = file.path.split('/').last;
feedbackAttachmentList.add(FeedbackAttachment(
base64String: base64String,
file: file,
fileName: fileName,
));
notifyListeners();
}
removeFeedbackAttachment(String attachmentPath) {
feedbackAttachmentList.remove(attachmentPath);
removeFeedbackAttachment(FeedbackAttachment attachment) {
feedbackAttachmentList.remove(attachment);
notifyListeners();
}
@ -281,7 +302,7 @@ class ContactUsViewModel extends ChangeNotifier {
Future<void> insertCOCItem({required String subject, required String message, Function(dynamic)? onSuccess, Function(String)? onError}) async {
RequestInsertCOCItem requestInsertCOCItem = RequestInsertCOCItem();
requestInsertCOCItem.attachment = feedbackAttachmentList.isNotEmpty ? feedbackAttachmentList.first : "";
requestInsertCOCItem.attachment = feedbackAttachmentList.isNotEmpty ? feedbackAttachmentList.first.base64String : "";
requestInsertCOCItem.title = subject;
requestInsertCOCItem.details = message;
requestInsertCOCItem.cOCTypeName = selectedFeedbackType.id.toString();
@ -290,7 +311,7 @@ class ContactUsViewModel extends ChangeNotifier {
requestInsertCOCItem.isUserLoggedIn = true;
requestInsertCOCItem.projectID = 0;
requestInsertCOCItem.patientName = "${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}";
requestInsertCOCItem.fileName = "Image 1";
requestInsertCOCItem.fileName = feedbackAttachmentList.isNotEmpty ? feedbackAttachmentList.first.fileName : "";
requestInsertCOCItem.appVersion = ApiConsts.appVersionID;
requestInsertCOCItem.uILanguage = appState.isArabic() ? "ar" : "en";
requestInsertCOCItem.browserInfo = Platform.localHostname;
@ -308,7 +329,15 @@ class ContactUsViewModel extends ChangeNotifier {
final result = await contactUsRepo.insertCOCItem(requestInsertCOCItem: requestInsertCOCItem, patientSelectedAppointment: patientFeedbackSelectedAppointment);
result.fold(
(failure) async => await errorHandlerService.handleError(failure: failure),
(failure) async {
LoaderBottomSheet.hideLoader();
await errorHandlerService.handleError(failure: failure);
},
// (failure) async {
// if(onError != null) {
// onError(failure.toString());
// }
// },
(apiResponse) {
if (apiResponse.messageStatus == 2) {
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});

@ -33,14 +33,10 @@ class InsuranceViewModel extends ChangeNotifier {
InsuranceViewModel({required this.insuranceRepo, required this.errorHandlerService});
initInsuranceProvider() {
debugPrint("InsuranceViewModel: initInsuranceProvider called, isInsuranceDataToBeLoaded=$isInsuranceDataToBeLoaded");
if (isInsuranceDataToBeLoaded) {
patientInsuranceList.clear();
isInsuranceLoading = true;
debugPrint("InsuranceViewModel: Calling getPatientInsuranceDetails API");
getPatientInsuranceDetails();
} else {
debugPrint("InsuranceViewModel: Skipping API call, isInsuranceDataToBeLoaded is false");
}
patientInsuranceCardHistoryList.clear();
isInsuranceHistoryLoading = true;
@ -82,17 +78,14 @@ class InsuranceViewModel extends ChangeNotifier {
Future<void> getPatientInsuranceDetails({Function(dynamic)? onSuccess, Function(String)? onError}) async {
if (!isInsuranceDataToBeLoaded) {
debugPrint("InsuranceViewModel: getPatientInsuranceDetails - skipping, flag is false");
return;
}
debugPrint("InsuranceViewModel: getPatientInsuranceDetails - making API call");
final result = await insuranceRepo.getPatientInsuranceDetails();
result.fold(
// (failure) async => await errorHandlerService.handleError(failure: failure),
(failure) async {
debugPrint("InsuranceViewModel: API call failed - ${failure.toString()}");
isInsuranceLoading = false;
isInsuranceDataToBeLoaded = true;
isInsuranceExpired = false;
@ -102,11 +95,9 @@ class InsuranceViewModel extends ChangeNotifier {
},
(apiResponse) {
if (apiResponse.messageStatus == 2) {
debugPrint("InsuranceViewModel: API returned error status");
isInsuranceDataToBeLoaded = false;
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
} else if (apiResponse.messageStatus == 1) {
debugPrint("InsuranceViewModel: API success - received ${apiResponse.data?.length ?? 0} insurance records");
patientInsuranceList = apiResponse.data!;
isInsuranceLoading = false;
isInsuranceDataToBeLoaded = false;
@ -115,7 +106,6 @@ class InsuranceViewModel extends ChangeNotifier {
isInsuranceExpired = DateTime.now().isAfter(
DateUtil.convertStringToDate(patientInsuranceList.first.cardValidTo),
);
debugPrint("InsuranceViewModel: Insurance card expired: $isInsuranceExpired");
}
isInsuranceExpiryBannerShown = isInsuranceExpired;

@ -29,6 +29,7 @@ abstract class LabRepo {
Future<Either<Failure, GenericApiModel<LabOrdersResponseByAi>>> getPatientAiOverViewLabOrders({required int langId, required dynamic payload});
Future<Either<Failure, GenericApiModel<LabOrderResponseByAi>>> getPatientAiOverViewLabOrder(var payloa);
Map<String, List<LabResult>> groupLabResultsByPackageName(List<LabResult> labResults);
}
class LabRepoImp implements LabRepo {
@ -37,6 +38,55 @@ class LabRepoImp implements LabRepo {
LabRepoImp({required this.loggerService, required this.apiClient});
/// Groups a list of LabResult objects by their PackageName
/// Returns a Map where the key is the PackageName and the value is a List of LabResult objects
/// Both the keys and the lists are sorted alphabetically
///
/// Example usage:
/// ```dart
/// List<LabResult> labResults = [/* your lab results */];
/// Map<String, List<LabResult>> groupedResults = labRepo.groupLabResultsByPackageName(labResults);
///
/// // Access results by package name
/// groupedResults.forEach((packageName, results) {
/// print('Package: $packageName has ${results.length} results');
/// });
/// ```
Map<String, List<LabResult>> groupLabResultsByPackageName(List<LabResult> labResults) {
Map<String, List<LabResult>> groupedResults = {};
// Group results by package name
for (var labResult in labResults) {
String packageName = labResult.PackageName ?? 'Unknown Package';
if (groupedResults.containsKey(packageName)) {
// Package name exists, add to existing list
groupedResults[packageName]!.add(labResult);
} else {
// Package name doesn't exist, create new key with new list
groupedResults[packageName] = [labResult];
}
}
// Sort each list alphabetically by description
groupedResults.forEach((key, list) {
list.sort((a, b) {
String descA = (a.description ?? '').toLowerCase();
String descB = (b.description ?? '').toLowerCase();
return descA.compareTo(descB);
});
});
// Sort the map keys alphabetically and return a new sorted map
var sortedKeys = groupedResults.keys.toList()..sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
Map<String, List<LabResult>> sortedGroupedResults = {};
for (var key in sortedKeys) {
sortedGroupedResults[key] = groupedResults[key]!;
}
return sortedGroupedResults;
}
@override
Future<Either<Failure, GenericApiModel<List<PatientLabOrdersResponseModel>>>> getPatientLabOrders() async {
Map<String, dynamic> mapDevice = {};

@ -57,7 +57,11 @@ class LabViewModel extends ChangeNotifier {
PatientLabOrdersResponseModel? currentlySelectedPatientOrder;
List<LabResult> mainLabResultsByHospitals = [];
// Flat list for UI compatibility
List<LabResult> mainLabResultsByHospitalsList = [];
// Grouped map by package name for efficient lookups
Map<String, List<LabResult>> mainLabResultsByPackageName = {};
List<LabResult> mainLabResults = [];
List<DataPoint> mainGraphPoints = [];
@ -383,7 +387,8 @@ class LabViewModel extends ChangeNotifier {
Future<void> getPatientLabResultByHospital(PatientLabOrdersResponseModel laborder) async {
isLabResultByHospitalLoading = true;
notifyListeners();
mainLabResultsByHospitals.clear();
mainLabResultsByHospitalsList.clear();
mainLabResultsByPackageName.clear();
final result = await labRepo.getPatientLabResultsByHospitals(laborder, Utils.isVidaPlusProject(int.parse(laborder.projectID ?? "0")));
@ -397,7 +402,9 @@ class LabViewModel extends ChangeNotifier {
isLabResultByHospitalLoading = false;
if (apiResponse.messageStatus == 2) {
} else if (apiResponse.messageStatus == 1) {
mainLabResultsByHospitals = apiResponse.data ?? [];
mainLabResultsByHospitalsList = apiResponse.data ?? [];
// Group the results by package name for efficient lookups
mainLabResultsByPackageName = labRepo.groupLabResultsByPackageName(mainLabResultsByHospitalsList);
notifyListeners();
}
},
@ -842,6 +849,25 @@ class LabViewModel extends ChangeNotifier {
notifyListeners();
}
// /// Finds a matching LabResult from the grouped map by package name
// /// Searches through all packages to find a result matching the test code or description
// LabResult? _findMatchingLabResult(TestDetails testDetail) {
// // Search through all packages in the grouped map
// for (var packageResults in mainLabResultsByPackageName.values) {
// try {
// var matchingResult = packageResults.firstWhere(
// (labResult) => labResult.testCode == testDetail.testCode || labResult.description == testDetail.description,
// );
// return matchingResult;
// } catch (e) {
// // Continue searching in other packages
// continue;
// }
// }
// // Return null if no match found
// return LabResult();
// }
Future<void> getAiOverviewLabOrders({required int langId, required PatientLabOrdersResponseModel labOrder, required String loadingText}) async {
isLabAIAnalysisNeedsToBeShown = true;
notifyListeners();
@ -858,7 +884,7 @@ class LabViewModel extends ChangeNotifier {
if (labOrder.testDetails != null && labOrder.testDetails!.isNotEmpty) {
for (var testDetail in labOrder.testDetails!) {
LabResult? matchingResult =
mainLabResultsByHospitals.firstWhere((labResult) => labResult.testCode == testDetail.testCode || labResult.description == testDetail.description, orElse: () => LabResult());
mainLabResultsByHospitalsList.firstWhere((labResult) => labResult.testCode == testDetail.testCode || labResult.description == testDetail.description, orElse: () => LabResult());
testResults.add({
"test_name": testDetail.description ?? "",
"result_value": matchingResult.resultValue ?? "",

@ -28,6 +28,7 @@ class LabResult {
String? packageShortDescription;
int? testId;
String? testShortDescription;
String? PackageName;
LabResult(
{this.description,
@ -56,6 +57,7 @@ class LabResult {
this.referenceLow,
this.testId,
this.testShortDescription,
this.PackageName
});
LabResult.fromJson(Map<String, dynamic> json) {
@ -88,6 +90,7 @@ class LabResult {
resultTypeID = json['ResultTypeID'];
testId = json['TestID'];
testShortDescription = json['TestShortDescription'];
PackageName = json['PackageName'];
}
Map<String, dynamic> toJson() {
@ -118,6 +121,7 @@ class LabResult {
data['ReferenceLow'] = this.referenceLow;
data['TestID'] = this.testId;
data['TestShortDescription'] = this.testShortDescription;
data['PackageName'] = this.PackageName;
return data;
}

@ -29,7 +29,11 @@ class MedicalFileViewModel extends ChangeNotifier {
bool isPatientSickLeavePDFLoading = false;
bool isPatientMedicalReportsListLoading = false;
bool isPatientAllergiesListLoading = false;
bool _isFamilyFilesLoading = false;
bool get isFamilyFilesLoading => _isFamilyFilesLoading;
set setIsFamilyFilesLoading(bool value) => _isFamilyFilesLoading = value;
MedicalFileRepo medicalFileRepo;
ErrorHandlerService errorHandlerService;
@ -42,7 +46,7 @@ class MedicalFileViewModel extends ChangeNotifier {
bool isSickLeavesSortByClinic = true;
// bool isSickLeavesDataNeedsReloading = true;
bool isSickLeavesDataNeedsReloading = true;
List<GetAllergiesResponseModel> patientAllergiesList = [];
@ -165,15 +169,15 @@ class MedicalFileViewModel extends ChangeNotifier {
}
setIsPatientSickLeaveListLoading(bool val) {
// if (val && isSickLeavesDataNeedsReloading) {
patientSickLeaveList.clear();
patientSickLeavesByClinic.clear();
if (val && isSickLeavesDataNeedsReloading) {
patientSickLeaveList.clear();
patientSickLeavesByClinic.clear();
patientSickLeavesByHospital.clear();
patientSickLeavesViewList.clear();
patientSickLeavePDFBase64 = "";
isSickLeavesSortByClinic = true;
isPatientSickLeaveListLoading = val;
// }
}
notifyListeners();
}
@ -282,9 +286,9 @@ class MedicalFileViewModel extends ChangeNotifier {
}
Future<void> getPatientSickLeaveList({Function(dynamic)? onSuccess, Function(String)? onError}) async {
// if (!isSickLeavesDataNeedsReloading) {
// return;
// }
if (!isSickLeavesDataNeedsReloading) {
return;
}
patientSickLeaveList.clear();
notifyListeners();
@ -300,6 +304,7 @@ class MedicalFileViewModel extends ChangeNotifier {
// ),
(failure) async {
isPatientSickLeaveListLoading = false;
isSickLeavesDataNeedsReloading = true;
notifyListeners();
},
(apiResponse) {
@ -330,7 +335,7 @@ class MedicalFileViewModel extends ChangeNotifier {
}
}
patientSickLeavesViewList = patientSickLeavesByClinic;
// isSickLeavesDataNeedsReloading = false;
isSickLeavesDataNeedsReloading = false;
notifyListeners();
if (onSuccess != null) {
@ -430,21 +435,42 @@ class MedicalFileViewModel extends ChangeNotifier {
}
Future<void> getFamilyFiles({int? status, Function(dynamic)? onSuccess, Function(String)? onError}) async {
setIsFamilyFilesLoading = true;
final shouldShowLoader = patientFamilyFiles.isEmpty;
if (shouldShowLoader) {
LoaderBottomSheet.showLoader();
}
final result = await medicalFileRepo.getPatientFamilyFiles(status, _appState.getSuperUserID != null ? _appState.getSuperUserID! : _appState.getAuthenticatedUser()!.patientId!);
result.fold(
(failure) async => await errorHandlerService.handleError(
failure: failure,
onOkPressed: () {
onError!(failure.message);
},
),
(failure) async {
setIsFamilyFilesLoading = false;
await errorHandlerService.handleError(
failure: failure,
onOkPressed: () {
if (shouldShowLoader) {
LoaderBottomSheet.hideLoader();
}
onError!(failure.message);
},
);
},
(apiResponse) {
if (apiResponse.messageStatus == 2) {
setIsFamilyFilesLoading = false;
// _dialogService.showErrorBottomSheet(message: apiResponse.errorMessage!, onOkPressed: () {});
if (shouldShowLoader) {
LoaderBottomSheet.hideLoader();
}
} else if (apiResponse.messageStatus == 1) {
if (apiResponse.data != null) {
// Add current user as the first active family file
final currentUser = _appState.getAuthenticatedUser()!;
final currentUserFamilyFile = FamilyFileResponseModelLists(
patientId: currentUser.patientId,
@ -524,6 +550,12 @@ class MedicalFileViewModel extends ChangeNotifier {
pendingFamilyFiles = newPendingFamilyFiles;
}
// Clear loading state on success
setIsFamilyFilesLoading = false;
if (shouldShowLoader) {
LoaderBottomSheet.hideLoader();
}
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
@ -661,13 +693,10 @@ class MedicalFileViewModel extends ChangeNotifier {
}
Future<void> handleFamilyFileRequestOTPVerification() async {
LoaderBottomSheet.showLoader();
if (!_appState.getIsChildLoggedIn) {
await getFamilyFiles(status: 0);
await getAllPendingRecordsByResponseId();
}
LoaderBottomSheet.hideLoader();
}
Future<void> removeFileFromFamilyMembers({int? id}) async {

@ -25,7 +25,7 @@ import 'models/resp_models/appointment_details_resp_model.dart';
abstract class MyAppointmentsRepo {
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientAppointments(
{required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false});
{required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false, bool isForTimeLine = false});
Future<Either<Failure, GenericApiModel<PatientAppointmentShareResponseModel>>> getPatientShareAppointment(
{required int projectID, required int clinicID, required String appointmentNo, required bool isLiveCareAppointment});
@ -53,7 +53,7 @@ abstract class MyAppointmentsRepo {
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientAppointmentsForTimeLine();
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientDoctorsList();
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientDoctorsList({bool isTop8 = false});
Future<Either<Failure, GenericApiModel<List<GetFavoriteDoctorsListModel>>>> getFavouriteDoctorsList();
@ -105,14 +105,15 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo {
@override
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientAppointments(
{required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false}) async {
{required bool isActiveAppointment, required bool isArrivedAppointments, bool isForEyeMeasurement = false, bool isForTimeLine = false}) async {
Map<String, dynamic> mapDevice = {
"IsActiveAppointment": isActiveAppointment,
"IsComingFromCOC": false,
"isForUpcomming": false,
"IsForMedicalReport": false,
"IsForArrived": isArrivedAppointments,
"IsIrisPrescription": isForEyeMeasurement
"IsIrisPrescription": isForEyeMeasurement,
"IsForTimeLine": isForTimeLine
};
try {
@ -172,9 +173,9 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo {
try {
final list = response['OnlineCheckInAppointments'];
patientShareObj = (list == null || list.isEmpty) ? PatientAppointmentShareResponseModel() : PatientAppointmentShareResponseModel.fromJson(list[0]);
patientShareObj.isCash = response["IsCash"];
patientShareObj.isEligible = response["IsEligible"];
patientShareObj.isInsured = response["IsInsured"];
patientShareObj.isCash = isLiveCareAppointment ? list[0]["IsCash"] : response["IsCash"];
patientShareObj.isEligible = isLiveCareAppointment ? list[0]["IsEligible"] : response["IsEligible"];
patientShareObj.isInsured = isLiveCareAppointment ? list[0]["IsInsured"] : response["IsInsured"];
// if (list == null || list.isEmpty) {
// throw Exception("patient share list is empty");
@ -514,11 +515,12 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo {
}
@override
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientDoctorsList() async {
Future<Either<Failure, GenericApiModel<List<PatientAppointmentHistoryResponseModel>>>> getPatientDoctorsList({bool isTop8 = false}) async {
Map<String, dynamic> mapDevice = {
"Top": 0,
"beforeDays": 0,
"exludType": 4,
"IsTop8": isTop8,
};
try {

@ -54,6 +54,9 @@ class MyAppointmentsViewModel extends ChangeNotifier {
bool isFavouriteDoctorsDataFetched = false;
bool isAppointmentDataToBeLoaded = true;
bool isMyDoctorsDataToBeLoaded = true;
bool isArrivedAppointmentDataLoaded = false;
bool isFullArrivedAppointmentsLoaded = false;
bool isEyeMeasurementsAppointmentsLoading = false;
@ -75,7 +78,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
List<PatientAppointmentHistoryResponseModel> patientUpcomingAppointmentsHistoryList = [];
List<PatientAppointmentHistoryResponseModel> patientArrivedAppointmentsHistoryList = [];
List<PatientAppointmentHistoryResponseModel> patientTimelineAppointmentsList = [];
// List<PatientAppointmentHistoryResponseModel> patientAllArrivedAppointmentsHistoryList = [];
List<PatientAppointmentHistoryResponseModel> patientMyDoctorsList = [];
@ -171,11 +174,12 @@ class MyAppointmentsViewModel extends ChangeNotifier {
patientAppointmentsHistoryList.clear();
patientUpcomingAppointmentsHistoryList.clear();
patientArrivedAppointmentsHistoryList.clear();
patientTimelineAppointmentsList.clear();
// patientAllArrivedAppointmentsHistoryList.clear();
patientEyeMeasurementsAppointmentsHistoryList.clear();
isMyAppointmentsLoading = true;
isTimeLineAppointmentsLoading = true;
patientMyDoctorsList.clear();
isFullArrivedAppointmentsLoaded = false;
}
isTamaraDetailsLoading = true;
isAppointmentPatientShareLoading = true;
@ -210,9 +214,15 @@ class MyAppointmentsViewModel extends ChangeNotifier {
notifyListeners();
}
setIsMyDoctorsDataToBeLoaded(bool val) {
isMyDoctorsDataToBeLoaded = val;
notifyListeners();
}
setIsAppointmentDataToBeLoaded(bool val) {
if (val) {
isMyAppointmentsLoading = true;
isMyDoctorsDataToBeLoaded = true;
}
isAppointmentDataToBeLoaded = val;
notifyListeners();
@ -259,7 +269,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
);
}
Future<void> getPatientAppointments(bool isActiveAppointment, bool isArrivedAppointments, {Function(dynamic)? onSuccess, Function(String)? onError}) async {
Future<void> getPatientAppointments(bool isActiveAppointment, bool isArrivedAppointments, {bool isForTimeLine = false, Function(dynamic)? onSuccess, Function(String)? onError}) async {
if (patientArrivedAppointmentsHistoryList.isNotEmpty) {
isPatientHasQueueAppointment = false;
notifyListeners();
@ -268,15 +278,19 @@ class MyAppointmentsViewModel extends ChangeNotifier {
getPatientAppointmentQueueDetails();
}
}
if (!isAppointmentDataToBeLoaded) return;
debugPrint('getPatientAppointments CALLED!!!');
// Skip API call if full data is already loaded and we're not requesting timeline view
if (!isForTimeLine && isFullArrivedAppointmentsLoaded && !isAppointmentDataToBeLoaded) {
return;
}
if (!isAppointmentDataToBeLoaded) return;
patientAppointmentsByClinic.clear();
patientAppointmentsByHospital.clear();
patientAppointmentsViewList.clear();
patientTimelineAppointmentsList.clear();
// patientAllArrivedAppointmentsHistoryList.clear();
filteredAppointmentList.clear();
patientAppointmentsHistoryList.clear();
patientUpcomingAppointmentsHistoryList.clear();
@ -285,7 +299,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
final results = await Future.wait([
myAppointmentsRepo.getPatientAppointments(isActiveAppointment: isActiveAppointment, isArrivedAppointments: isArrivedAppointments),
myAppointmentsRepo.getPatientAppointments(isActiveAppointment: false, isArrivedAppointments: true),
myAppointmentsRepo.getPatientAppointments(isActiveAppointment: false, isArrivedAppointments: true, isForTimeLine: isForTimeLine),
]);
results[0].fold(
@ -314,6 +328,11 @@ class MyAppointmentsViewModel extends ChangeNotifier {
patientArrivedAppointmentsHistoryList = apiResponse.data!;
isMyAppointmentsLoading = false;
isAppointmentDataToBeLoaded = false;
if (!isForTimeLine) {
// patientAllArrivedAppointmentsHistoryList = apiResponse.data!;
isArrivedAppointmentDataLoaded = true;
isFullArrivedAppointmentsLoaded = true;
}
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
@ -340,13 +359,73 @@ class MyAppointmentsViewModel extends ChangeNotifier {
}
// }
debugPrint('Upcoming Appointments: ${patientUpcomingAppointmentsHistoryList.length}');
debugPrint('Arrived Appointments: ${patientArrivedAppointmentsHistoryList.length}');
debugPrint('All Appointments: ${patientAppointmentsHistoryList.length}');
getFiltersForSelectedAppointmentList(filteredAppointmentList);
notifyListeners();
}
changeTabToArrived() {
if (isFullArrivedAppointmentsLoaded && patientUpcomingAppointmentsHistoryList.isEmpty && patientArrivedAppointmentsHistoryList.isNotEmpty && selectedTabIndex == 0) {
selectedTabIndex = 1;
updateListWRTTab(1);
notifyListeners();
}
}
/// Loads full arrived appointments list if not already loaded
/// This is called when navigating to My Appointments page from landing page
Future<void> loadFullArrivedAppointmentsIfNeeded({Function(dynamic)? onSuccess, Function(String)? onError}) async {
// Skip if already loaded
if (isFullArrivedAppointmentsLoaded) {
// Auto-switch to Arrived tab if no upcoming appointments (even when data is already loaded)
if (patientUpcomingAppointmentsHistoryList.isEmpty && patientArrivedAppointmentsHistoryList.isNotEmpty && selectedTabIndex == 0) {
selectedTabIndex = 1;
updateListWRTTab(1);
notifyListeners();
}
return;
}
setIsAppointmentsHistoryLoading(true);
// Fetch full arrived appointments (isForTimeLine = false)
final result = await myAppointmentsRepo.getPatientAppointments(isActiveAppointment: false, isArrivedAppointments: true, isForTimeLine: false);
result.fold(
(failure) async => await errorHandlerService.handleError(failure: failure),
(apiResponse) {
if (apiResponse.messageStatus == 2) {
// Handle error
} else if (apiResponse.messageStatus == 1) {
patientArrivedAppointmentsHistoryList = apiResponse.data!;
// patientAllArrivedAppointmentsHistoryList = apiResponse.data!;
isArrivedAppointmentDataLoaded = true;
isFullArrivedAppointmentsLoaded = true;
// Rebuild the combined list
patientAppointmentsHistoryList.clear();
patientAppointmentsHistoryList.addAll(patientUpcomingAppointmentsHistoryList);
patientAppointmentsHistoryList.addAll(patientArrivedAppointmentsHistoryList);
isMyAppointmentsLoading = false;
// Auto-switch to Arrived tab if no upcoming appointments
if (patientUpcomingAppointmentsHistoryList.isEmpty && patientArrivedAppointmentsHistoryList.isNotEmpty && selectedTabIndex == 0) {
selectedTabIndex = 1;
updateListWRTTab(1);
} else {
// Update filtered list based on current tab
updateListWRTTab(selectedTabIndex);
}
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
}
},
);
}
void getFiltersForSelectedAppointmentList(List<PatientAppointmentHistoryResponseModel> filteredAppointmentList) {
availableFilters.clear();
if (filteredAppointmentList.isEmpty == true) return;
@ -355,9 +434,9 @@ class MyAppointmentsViewModel extends ChangeNotifier {
availableFilters.add(AppointmentListingFilters.LIVECARE);
}
if (filteredAppointmentList.any((element) => element.isLiveCareAppointment == false)) {
availableFilters.add(AppointmentListingFilters.WALKIN);
}
// if (filteredAppointmentList.any((element) => element.isLiveCareAppointment == false)) {
// availableFilters.add(AppointmentListingFilters.WALKIN);
// }
if (filteredAppointmentList.any((element) => AppointmentType.isArrived(element) == true)) {
availableFilters.add(AppointmentListingFilters.ARRIVED);
@ -673,13 +752,13 @@ class MyAppointmentsViewModel extends ChangeNotifier {
);
}
Future<void> getPatientMyDoctors({Function(dynamic)? onSuccess, Function(String)? onError}) async {
// if (!isAppointmentDataToBeLoaded) return;
Future<void> getPatientMyDoctors({bool isTop8 = false, Function(dynamic)? onSuccess, Function(String)? onError}) async {
if (!isMyDoctorsDataToBeLoaded) return;
isPatientMyDoctorsLoading = true;
patientMyDoctorsList.clear();
notifyListeners();
final result = await myAppointmentsRepo.getPatientDoctorsList();
final result = await myAppointmentsRepo.getPatientDoctorsList(isTop8: isTop8);
result.fold(
// (failure) async => await errorHandlerService.handleError(failure: failure),
@ -692,6 +771,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
} else if (apiResponse.messageStatus == 1) {
patientMyDoctorsList = apiResponse.data!;
isPatientMyDoctorsLoading = false;
isMyDoctorsDataToBeLoaded = false;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
@ -793,15 +873,15 @@ class MyAppointmentsViewModel extends ChangeNotifier {
selectedFilter = [];
// if(previouslySelectedTab == selectedTabIndex ) return;
switch (index) {
// case 0:
// filteredAppointmentList.clear();
// filteredAppointmentList.addAll(patientAppointmentsHistoryList);
// break;
case 0:
filteredAppointmentList.clear();
filteredAppointmentList.addAll(patientAppointmentsHistoryList);
break;
case 1:
filteredAppointmentList.clear();
filteredAppointmentList.addAll(patientUpcomingAppointmentsHistoryList);
break;
case 2:
case 1:
filteredAppointmentList.clear();
filteredAppointmentList.addAll(patientArrivedAppointmentsHistoryList);
break;
@ -827,11 +907,12 @@ class MyAppointmentsViewModel extends ChangeNotifier {
this.end = end;
isDateFilterSelected = true;
List<PatientAppointmentHistoryResponseModel> sourceList = [];
if (selectedTabIndex == 0) {
sourceList = patientAppointmentsHistoryList;
} else if (selectedTabIndex == 1) {
// if (selectedTabIndex == 0) {
// sourceList = patientAppointmentsHistoryList;
// } else
if (selectedTabIndex == 0) {
sourceList = patientUpcomingAppointmentsHistoryList;
} else if (selectedTabIndex == 2) {
} else if (selectedTabIndex == 1) {
sourceList = patientArrivedAppointmentsHistoryList;
}
// if (isDateFilterSelected) sourceList = filteredAppointmentList;
@ -1291,7 +1372,6 @@ class MyAppointmentsViewModel extends ChangeNotifier {
);
} catch (e) {
// Ignore errors if reminder doesn't exist
debugPrint("No existing ${type.displayName} to remove");
}
}
}

@ -11,17 +11,29 @@ import 'package:flutter_paytabs_bridge/PaymentSdkTransactionType.dart';
import 'package:flutter_paytabs_bridge/flutter_paytabs_bridge.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/cache_consts.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/core/paytabs_config.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/paytabs/models/paytabs_transaction_response_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/payfort_request_insert_model.dart';
import 'package:hmg_patient_app_new/features/payfort/models/payfort_response_insert_model.dart';
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';
class PayTabsViewModel extends ChangeNotifier {
late PaymentSdkConfigurationDetails paymentConfiguration;
final AppState appState;
final PayfortRepo payfortRepo;
final ErrorHandlerService errorHandlerService;
late PaytabsTransactionResponseModel paytabsTransactionResponseModel;
PayTabsViewModel({
required this.appState,
required this.payfortRepo,
required this.errorHandlerService,
});
setPaymentConfiguration(String paymentDescription, num amount, {bool isAddCard = false}) {
@ -152,6 +164,42 @@ class PayTabsViewModel extends ChangeNotifier {
notifyListeners();
}
Future<void> paytabsRequestInsert({required PayfortRequestInsertModel payfortRequestInsertModel, Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await payfortRepo.payfortRequestInsert(payfortRequestInsertModel: payfortRequestInsertModel);
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) {
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
}
},
);
}
Future<void> paytabsResponseInsert({required PayfortResponseInsertModel payfortResponseInsertModel, Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await payfortRepo.payfortResponseInsert(payfortResponseInsertModel: payfortResponseInsertModel);
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) {
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);
}
}
},
);
}
startApplePayPayment({Function(PaytabsTransactionResponseModel)? onSuccess, Function(String)? onError}) async {
bool _callbackFired = false; // prevent duplicate callback invocations
@ -189,41 +237,132 @@ class PayTabsViewModel extends ChangeNotifier {
});
}
startCardPayment({Function(PaytabsTransactionResponseModel)? onSuccess, Function(String)? onError}) {
startCardPayment({
required String paymentOption,
required String liveServiceID,
required String isScheduleLiveCare,
required String appointmentDate,
required String appointmentNo,
required String doctorID,
required String transactionID,
required String amount,
required String fileNumber,
required String orderDescription,
required String serviceID,
required String projectID,
Function(PaytabsTransactionResponseModel)? onSuccess,
Function(String)? onError,
}) async {
bool _callbackFired = false; // prevent duplicate callback invocations
FlutterPaytabsBridge.startCardPayment(paymentConfiguration, (event) {
if (_callbackFired) return; // ignore any subsequent callback calls
try {
LoaderBottomSheet.showLoader();
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);
// Create PayfortRequestInsertModel (same as Payfort implementation)
PayfortRequestInsertModel payfortRequestInsertModel = PayfortRequestInsertModel();
payfortRequestInsertModel.commandType = "SALE";
payfortRequestInsertModel.amount = num.parse(amount);
payfortRequestInsertModel.customerEmail = getIt.get<AppState>().getAuthenticatedUser()!.emailAddress;
payfortRequestInsertModel.orderDescription = orderDescription;
payfortRequestInsertModel.serviceID = serviceID;
payfortRequestInsertModel.paymentOption = paymentOption;
payfortRequestInsertModel.clientRequestID = transactionID;
payfortRequestInsertModel.projectID = projectID;
payfortRequestInsertModel.fileNumber = getIt.get<AppState>().getAuthenticatedUser()!.patientId.toString();
payfortRequestInsertModel.customerName = "${getIt.get<AppState>().getAuthenticatedUser()!.firstName} ${getIt.get<AppState>().getAuthenticatedUser()!.lastName}";
payfortRequestInsertModel.currency = getIt.get<AppState>().getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED";
payfortRequestInsertModel.liveServiceID = liveServiceID;
payfortRequestInsertModel.latitude = getIt.get<AppState>().userLat.toString();
payfortRequestInsertModel.longitude = getIt.get<AppState>().userLong.toString();
payfortRequestInsertModel.merchantReference = transactionID;
payfortRequestInsertModel.deviceToken = await Utils.getStringFromPrefs(CacheConst.pushToken);
payfortRequestInsertModel.language = getIt.get<AppState>().isArabic() ? "ar" : "en";
payfortRequestInsertModel.integrationId = "2"; // PayTabs integration ID
payfortRequestInsertModel.isSchedule = isScheduleLiveCare;
payfortRequestInsertModel.appointmentDate = appointmentDate;
payfortRequestInsertModel.appointmentNo = appointmentNo;
payfortRequestInsertModel.doctorID = doctorID;
// Call request insert API before payment
await paytabsRequestInsert(payfortRequestInsertModel: payfortRequestInsertModel);
LoaderBottomSheet.hideLoader();
// Start PayTabs payment
FlutterPaytabsBridge.startCardPayment(paymentConfiguration, (event) async {
if (_callbackFired) return; // ignore any subsequent callback calls
final transactionDetails = event["data"] as Map<Object?, Object?>?;
if (event["status"] == "success") {
_callbackFired = true;
LoaderBottomSheet.showLoader();
// Cast Map<Object?, Object?> to Map<String, dynamic>
final data = Map<String, dynamic>.from(event["data"] as Map);
paytabsTransactionResponseModel = PaytabsTransactionResponseModel.fromJson(data);
// Create PayfortResponseInsertModel (same as Payfort implementation)
PayfortResponseInsertModel payfortResponseInsertModel = PayfortResponseInsertModel();
payfortResponseInsertModel.fortId = paytabsTransactionResponseModel.transactionReference ?? "";
payfortResponseInsertModel.commandType = paytabsTransactionResponseModel.transactionType ?? "SALE";
// payfortResponseInsertModel.amount = paytabsTransactionResponseModel.tranTotal ?? 0;
payfortResponseInsertModel.amount = num.parse(amount);
payfortResponseInsertModel.sadadOLP = "";
payfortResponseInsertModel.paymentOption = paytabsTransactionResponseModel.paymentInfo?.paymentMethod ?? "";
payfortResponseInsertModel.customerIP = "";
payfortResponseInsertModel.eCI = "";
payfortResponseInsertModel.responseMessage = paytabsTransactionResponseModel.payResponseReturn ?? "";
payfortResponseInsertModel.cardNumber = paytabsTransactionResponseModel.paymentInfo!.paymentDescription;
payfortResponseInsertModel.cardBin = "";
payfortResponseInsertModel.installmentsNumber = 0;
payfortResponseInsertModel.status = paytabsTransactionResponseModel.isSuccess! ? "success" : "failed";
payfortResponseInsertModel.merchantRef = transactionID;
payfortResponseInsertModel.isRefund = false;
payfortResponseInsertModel.remmeberMe = true;
payfortResponseInsertModel.reconciliationReference = "";
// Call response insert API after payment
await paytabsResponseInsert(payfortResponseInsertModel: payfortResponseInsertModel);
LoaderBottomSheet.hideLoader();
if (paytabsTransactionResponseModel.isSuccess!) {
if (onSuccess != null) {
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";
if (onError != null) {
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");
if (onError != null) {
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");
}
} 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");
});
} catch (error) {
debugPrint("PayTabs Payment Error: $error");
LoaderBottomSheet.hideLoader();
if (onError != null) {
onError("Payment initialization failed: $error");
}
});
}
}
startAlternatePayment({Function(PaytabsTransactionResponseModel)? onSuccess, Function(String)? onError}) {

@ -64,7 +64,7 @@ class PrescriptionsViewModel extends ChangeNotifier {
PrescriptionsViewModel({required this.prescriptionsRepo, required this.errorHandlerService, required this.navServices});
initPrescriptionsViewModel() {
// if (isPrescriptionsDataNeedsReloading) {
if (isPrescriptionsDataNeedsReloading) {
patientPrescriptionOrders.clear();
patientPrescriptionOrdersByClinic.clear();
patientPrescriptionOrdersByHospital.clear();
@ -72,7 +72,7 @@ class PrescriptionsViewModel extends ChangeNotifier {
prescriptionsOrderList.clear();
isPrescriptionsOrdersLoading = true;
getPatientPrescriptionOrders();
// }
}
isSortByClinic = true;
isPrescriptionsDeliveryOrdersLoading = true;
notifyListeners();
@ -138,9 +138,9 @@ class PrescriptionsViewModel extends ChangeNotifier {
}
Future<void> getPatientPrescriptionOrders({Function(dynamic)? onSuccess, Function(String)? onError}) async {
// if (!isPrescriptionsDataNeedsReloading) {
// return;
// }
if (!isPrescriptionsDataNeedsReloading) {
return;
}
final result = await prescriptionsRepo.getPatientPrescriptionOrders(patientId: "1231755");
@ -191,7 +191,9 @@ class PrescriptionsViewModel extends ChangeNotifier {
result.fold(
// (failure) async => await errorHandlerService.handleError(failure: failure),
(failure) async {
onError!(failure.message);
if (onError != null) {
onError(failure.message);
}
},
(apiResponse) async {
if (apiResponse.messageStatus == 2) {
@ -235,11 +237,16 @@ class PrescriptionsViewModel extends ChangeNotifier {
result.fold(
(failure) async {
onError!(failure.message);
if (onError != null) {
onError(failure.message);
}
},
(apiResponse) {
if (apiResponse.messageStatus == 2) {
onError!(apiResponse.errorMessage!);
if (onError != null) {
onError(apiResponse.errorMessage ?? "An error occurred while fetching the prescription instructions PDF.");
}
// onError!(apiResponse.errorMessage!);
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
} else if (apiResponse.messageStatus == 1) {
prescriptionInstructionsPDFLink = apiResponse.data;

@ -70,9 +70,7 @@ class ProfilePictureViewModel extends ChangeNotifier {
try {
_cachedImageBytes = base64Decode(imageData);
_cachedImageDataHash = '${imageData.length}_${imageData.hashCode}';
print('✅ Cached existing profile image');
} catch (e) {
print('❌ Error caching existing image: $e');
_cachedImageBytes = null;
_cachedImageDataHash = null;
}
@ -85,7 +83,6 @@ class ProfilePictureViewModel extends ChangeNotifier {
final currentPatientId = _appState.getAuthenticatedUser()?.patientId;
if (currentPatientId != null && currentPatientId != _currentPatientId) {
print('🔄 User switched detected: $_currentPatientId -> $currentPatientId');
_handleUserSwitch(currentPatientId);
return true;
}
@ -97,8 +94,6 @@ class ProfilePictureViewModel extends ChangeNotifier {
final oldPatientId = _currentPatientId;
_currentPatientId = newPatientId;
print('🧹 Clearing cache for old user: $oldPatientId');
// Clear AppState cache
_appState.clearProfileImageCache();
@ -113,17 +108,14 @@ class ProfilePictureViewModel extends ChangeNotifier {
notifyListeners();
// Load new user's profile image
print('📥 Loading profile image for new user: $newPatientId');
_profileSettingsViewModel.getProfileImage(
patientID: newPatientId,
forceRefresh: true,
onSuccess: (data) {
print('✅ Profile image loaded successfully for user: $newPatientId');
_tryCacheExistingImage();
notifyListeners();
},
onError: (error) {
print('❌ Error loading profile image: $error');
notifyListeners();
},
);
@ -133,22 +125,18 @@ class ProfilePictureViewModel extends ChangeNotifier {
void loadProfileImage({bool forceRefresh = false}) {
// Check if profile image is already loaded in AppState (skip if forcing refresh)
if (!forceRefresh && _appState.getProfileImageData != null && _appState.getProfileImageData!.isNotEmpty) {
print('✅ Profile image already cached in AppState');
return;
}
final patientID = _appState.getAuthenticatedUser()?.patientId;
if (patientID == null) {
print('⚠️ Cannot load profile image - no authenticated user');
return;
}
print('📥 Loading profile image for patient: $patientID (forceRefresh: $forceRefresh)');
_profileSettingsViewModel.getProfileImage(
patientID: patientID,
forceRefresh: forceRefresh,
onSuccess: (data) {
print('✅ Profile image loaded successfully');
_tryCacheExistingImage();
notifyListeners();
},
@ -188,10 +176,6 @@ class ProfilePictureViewModel extends ChangeNotifier {
false, // Don't show files option, only camera and gallery
(base64String, file) async {
try {
print('=== Starting image processing ===');
print('File path: ${file.path}');
print('File exists: ${await file.exists()}');
print('Original file size: ${await file.length() / 1024} KB');
// Compress and resize the image
print('Calling compressAndResizeImage...');
@ -266,14 +250,10 @@ class ProfilePictureViewModel extends ChangeNotifier {
onError('No authenticated user found');
return;
}
print('📤 Uploading profile image for patient: $patientID');
_profileSettingsViewModel.uploadProfileImage(
patientID: patientID,
imageData: base64String,
onSuccess: (data) async {
print('✅ Profile image uploaded successfully');
// Clear old cache first to ensure fresh data
_cachedImageBytes = null;
_cachedImageDataHash = null;
@ -287,9 +267,13 @@ class ProfilePictureViewModel extends ChangeNotifier {
// Increment version to trigger targeted rebuild (no full screen refresh)
_profileImageVersion.value++;
print('🔄 Profile image version updated to ${_profileImageVersion.value} (targeted rebuild)');
onSuccess(data);
final String successMessage = data is String
? data
: (data is Map && data['message'] != null
? data['message'].toString()
: data?.toString() ?? 'Success');
onSuccess(successMessage);
},
onError: (error) {
print('❌ Error uploading profile image: $error');
@ -308,7 +292,6 @@ class ProfilePictureViewModel extends ChangeNotifier {
try {
_cachedImageBytes = base64Decode(imageData!);
_cachedImageDataHash = currentHash;
print('🔄 Updated cached image bytes');
} catch (e) {
print('❌ Error decoding profile image: $e');
_cachedImageBytes = null;
@ -326,7 +309,6 @@ class ProfilePictureViewModel extends ChangeNotifier {
_cachedImageDataHash = null;
_selectedImage = null;
notifyListeners();
print('🧹 Cleared all profile picture cache');
}
/// Check if we should show shimmer loading

@ -1964,5 +1964,9 @@ abstract class LocaleKeys {
static const doctorSchedule = 'doctorSchedule';
static const weeklyShift = 'weeklyShift';
static const dayOff = 'dayOff';
static const hide = 'hide';
static const viewPatientQR = 'viewPatientQR';
static const patientQR = 'patientQR';
static const forText = 'forText';
}

@ -63,6 +63,7 @@ import 'core/utils/size_utils.dart';
import 'features/monthly_reports/terms_conditions_view_model.dart';
import 'firebase_options.dart';
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
@ -109,11 +110,6 @@ Future<void> callInitializations() async {
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
// Download the latest JSONs from your server
// await TranslationService.downloadTranslations();
// await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
try {
// Attempt to get the default app. If it exists, this avoids the error.
await Firebase.app();
@ -374,6 +370,7 @@ class MyApp extends StatelessWidget {
darkTheme: AppTheme.getDarkTheme(isArabic),
themeMode: profileVm.isDarkMode ? ThemeMode.dark : ThemeMode.light,
navigatorKey: getIt.get<NavigationService>().navigatorKey,
navigatorObservers: [getIt.get<NavigationService>().routeObserver],
);
},
);

@ -1200,8 +1200,8 @@ class _AppointmentDetailsPageState extends State<AppointmentDetailsPage> {
patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel,
onSuccess: (apiResponse) {
LoaderBottomSheet.hideLoader();
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
myAppointmentsViewModel.getPatientAppointments(true, false);
// myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
// myAppointmentsViewModel.getPatientAppointments(true, false);
showCommonBottomSheet(context,
child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentConfirmedSuccessfully.tr(context: context)), callBackFunc: (str) {
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);

@ -69,12 +69,18 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
widget.patientAppointmentHistoryResponseModel.clinicID,
widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(), widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false, onSuccess: (val) {
myAppointmentsViewModel.getTamaraInstallmentsDetails().then((val) {
if (myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel != null) {
// if (myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel != null && myAppointmentsViewModel.patientAppointmentShareResponseModel != null) {
if (myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel != null &&
myAppointmentsViewModel.patientAppointmentShareResponseModel?.patientShareWithTax != null &&
myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel?.minLimit?.amount != null &&
myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel?.maxLimit?.amount != null) {
if (myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax! >= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.minLimit!.amount! &&
myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax! <= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.maxLimit!.amount!) {
setState(() {
isShowTamara = true;
});
if (mounted) {
setState(() {
isShowTamara = true;
});
}
}
}
});
@ -139,12 +145,30 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "MADA";
if (appState.isPaytabsEnabled) {
transID = Utils.getAppointmentTransID(
widget.patientAppointmentHistoryResponseModel.projectID,
widget.patientAppointmentHistoryResponseModel.clinicID,
widget.patientAppointmentHistoryResponseModel.appointmentNo,
);
paytabsViewModel.setPaymentConfiguration(
"Appointment Payment",
num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString()),
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
paytabsViewModel.startCardPayment(
paymentOption: selectedPaymentMethod,
liveServiceID: "0",
isScheduleLiveCare: widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? "true" : "false",
appointmentDate: widget.patientAppointmentHistoryResponseModel.appointmentDate!,
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(),
doctorID: widget.patientAppointmentHistoryResponseModel.doctorID.toString(),
transactionID: transID,
amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(),
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
orderDescription: "Appointment Payment",
serviceID: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum().toString(),
projectID: widget.patientAppointmentHistoryResponseModel.projectID.toString(),
onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
await myAppointmentsViewModel.createAdvancePayment(
paymentMethodName: selectedPaymentMethod,
projectID: widget.patientAppointmentHistoryResponseModel.projectID,
@ -362,12 +386,30 @@ class _AppointmentPaymentPageState extends State<AppointmentPaymentPage> {
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "VISA";
if (appState.isPaytabsEnabled) {
transID = Utils.getAppointmentTransID(
widget.patientAppointmentHistoryResponseModel.projectID,
widget.patientAppointmentHistoryResponseModel.clinicID,
widget.patientAppointmentHistoryResponseModel.appointmentNo,
);
paytabsViewModel.setPaymentConfiguration(
"Appointment Payment",
num.parse(myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString()),
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
paytabsViewModel.startCardPayment(
paymentOption: selectedPaymentMethod,
liveServiceID: "0",
isScheduleLiveCare: widget.patientAppointmentHistoryResponseModel.isLiveCareAppointment! ? "true" : "false",
appointmentDate: widget.patientAppointmentHistoryResponseModel.appointmentDate!,
appointmentNo: widget.patientAppointmentHistoryResponseModel.appointmentNo.toString(),
doctorID: widget.patientAppointmentHistoryResponseModel.doctorID.toString(),
transactionID: transID,
amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(),
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
orderDescription: "Appointment Payment",
serviceID: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum().toString(),
projectID: widget.patientAppointmentHistoryResponseModel.projectID.toString(),
onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr(context: context));
await myAppointmentsViewModel.createAdvancePayment(
paymentMethodName: selectedPaymentMethod,
projectID: widget.patientAppointmentHistoryResponseModel.projectID,

@ -43,8 +43,11 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
@override
void initState() {
scheduleMicrotask(() {
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
if (!myAppointmentsViewModel.isMyAppointmentsLoading) {
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: false);
}
// myAppointmentsViewModel.changeTabToArrived();
});
super.initState();
}
@ -67,9 +70,9 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
activeTextColor: Color(0xffED1C2B),
activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1),
tabs: [
CustomTabBarModel(null, LocaleKeys.allAppt.tr(context: context)),
// CustomTabBarModel(null, LocaleKeys.allAppt.tr(context: context)),
CustomTabBarModel(null, LocaleKeys.upcoming.tr(context: context)),
CustomTabBarModel(null, LocaleKeys.completed.tr(context: context)),
CustomTabBarModel(null, LocaleKeys.arrived.tr(context: context)),
],
onTabChange: (index) {
setState(() {
@ -78,6 +81,10 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
myAppointmentsViewModel.onTabChange(index);
myAppointmentsViewModel.updateListWRTTab(index);
context.read<DateRangeSelectorRangeViewModel>().flush();
if(index == 1) {
myAppointmentsViewModel.loadFullArrivedAppointmentsIfNeeded();
}
},
).paddingSymmetrical(24.h, 0.h),
// Consumer<MyAppointmentsViewModel>(builder: (context, myAppointmentsVM, child) {
@ -148,7 +155,7 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
? myAppointmentsVM.patientAppointmentsViewList.length
: 1,
itemBuilder: (context, index) {
final isExpanded = myAppointmentsVM.selectedTabIndex == 1 ? true : expandedIndex == index;
final isExpanded = myAppointmentsVM.selectedTabIndex == 0 ? true : expandedIndex == index;
return myAppointmentsVM.isMyAppointmentsLoading
? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true),

@ -39,7 +39,7 @@ class _MyDoctorsPageState extends State<MyDoctorsPage> {
@override
void initState() {
scheduleMicrotask(() {
myAppointmentsViewModel.getPatientMyDoctors();
myAppointmentsViewModel.getPatientMyDoctors(isTop8: false);
});
super.initState();
}

@ -14,6 +14,7 @@ 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/book_appointments/book_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_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/contact_us/contact_us_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/models/resp_models/patient_appointment_history_response_model.dart';
@ -231,7 +232,7 @@ class _AppointmentCardState extends State<AppointmentCard> {
Utils.buildImgWithNetwork(
url: widget.isLoading
? 'https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png'
: widget.patientAppointmentHistoryResponseModel.doctorImageURL!,
: widget.patientAppointmentHistoryResponseModel.doctorImageURL,
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
@ -277,7 +278,7 @@ class _AppointmentCardState extends State<AppointmentCard> {
(widget.isLoading ? 'Dr' : "${widget.patientAppointmentHistoryResponseModel.doctorTitle}").toText16(isBold: true, maxlines: 1),
Expanded(
child: (widget.isLoading ? 'John Doe' : " ${widget.patientAppointmentHistoryResponseModel.doctorNameObj!.truncate(30)}").toText16(
isBold: true, maxlines: 2, textOverflow: TextOverflow.ellipsis, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")),
isBold: true, maxlines: 1, textOverflow: TextOverflow.ellipsis, isEnglishOnly: !Utils.isArabicText(widget.patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")),
),
SizedBox(width: 12.w),
(widget.patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null &&
@ -677,7 +678,15 @@ class _AppointmentCardState extends State<AppointmentCard> {
projectName: widget.patientAppointmentHistoryResponseModel.projectName,
);
final clinic = GetClinicsListResponseModel(
clinicID: widget.patientAppointmentHistoryResponseModel.clinicID,
clinicDescription: widget.patientAppointmentHistoryResponseModel.clinicName,
clinicDescriptionN: widget.patientAppointmentHistoryResponseModel.clinicName,
);
widget.bookAppointmentsViewModel.setSelectedDoctor(doctor);
widget.bookAppointmentsViewModel.setSelectedClinic(clinic);
LoaderBottomSheet.showLoader();
await widget.bookAppointmentsViewModel.getDoctorFreeSlots(

@ -101,7 +101,7 @@ class AppointmentDoctorCard extends StatelessWidget {
Row(
children: [
Expanded(
child: patientAppointmentHistoryResponseModel.doctorNameObj!.toText16(
child: (patientAppointmentHistoryResponseModel.doctorNameObj ?? "").toText16(
isBold: true,
isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? ""),
textOverflow: TextOverflow.ellipsis,
@ -127,9 +127,9 @@ class AppointmentDoctorCard extends StatelessWidget {
runSpacing: 6.h,
children: [
AppCustomChipWidget(
labelText: (patientAppointmentHistoryResponseModel.clinicName!.length > 20
? '${patientAppointmentHistoryResponseModel.clinicName!.substring(0, 20)}...'
: patientAppointmentHistoryResponseModel.clinicName!),
labelText: ((patientAppointmentHistoryResponseModel.clinicName ?? "").length > 20
? '${(patientAppointmentHistoryResponseModel.clinicName ?? "").substring(0, 20)}...'
: (patientAppointmentHistoryResponseModel.clinicName ?? "")),
labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 4.w),
),
AppCustomChipWidget(

@ -26,7 +26,7 @@ class BuildDoctorRowAppointmentRating extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.buildImgWithNetwork(
url: patientAppointmentHistoryResponseModel.doctorImageURL!,
url: patientAppointmentHistoryResponseModel.doctorImageURL,
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,

@ -16,6 +16,7 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/appointments/widgets/region_bottomsheet/region_list_item.dart' show RegionListItem;
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import 'package:lottie/lottie.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
@ -117,6 +118,14 @@ class _RegionBottomSheetBodyState extends State<RegionBottomSheetBody> {
height: MediaQuery.of(context).size.height * 0.5,
child: Column(
children: [
// AppCustomChipWidget(
// richText: Row(
// children: [
// "${appState.getAuthenticatedUser()!.age} ".toText10(color: AppColors.blackColor, isBold: true),
// LocaleKeys.yearsOld.tr(context: context).toText10(color: AppColors.blackColor, isBold: true),
// ],
// ),
// ),
// Consumer<AppointmentViaRegionViewmodel>(
// builder: (context, regionVM, _) {
// return Padding(

@ -88,7 +88,7 @@ class AskDoctorAppointmentCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Utils.buildImgWithNetwork(
url: askDoctorAppointmentHistoryList.doctorImageURL!,
url: askDoctorAppointmentHistoryList.doctorImageURL,
iconColor: AppColors.blackBgColor,
width: 63.h,
height: 63.h,

@ -17,6 +17,9 @@ import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/app_bar_widget.dart';
import 'package:hmg_patient_app_new/widgets/bottomsheet/generic_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart' show CustomButton;
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/date_range_calender.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/models/calendar_design_type.dart';
import 'package:hmg_patient_app_new/widgets/dropdown/country_dropdown_widget.dart';
import 'package:hmg_patient_app_new/widgets/input_widget.dart';
import 'package:provider/provider.dart';
@ -153,30 +156,56 @@ class _RegisterNew extends State<RegisterNew> {
},
).withVerticalPadding(8),
Divider(height: 1),
TextInputWidget(
labelText: LocaleKeys.dob.tr(context: context),
hintText: "11 July, 1994",
controller: authVm.dobController,
focusNode: _dobFocusNode,
isEnable: true,
prefix: null,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
padding: EdgeInsets.symmetric(vertical: 8.h),
leadingIcon: AppAssets.birthday_cake,
selectionType: SelectionTypeEnum.calendar,
onCalendarTypeChanged: authVm.onCalenderTypeChange,
onChange: (value) {
authVm.onDobChange(value);
// Clear error when user selects a date
authVm.clearDobError();
GestureDetector(
onTap: () async {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.selectDate.tr(context: context),
context,
child: DateRangeSelector(
onRangeSelected: (DateTime? start, DateTime? end) {
if (start != null) {
var picked = start.toString().split(" ").first;
// authVm.dobController.text = picked;
authVm.onDobChange(picked);
// Clear error when user selects a date
authVm.clearDobError();
}
},
designType: CalendarDesignType.designV2,
showCalendarToggle: true,
onCalendarTypeChanged: null,
btnTitle: LocaleKeys.done.tr(context: context),
),
isFullScreen: false,
isCloseButtonVisible: true,
callBackFunc: () {},
);
},
fontFamily: "Poppins",
hasError: false,
// Don't show individual field border
errorMessage: authVm.dobError, // Show error message if exists
).withVerticalPadding(8),
child: TextInputWidget(
labelText: LocaleKeys.dob.tr(context: context),
hintText: "11 July, 1994",
controller: authVm.dobController,
focusNode: _dobFocusNode,
isEnable: false,
prefix: null,
isAllowRadius: true,
isBorderAllowed: false,
isAllowLeadingIcon: true,
padding: EdgeInsets.symmetric(vertical: 8.h),
leadingIcon: AppAssets.birthday_cake,
selectionType: SelectionTypeEnum.calendar,
onCalendarTypeChanged: authVm.onCalenderTypeChange,
onChange: (value) {
authVm.onDobChange(value);
// Clear error when user selects a date
authVm.clearDobError();
},
fontFamily: "Poppins",
hasError: false,
// Don't show individual field border
errorMessage: authVm.dobError, // Show error message if exists
).withVerticalPadding(8),
),
],
),
),

@ -16,6 +16,7 @@ import 'package:hmg_patient_app_new/features/authentication/authentication_view_
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/authentication/login.dart';
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.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/appbar/app_bar_widget.dart';
import 'package:hmg_patient_app_new/widgets/bottomsheet/generic_bottom_sheet.dart';
@ -30,11 +31,24 @@ class SavedLogin extends StatefulWidget {
_SavedLogin createState() => _SavedLogin();
}
class _SavedLogin extends State<SavedLogin> {
class _SavedLogin extends State<SavedLogin> with RouteAware {
LoginTypeEnum loginType = LoginTypeEnum.sms;
late AuthenticationViewModel authVm;
late AppState appState;
bool? isOther;
PageRoute<dynamic>? _pageRoute;
void _assignSavedLoginData() {
authVm.phoneNumberController.text = appState.getSelectDeviceByImeiRespModelElement!.mobile!.startsWith("0")
? appState.getSelectDeviceByImeiRespModelElement!.mobile!.replaceFirst("0", "")
: appState.getSelectDeviceByImeiRespModelElement!.mobile!;
authVm.nationalIdController.text = appState.getSelectDeviceByImeiRespModelElement!.identificationNo!;
if (isOther == true) {
authVm.phoneNumberController.text = appState.getSelectDeviceByImeiRespModelElement!.mobile!;
authVm.selectedCountrySignup = CountryEnum.others;
}
}
@override
void initState() {
@ -43,19 +57,10 @@ class _SavedLogin extends State<SavedLogin> {
appState = getIt.get<AppState>();
loginType = LoginTypeExtension.fromValue(appState.getSelectDeviceByImeiRespModelElement!.logInType!)!;
authVm.phoneNumberController.text = appState.getSelectDeviceByImeiRespModelElement!.mobile!.startsWith("0")
? appState.getSelectDeviceByImeiRespModelElement!.mobile!.replaceFirst("0", "")
: appState.getSelectDeviceByImeiRespModelElement!.mobile!;
authVm.nationalIdController.text = appState.getSelectDeviceByImeiRespModelElement!.identificationNo!;
// Check if this is an "Others" country user
isOther = appState.getSelectDeviceByImeiRespModelElement!.isOther == true;
// Set country to Others if isOther flag is true - this ensures _Others APIs are called
if (isOther!) {
authVm.phoneNumberController.text = appState.getSelectDeviceByImeiRespModelElement!.mobile!;
authVm.selectedCountrySignup = CountryEnum.others;
}
_assignSavedLoginData();
if (loginType == LoginTypeEnum.fingerprint || loginType == LoginTypeEnum.face) {
authVm.loginWithFingerPrintFace(() {});
@ -63,6 +68,30 @@ class _SavedLogin extends State<SavedLogin> {
super.initState();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final route = ModalRoute.of(context);
if (route is PageRoute && route != _pageRoute) {
if (_pageRoute != null) {
getIt.get<NavigationService>().routeObserver.unsubscribe(this);
}
_pageRoute = route;
getIt.get<NavigationService>().routeObserver.subscribe(this, route);
}
}
@override
void didPopNext() {
_assignSavedLoginData();
}
@override
void dispose() {
getIt.get<NavigationService>().routeObserver.unsubscribe(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(

@ -66,14 +66,14 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
// bookAppointmentsViewModel.getLocation();
immediateLiveCareViewModel.initImmediateLiveCare();
if (appState.isAuthenticated) {
getIt.get<MyAppointmentsViewModel>().getPatientMyDoctors();
getIt.get<MyAppointmentsViewModel>().getPatientFavouriteDoctors();
getIt.get<MyAppointmentsViewModel>().getPatientMyDoctors(isTop8: true);
// getIt.get<MyAppointmentsViewModel>().getPatientFavouriteDoctors();
}
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (bookAppointmentsViewModel.selectedTabIndex == 1) {
if (appState.isAuthenticated) {
getIt.get<MyAppointmentsViewModel>().getPatientMyDoctors();
getIt.get<MyAppointmentsViewModel>().getPatientMyDoctors(isTop8: true);
getIt.get<MyAppointmentsViewModel>().getPatientFavouriteDoctors();
showUnKnownClinicBottomSheet();
}
@ -166,6 +166,7 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 14.h),
],
).paddingSymmetrical(24.h, 0.h).onPress(() {
myAppointmentsVM.setIsMyDoctorsDataToBeLoaded(true);
Navigator.of(context).push(
CustomPageRoute(
page: MyDoctorsPage(),
@ -195,7 +196,7 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Utils.buildImgWithNetwork(
url: myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!,
url: myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL,
iconColor: AppColors.transparent,
width: 64.h,
height: 64.h,
@ -388,7 +389,7 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Utils.buildImgWithNetwork(
url: myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl!,
url: myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl,
iconColor: AppColors.transparent,
width: 64.h,
height: 64.h,
@ -551,6 +552,7 @@ class _BookAppointmentPageState extends State<BookAppointmentPage> {
Transform.flip(flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon, iconColor: AppColors.textColor, width: 40.h, height: 40.h)),
],
).onPress(() {
Clarity.sendCustomEvent('Select_Region_Page');
bookAppointmentsViewModel.setProjectID(null);
openRegionListBottomSheet(context, RegionBottomSheetType.FOR_REGION);
}),

@ -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';
@ -8,6 +9,7 @@ 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/book_appointments/book_appointments_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/my_appointments/my_appointments_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart';
@ -29,6 +31,7 @@ class DoctorProfilePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
Clarity.setCurrentScreenName('Doctor Profile Page');
final bookAppointmentsViewModel = Provider.of<BookAppointmentsViewModel>(context, listen: false);
final appState = getIt.get<AppState>();
return Scaffold(
@ -86,7 +89,7 @@ class DoctorProfilePage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.buildImgWithNetwork(
url: bookAppointmentsViewModel.doctorsProfileResponseModel.doctorImageURL!,
url: bookAppointmentsViewModel.doctorsProfileResponseModel.doctorImageURL,
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
@ -113,7 +116,7 @@ class DoctorProfilePage extends StatelessWidget {
],
),
Utils.buildImgWithNetwork(
url: bookAppointmentsViewModel.doctorsProfileResponseModel.nationalityFlagURL!,
url: bookAppointmentsViewModel.doctorsProfileResponseModel.nationalityFlagURL,
iconColor: AppColors.transparent,
width: 32.h,
height: 32.h,
@ -240,6 +243,14 @@ class DoctorProfilePage extends StatelessWidget {
bookAppointmentsViewModel.selectedDoctor.clinicName = bookAppointmentsViewModel.doctorsProfileResponseModel.clinicDescription;
bookAppointmentsViewModel.selectedDoctor.projectName = bookAppointmentsViewModel.doctorsProfileResponseModel.projectName;
final clinic = GetClinicsListResponseModel(
clinicID: bookAppointmentsViewModel.doctorsProfileResponseModel.clinicID,
clinicDescription: bookAppointmentsViewModel.doctorsProfileResponseModel.clinicDescription,
clinicDescriptionN: bookAppointmentsViewModel.doctorsProfileResponseModel.clinicDescription,
);
bookAppointmentsViewModel.setSelectedClinic(clinic);
LoaderBottomSheet.showLoader();
bookAppointmentsViewModel.isLiveCareSchedule
? await bookAppointmentsViewModel.getLiveCareDoctorFreeSlots(

@ -130,11 +130,29 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "MADA";
if (appState.isPaytabsEnabled) {
transID = Utils.getAppointmentTransID(
immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceID!,
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
DateTime.now().millisecondsSinceEpoch,
);
paytabsViewModel.setPaymentConfiguration(
"LiveCare Payment",
num.parse(immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total ?? "0.0"),
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
paytabsViewModel.startCardPayment(
paymentOption: selectedPaymentMethod,
liveServiceID: immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceID.toString(),
isScheduleLiveCare: "false",
appointmentDate: DateUtil.convertDateToString(DateTime.now()),
appointmentNo: "0",
doctorID: "0",
transactionID: transID,
amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(),
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
orderDescription: "LiveCare Payment",
serviceID: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum().toString(),
projectID: (ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12).toString(),
onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr());
await immediateLiveCareViewModel.addNewCallRequestForImmediateLiveCare(transactionData.transactionReference!);
await immediateLiveCareViewModel.getPatientLiveCareHistory();
@ -281,11 +299,29 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "VISA";
if (appState.isPaytabsEnabled) {
transID = Utils.getAppointmentTransID(
immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceID!,
ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12,
DateTime.now().millisecondsSinceEpoch,
);
paytabsViewModel.setPaymentConfiguration(
"LiveCare Payment",
num.parse(immediateLiveCareViewModel.liveCareImmediateAppointmentFeesList.total ?? "0.0"),
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
paytabsViewModel.startCardPayment(
paymentOption: selectedPaymentMethod,
liveServiceID: immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceID.toString(),
isScheduleLiveCare: "false",
appointmentDate: DateUtil.convertDateToString(DateTime.now()),
appointmentNo: "0",
doctorID: "0",
transactionID: transID,
amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(),
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
orderDescription: "LiveCare Payment",
serviceID: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum().toString(),
projectID: (ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12).toString(),
onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingPaymentStatusPleaseWait.tr());
await immediateLiveCareViewModel.addNewCallRequestForImmediateLiveCare(transactionData.transactionReference!);
await immediateLiveCareViewModel.getPatientLiveCareHistory();
@ -544,12 +580,12 @@ class _ImmediateLiveCarePaymentPageState extends State<ImmediateLiveCarePaymentP
if (selectedPaymentMethod == "TAMARA") {
if (Platform.isAndroid) {
Uri uri = new Uri.dataFromString(url);
tamaraPaymentStatus = uri.queryParameters['status']!;
tamaraOrderID = uri.queryParameters['AuthorizePaymentId']!;
tamaraPaymentStatus = uri.queryParameters['status'] ?? "";
tamaraOrderID = uri.queryParameters['AuthorizePaymentId'] ?? "";
} else {
Uri uri = new Uri.dataFromString(url);
tamaraPaymentStatus = uri.queryParameters['paymentStatus']!;
tamaraOrderID = uri.queryParameters['orderId']!;
tamaraPaymentStatus = uri.queryParameters['paymentStatus'] ?? "";
tamaraOrderID = uri.queryParameters['orderId'] ?? "";
}
}

@ -424,6 +424,7 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
void initiateBookAppointment() async {
// LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: false, loadingText: bookAppointmentsViewModel.isPatientRescheduleAppointment ? LocaleKeys.reschedulingAppo.tr(context: context) : LocaleKeys.bookingYourAppointment.tr(context: context));
Clarity.sendCustomEvent('Appointment_book_success');
LoaderBottomSheet.showLoader(
loadingText: bookAppointmentsViewModel.isPatientRescheduleAppointment
? LocaleKeys.reschedulingAppo.tr(context: context)
@ -449,6 +450,7 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
callBackFunc: () {
bookAppointmentsViewModel.setIsPatientRescheduleAppointment(false);
bookAppointmentsViewModel.setIsLiveCareSchedule(false);
bookAppointmentsViewModel.setSelectedChiefComplaintID(0);
Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
@ -524,6 +526,7 @@ class _ReviewAppointmentPageState extends State<ReviewAppointmentPage> {
child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()).paddingSymmetrical(0.h, 24.h), callBackFunc: () {
bookAppointmentsViewModel.setIsLiveCareSchedule(false);
bookAppointmentsViewModel.setIsPatientRescheduleAppointment(false);
bookAppointmentsViewModel.setSelectedChiefComplaintID(0);
Navigator.pushAndRemoveUntil(context, CustomPageRoute(page: LandingNavigation()), (r) => false);
}, isFullScreen: false, isCloseButtonVisible: false, isAutoDismiss: true);
});

@ -1021,6 +1021,7 @@ class _SelectClinicPageState extends State<SelectClinicPage> {
void onClinicSelected(GetClinicsListResponseModel clinic) {
bookAppointmentsViewModel.setSelectedClinic(clinic);
bookAppointmentsViewModel.setIsDoctorsListLoading(true);
searchEditingController.text = "";
if (clinic.isLiveCareClinicAndOnline ?? false) {
Navigator.of(context).push(
CustomPageRoute(
@ -1202,7 +1203,9 @@ class _SelectClinicPageState extends State<SelectClinicPage> {
getTitle(AppointmentViaRegionViewmodel data) {
if (data.selectedRegionId == null) {
return LocaleKeys.selectRegion.tr(context: context).toText20(isBold: true);
return Expanded(
child: "${LocaleKeys.selectRegion.tr(context: context)} ${LocaleKeys.forText.tr(context: context)} '${bookAppointmentsViewModel.selectedClinic.clinicDescription}'".toText20(isBold: true),
);
} else {
return Transform.flip(
flipX: data.isArabic ? true : false,

@ -48,7 +48,7 @@ class _SelectDoctorPageState extends State<SelectDoctorPage> {
Clarity.setCurrentScreenName('Select Doctor Page');
_scrollController = ScrollController();
scheduleMicrotask(() {
bookAppointmentsViewModel.setIsNearestAppointmentSelected(true);
bookAppointmentsViewModel.setIsNearestAppointmentSelected(false);
if (bookAppointmentsViewModel.isLiveCareSchedule) {
bookAppointmentsViewModel.getLiveCareDoctorsList();
} else {
@ -57,7 +57,7 @@ class _SelectDoctorPageState extends State<SelectDoctorPage> {
} else if (bookAppointmentsViewModel.isGetDocForHealthCal) {
bookAppointmentsViewModel.getDoctorsListByHealthCal();
} else {
bookAppointmentsViewModel.getDoctorsList();
bookAppointmentsViewModel.getDoctorsList(isNearest: false);
}
}
});
@ -133,8 +133,28 @@ class _SelectDoctorPageState extends State<SelectDoctorPage> {
isLoading: bookAppointmentsVM.isClinicsListLoading,
doctorsListResponseModel: bookAppointmentsVM.liveCareDoctorsList[index],
bookAppointmentsViewModel: bookAppointmentsViewModel,
).onPress(() {
// onLiveCareClinicSelected(bookAppointmentsVM.liveCareClinicsList[index]);
).onPress(() async {
bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.liveCareDoctorsList[index]);
LoaderBottomSheet.showLoader();
await bookAppointmentsVM.getDoctorProfile(onSuccess: (dynamic respData) {
LoaderBottomSheet.hideLoader();
Navigator.of(context).push(
CustomPageRoute(
page: DoctorProfilePage(
isDoctorAllowedToBook: true,
),
),
);
}, onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
}),
),
),

@ -138,12 +138,30 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "MADA";
if (appState.isPaytabsEnabled) {
transID = Utils.getAppointmentTransID(
bookAppointmentsViewModel.selectedDoctor.projectID!,
bookAppointmentsViewModel.selectedDoctor.clinicID!,
bookAppointmentsViewModel.selectedDoctor.doctorID!,
);
paytabsViewModel.setPaymentConfiguration(
"Appointment Payment",
bookAppointmentsViewModel.patientWalkInAppointmentShareResponseModel!.patientShareWithTax!,
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.bookingWaitingAppointment.tr(context: context));
paytabsViewModel.startCardPayment(
paymentOption: selectedPaymentMethod,
liveServiceID: "0",
isScheduleLiveCare: "false",
appointmentDate: DateUtil.convertDateToString(DateTime.now()),
appointmentNo: DateTime.now().millisecondsSinceEpoch.toString(),
doctorID: bookAppointmentsViewModel.selectedDoctor.doctorID.toString(),
transactionID: transID,
amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(),
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
orderDescription: "Appointment Payment",
serviceID: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum().toString(),
projectID: bookAppointmentsViewModel.selectedDoctor.projectID.toString(),
onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.bookingWaitingAppointment.tr(context: context));
await bookAppointmentsViewModel.insertSpecificAppointmentForWalkIn(onSuccess: (val) async {
String appointmentNo = val.data['AppointmentNo'].toString();
await myAppointmentsViewModel.createAdvancePayment(
@ -289,12 +307,31 @@ class _WaitingAppointmentPaymentPageState extends State<WaitingAppointmentPaymen
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "VISA";
if (appState.isPaytabsEnabled) {
transID = Utils.getAppointmentTransID(
bookAppointmentsViewModel.selectedDoctor.projectID!,
bookAppointmentsViewModel.selectedDoctor.clinicID!,
bookAppointmentsViewModel.selectedDoctor.doctorID!,
);
paytabsViewModel.setPaymentConfiguration(
"Appointment Payment",
bookAppointmentsViewModel.patientWalkInAppointmentShareResponseModel!.patientShareWithTax!,
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.bookingWaitingAppointment.tr(context: context));
paytabsViewModel.startCardPayment(
paymentOption: selectedPaymentMethod,
liveServiceID: "0",
isScheduleLiveCare: "false",
appointmentDate: DateUtil.convertDateToString(DateTime.now()),
appointmentNo: DateTime.now().millisecondsSinceEpoch.toString(),
doctorID: bookAppointmentsViewModel.selectedDoctor.doctorID.toString(),
transactionID: transID,
amount: myAppointmentsViewModel.patientAppointmentShareResponseModel!.patientShareWithTax!.toString(),
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
orderDescription: "Appointment Payment",
serviceID: ServiceTypeEnum.appointmentPayment.getIdFromServiceEnum().toString(),
projectID: bookAppointmentsViewModel.selectedDoctor.projectID.toString(),
onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.bookingWaitingAppointment.tr(context: context));
await bookAppointmentsViewModel.insertSpecificAppointmentForWalkIn(onSuccess: (val) async {
String appointmentNo = val.data['AppointmentNo'].toString();
await myAppointmentsViewModel.createAdvancePayment(

@ -14,8 +14,10 @@ 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/book_appointments_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/book_appointments/models/timeslots.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/dental_chief_complaints_page.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/review_appointment_page.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/waiting_appointment/waiting_appointment_info.dart';
import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart';
@ -95,63 +97,116 @@ class _AppointmentCalendarState extends State<AppointmentCalendar> {
// ),
SizedBox(
height: MediaQuery.of(context).size.height * 0.45,
child: Directionality(
textDirection: isArabic ? ui.TextDirection.rtl : ui.TextDirection.ltr,
child: Localizations.override(
context: context,
locale: isArabic ? const Locale('ar', 'SA') : const Locale('en'),
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),
),
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: Stack(
children: [
Directionality(
textDirection: isArabic ? ui.TextDirection.rtl : ui.TextDirection.ltr,
child: Localizations.override(
context: context,
locale: isArabic ? const Locale('ar', 'SA') : const Locale('en'),
child: ClarityUnmask(
child: SfCalendar(
controller: _calendarController,
minDate: DateTime.now(),
initialDisplayDate: DateUtil.convertStringToDate(bookAppointmentsViewModel.freeSlotsResponse[0]),
showNavigationArrow: false,
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),
),
),
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!);
},
),
),
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,
),
),
// Custom Navigation Arrows
Positioned(
top: 0,
right: isArabic ? null : 0,
left: isArabic ? 0 : null,
child: SizedBox(
height: 60.h,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Previous Month Arrow
GestureDetector(
onTap: () {
setState(() {
_calendarController.backward!();
});
},
child: Container(
padding: EdgeInsets.all(8.h),
child: Icon(
// isArabic ? Icons.arrow_forward_ios : Icons.arrow_back_ios,
Icons.arrow_back_ios,
size: 24.h,
color: AppColors.primaryRedColor,
),
),
),
// Next Month Arrow
GestureDetector(
onTap: () {
setState(() {
_calendarController.forward!();
});
},
child: Container(
padding: EdgeInsets.all(8.h),
child: Icon(
// isArabic ? Icons.arrow_back_ios : Icons.arrow_forward_ios,
Icons.arrow_forward_ios,
size: 24.h,
color: AppColors.primaryRedColor,
),
),
),
],
),
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!);
},
),
),
),
],
),
),
SizedBox(height: 10.h),
@ -164,7 +219,7 @@ class _AppointmentCalendarState extends State<AppointmentCalendar> {
? ClarityUnmask(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.15,
maxHeight: MediaQuery.of(context).size.height * 0.25,
minHeight: 0,
),
child: Directionality(
@ -213,15 +268,31 @@ class _AppointmentCalendarState extends State<AppointmentCalendar> {
),
);
} else {
bookAppointmentsViewModel.getAppointmentNearestGate(
projectID: bookAppointmentsViewModel.selectedDoctor.projectID!, clinicID: bookAppointmentsViewModel.selectedDoctor.clinicID!);
bookAppointmentsViewModel.setSelectedAppointmentDateTime(selectedDate, selectedTime, selectedDateDisplay);
Navigator.of(context).pop();
Navigator.of(context).push(
CustomPageRoute(
page: ReviewAppointmentPage(),
),
);
if (bookAppointmentsViewModel.selectedClinic.clinicID == 17 && bookAppointmentsViewModel.selectedChiefComplaintID == 0) {
GetClinicsListResponseModel selectedClinic = GetClinicsListResponseModel(
clinicID: bookAppointmentsViewModel.selectedClinic.clinicID,
clinicDescription: bookAppointmentsViewModel.selectedClinic.clinicDescription,
isLiveCareClinicAndOnline: false,
liveCareServiceID: 0,
liveCareClinicID: 0);
bookAppointmentsViewModel.setProjectID(bookAppointmentsViewModel.selectedDoctor.projectID.toString());
bookAppointmentsViewModel.setSelectedClinic(selectedClinic);
bookAppointmentsViewModel.setIsChiefComplaintsListLoading(true);
Navigator.of(context).push(
CustomPageRoute(
page: DentalChiefComplaintsPage(),
),
);
} else {
bookAppointmentsViewModel.getAppointmentNearestGate(projectID: bookAppointmentsViewModel.selectedDoctor.projectID!, clinicID: bookAppointmentsViewModel.selectedDoctor.clinicID!);
bookAppointmentsViewModel.setSelectedAppointmentDateTime(selectedDate, selectedTime, selectedDateDisplay);
Navigator.of(context).pop();
Navigator.of(context).push(
CustomPageRoute(
page: ReviewAppointmentPage(),
),
);
}
}
} else {
showCommonBottomSheetWithoutHeight(
@ -231,8 +302,7 @@ class _AppointmentCalendarState extends State<AppointmentCalendar> {
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(context: context)).toText16(color: AppColors.blackColor),
SizedBox(height: 16.h),
@ -331,12 +401,7 @@ class _AppointmentCalendarState extends State<AppointmentCalendar> {
selectedButtonIndex = 0;
List<Map<String, dynamic>> timeList = [];
for (var i = 0; i < dayEvents.length; i++) {
Map<String, dynamic> timeSlot = {
"isoTime": dayEvents[i].isoTime,
"start": dayEvents[i].start.toString(),
"end": dayEvents[i].end.toString(),
"vidaDate": dayEvents[i].vidaDate
};
Map<String, dynamic> timeSlot = {"isoTime": dayEvents[i].isoTime, "start": dayEvents[i].start.toString(), "end": dayEvents[i].end.toString(), "vidaDate": dayEvents[i].vidaDate};
timeList.add(timeSlot);
}
if (dayEvents.isNotEmpty) {

@ -144,8 +144,9 @@ class DoctorCard extends StatelessWidget {
spacing: 3.h,
runSpacing: 4.h,
children: [
bookAppointmentsViewModel.isNearestAppointmentSelected
? doctorsListResponseModel.nearestFreeSlot != null
// bookAppointmentsViewModel.isNearestAppointmentSelected
// ?
doctorsListResponseModel.nearestFreeSlot != null
? AppCustomChipWidget(
labelText: (isLoading ? "Cardiologist" : DateUtil.getDateStringForNearestSlot(doctorsListResponseModel.nearestFreeSlot)),
// richText: (isLoading ? "Cardiologist" : DateUtil.getDateStringForNearestSlot(doctorsListResponseModel.nearestFreeSlot))
@ -155,7 +156,8 @@ class DoctorCard extends StatelessWidget {
textColor: AppColors.successColor,
).toShimmer2(isShow: isLoading)
: SizedBox.shrink()
: SizedBox.shrink(),
// : SizedBox.shrink()
,
AppCustomChipWidget(
labelText: "${isLoading ? "Cardiologist" : doctorsListResponseModel.clinicName}",
).toShimmer2(isShow: isLoading),
@ -167,9 +169,10 @@ class DoctorCard extends StatelessWidget {
],
),
),
(doctorsListResponseModel.isLiveCare ?? true)
? SizedBox.shrink()
: Expanded(
// (doctorsListResponseModel.isLiveCare ?? true)
// ? SizedBox.shrink()
// :
Expanded(
flex: 1,
child: Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown).toShimmer2(isShow: isLoading),
),

@ -260,7 +260,7 @@ class FeedbackPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.buildImgWithNetwork(
url: contactUsViewModel.patientFeedbackSelectedAppointment!.doctorImageURL!,
url: contactUsViewModel.patientFeedbackSelectedAppointment?.doctorImageURL??"",
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,
@ -354,10 +354,10 @@ class FeedbackPage extends StatelessWidget {
ImageOptions.showImageOptionsNew(
context,
true,
(String image, file) {
// Navigator.pop(context);
contactUsViewModel.addFeedbackAttachment(image);
(String base64Image, file) {
// base64Image: The base64-encoded image string
// file: The File object
contactUsViewModel.addFeedbackAttachment(base64Image, file);
},
);
},
@ -384,25 +384,31 @@ class FeedbackPage extends StatelessWidget {
child: ListView.builder(
padding: EdgeInsets.all(16.h),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: contactUsViewModel.feedbackAttachmentList.length,
itemBuilder: (BuildContext context, int index) {
final attachment = contactUsViewModel.feedbackAttachmentList[index];
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.attach_file,
color: Color(0xff2B353E),
),
SizedBox(width: 8.w),
"Image ${index + 1}".toText14().paddingOnly(bottom: 8.h),
],
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.attach_file,
color: Color(0xff2B353E),
),
SizedBox(width: 8.w),
Expanded(
child: attachment.fileName.toText14().paddingOnly(bottom: 8.h),
),
],
),
),
Utils.buildSvgWithAssets(icon: AppAssets.cancel_circle_icon).onPress(() {
contactUsViewModel.removeFeedbackAttachment(contactUsViewModel.feedbackAttachmentList[index]);
contactUsViewModel.removeFeedbackAttachment(attachment);
}),
],
);

@ -124,12 +124,29 @@ class _ErOnlineCheckinPaymentPageState extends State<ErOnlineCheckinPaymentPage>
).paddingSymmetrical(24.h, 0.h).onPress(() {
selectedPaymentMethod = "MADA";
if (appState.isPaytabsEnabled) {
transID = Utils.getAdvancePaymentTransID(
emergencyServicesViewModel.selectedHospital!.iD,
appState.getAuthenticatedUser()!.patientId!,
);
paytabsViewModel.setPaymentConfiguration(
"ER Online Check-In Payment",
emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!,
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
if (emergencyServicesViewModel.isERBookAppointment) {
paytabsViewModel.startCardPayment(
paymentOption: selectedPaymentMethod,
liveServiceID: "0",
isScheduleLiveCare: "0",
appointmentDate: "",
appointmentNo: "0",
doctorID: "0",
transactionID: transID,
amount: emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!.toString(),
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
orderDescription: "ER Online Check-In Payment",
serviceID: ServiceTypeEnum.erOnlineCheckIn.getIdFromServiceEnum().toString(),
projectID: emergencyServicesViewModel.selectedHospital!.iD.toString(),
onSuccess: (PaytabsTransactionResponseModel transactionData) async {
if (emergencyServicesViewModel.isERBookAppointment) {
await emergencyServicesViewModel.ER_CreateAdvancePayment(
paymentMethodName: selectedPaymentMethod,
paymentReference: transactionData.transactionReference!,
@ -214,12 +231,29 @@ class _ErOnlineCheckinPaymentPageState extends State<ErOnlineCheckinPaymentPage>
).paddingSymmetrical(24.h, 0.h).onPress(() {
selectedPaymentMethod = "VISA";
if (appState.isPaytabsEnabled) {
transID = Utils.getAdvancePaymentTransID(
emergencyServicesViewModel.selectedHospital!.iD,
appState.getAuthenticatedUser()!.patientId!,
);
paytabsViewModel.setPaymentConfiguration(
"ER Online Check-In Payment",
emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!,
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
if (emergencyServicesViewModel.isERBookAppointment) {
paytabsViewModel.startCardPayment(
paymentOption: selectedPaymentMethod,
liveServiceID: "0",
isScheduleLiveCare: "0",
appointmentDate: "",
appointmentNo: "0",
doctorID: "0",
transactionID: transID,
amount: emergencyServicesViewModel.erOnlineCheckInPaymentDetailsResponse.patientShareWithTax!.toString(),
fileNumber: appState.getAuthenticatedUser()!.patientId.toString(),
orderDescription: "ER Online Check-In Payment",
serviceID: ServiceTypeEnum.erOnlineCheckIn.getIdFromServiceEnum().toString(),
projectID: emergencyServicesViewModel.selectedHospital!.iD.toString(),
onSuccess: (PaytabsTransactionResponseModel transactionData) async {
if (emergencyServicesViewModel.isERBookAppointment) {
await emergencyServicesViewModel.ER_CreateAdvancePayment(
paymentMethodName: selectedPaymentMethod,
paymentReference: transactionData.transactionReference!,

@ -89,7 +89,7 @@ class _HabibWalletState extends State<HabibWalletPage> {
"${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}"
.toText19(isBold: true, color: Colors.white),
"${LocaleKeys.fileno.tr(context: context)}: ${appState.getAuthenticatedUser()!.patientId!}"
.toText14(isBold: true, color: AppColors.greyTextColor, isEnglishOnly: true),
.toText14(isBold: true, color: AppColors.greyTextColor),
],
).expanded,
Utils.buildSvgWithAssets(icon: AppAssets.habiblogo, width: 24.h, height: 24.h, applyThemeColor: false),

@ -113,9 +113,23 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "MADA";
if (appState.isPaytabsEnabled) {
transID = Utils.getAdvancePaymentTransID(habibWalletVM.selectedHospital!.iD!, int.parse(habibWalletVM.fileNumber));
paytabsViewModel.setPaymentConfiguration("Advance Payment", habibWalletVM.walletRechargeAmount);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader();
paytabsViewModel.startCardPayment(
paymentOption: selectedPaymentMethod,
liveServiceID: "0",
isScheduleLiveCare: "false",
appointmentDate: DateUtil.convertDateToString(DateTime.now()),
appointmentNo: "0",
doctorID: "0",
transactionID: transID,
amount: habibWalletVM.walletRechargeAmount.toString(),
fileNumber: habibWalletVM.fileNumber.toString(),
orderDescription: "Advance Payment",
serviceID: ServiceTypeEnum.advancePayment.getIdFromServiceEnum().toString(),
projectID: habibWalletVM.selectedHospital!.iD!.toString(),
onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader();
await habibWalletVM.HISCreateAdvancePayment(
paymentMethodName: selectedPaymentMethod,
paidAmount: habibWalletVM.walletRechargeAmount,
@ -162,6 +176,7 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
);
});
}, onError: (err) {
LoaderBottomSheet.showLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err.toString()),
@ -295,9 +310,23 @@ class _WalletPaymentConfirmPageState extends State<WalletPaymentConfirmPage> {
).paddingSymmetrical(24.h, 0.h).onPress(() async {
selectedPaymentMethod = "VISA";
if (appState.isPaytabsEnabled) {
transID = Utils.getAdvancePaymentTransID(habibWalletVM.selectedHospital!.iD!, int.parse(habibWalletVM.fileNumber));
paytabsViewModel.setPaymentConfiguration("Advance Payment", habibWalletVM.walletRechargeAmount);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader();
paytabsViewModel.startCardPayment(
paymentOption: selectedPaymentMethod,
liveServiceID: "0",
isScheduleLiveCare: "false",
appointmentDate: DateUtil.convertDateToString(DateTime.now()),
appointmentNo: "0",
doctorID: "0",
transactionID: transID,
amount: habibWalletVM.walletRechargeAmount.toString(),
fileNumber: habibWalletVM.fileNumber.toString(),
orderDescription: "Advance Payment",
serviceID: ServiceTypeEnum.advancePayment.getIdFromServiceEnum().toString(),
projectID: habibWalletVM.selectedHospital!.iD!.toString(),
onSuccess: (PaytabsTransactionResponseModel transactionData) async {
LoaderBottomSheet.showLoader();
await habibWalletVM.HISCreateAdvancePayment(
paymentMethodName: selectedPaymentMethod,
paidAmount: habibWalletVM.walletRechargeAmount,

@ -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:get_it/get_it.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';
@ -13,8 +14,10 @@ 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/blood_donation/blood_donation_view_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart';
import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart';
import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/hmg_services_component_model.dart';
import 'package:hmg_patient_app_new/features/hospital/hospital_selection_view_model.dart';
import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart';
import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart';
import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart';
@ -45,6 +48,7 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../core/dependencies.dart';
import '../emergency_services/call_ambulance/widgets/HospitalBottomSheetBody.dart';
class ServicesPage extends StatefulWidget {
final bool showBackIcon;
@ -116,51 +120,51 @@ class _ServicesPageState extends State<ServicesPage> {
);
}
}),
// HmgServicesComponentModel(
// 11,
// LocaleKeys.indoorNavigation.tr(),
// "",
// AppAssets.indoor_nav_icon,
// bgColor: Color(0xff45A2F8),
// true,
// route: null,
// onTap: () {
// showCommonBottomSheetWithoutHeight(
// title: LocaleKeys.selectHospital.tr(),
// GetIt.instance<NavigationService>().navigatorKey.currentContext!,
// child: ChangeNotifierProvider(
// create: (context) => HospitalSelectionBottomSheetViewModel(getIt()),
// child: Consumer<HospitalSelectionBottomSheetViewModel>(
// builder: (_, vm, __) => HospitalBottomSheetBody(
// searchText: vm.searchController,
// displayList: vm.displayList,
// onFacilityClicked: (value) {
// vm.setSelectedFacility(value);
// vm.getDisplayList();
// },
// onHospitalClicked: (hospital) {
// Navigator.pop(GetIt.instance<NavigationService>().navigatorKey.currentContext!);
// vm.openPenguin(hospital);
// },
// onHospitalSearch: (value) {
// vm.searchHospitals(value ?? "");
// },
// selectedFacility: vm.selectedFacility,
// hmcCount: vm.hmcCount,
// hmgCount: vm.hmgCount,
// ),
// ),
// ),
// isFullScreen: false,
// isCloseButtonVisible: true,
// hasBottomPadding: false,
// backgroundColor: AppColors.bottomSheetBgColor,
// callBackFunc: () {
// GetIt.instance<NavigationService>().navigatorKey.currentContext!.read<EmergencyServicesViewModel>().clearSearchText();
// },
// );
// },
// ),
HmgServicesComponentModel(
11,
LocaleKeys.indoorNavigation.tr(),
"",
AppAssets.indoor_nav_icon,
bgColor: Color(0xff45A2F8),
true,
route: null,
onTap: () {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.selectHospital.tr(),
GetIt.instance<NavigationService>().navigatorKey.currentContext!,
child: ChangeNotifierProvider(
create: (context) => HospitalSelectionBottomSheetViewModel(getIt()),
child: Consumer<HospitalSelectionBottomSheetViewModel>(
builder: (_, vm, __) => HospitalBottomSheetBody(
searchText: vm.searchController,
displayList: vm.displayList,
onFacilityClicked: (value) {
vm.setSelectedFacility(value);
vm.getDisplayList();
},
onHospitalClicked: (hospital) {
Navigator.pop(GetIt.instance<NavigationService>().navigatorKey.currentContext!);
vm.openPenguin(hospital);
},
onHospitalSearch: (value) {
vm.searchHospitals(value ?? "");
},
selectedFacility: vm.selectedFacility,
hmcCount: vm.hmcCount,
hmgCount: vm.hmgCount,
),
),
),
isFullScreen: false,
isCloseButtonVisible: true,
hasBottomPadding: false,
backgroundColor: AppColors.bottomSheetBgColor,
callBackFunc: () {
GetIt.instance<NavigationService>().navigatorKey.currentContext!.read<EmergencyServicesViewModel>().clearSearchText();
},
);
},
),
HmgServicesComponentModel(
11, LocaleKeys.eReferralServices.tr(), "", AppAssets.eReferral, bgColor: AppColors.eReferralCardColor, true, route: null, onTap: () async {
if (getIt.get<AppState>().isAuthenticated) {

@ -17,6 +17,7 @@ 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:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:ui' as ui;
class LakumWalletDetails extends StatelessWidget {
const LakumWalletDetails({super.key});
@ -239,10 +240,10 @@ class LakumWalletDetails extends StatelessWidget {
return CustomButton(
height: 46.h,
icon: AppAssets.redeem_icon,
iconColor: AppColors.whiteColor,
iconColor: AppColors.textColor,
iconSize: 24.h,
backgroundColor: AppColors.habibPharmacyColor,
textColor: AppColors.whiteColor,
textColor: AppColors.textColor,
text: LocaleKeys.redeem.tr(context: context),
borderWidth: 0.w,
fontWeight: FontWeight.w600,
@ -350,7 +351,10 @@ class LakumWalletDetails extends StatelessWidget {
maxlines: 2,
),
SizedBox(height: 2.h),
formattedDate.toText12(color: AppColors.greyTextColor),
Directionality(
textDirection: ui.TextDirection.ltr,
child: formattedDate.toText12(color: AppColors.greyTextColor),
),
],
),
),
@ -363,10 +367,10 @@ class LakumWalletDetails extends StatelessWidget {
isBold: true,
color: transactionColor,
),
if (transaction.amount != null && transaction.amount! > 0)
"${transaction.amount?.toStringAsFixed(2) ?? "0.00"} SAR".toText11(
color: AppColors.greyTextColor,
),
// if (transaction.amount != null && transaction.amount! > 0)
// "${transaction.amount?.toStringAsFixed(2) ?? "0.00"} SAR".toText11(
// color: AppColors.greyTextColor,
// ),
],
),
],

@ -120,12 +120,23 @@ class _LandingPageState extends State<LandingPage> {
authVM = context.read<AuthenticationViewModel>();
habibWalletVM = context.read<HabibWalletViewModel>();
appointmentRatingViewModel = context.read<AppointmentRatingViewModel>();
appState = getIt.get<AppState>();
authVM.savePushTokenToAppState();
if (mounted) {
authVM.checkLastLoginStatus(() {
showQuickLogin(context);
});
final user = appState.getAuthenticatedUser();
final hasUserQuickLoginData =
user != null &&
(user.mobileNumber?.isNotEmpty ?? false) &&
(user.patientIdentificationNo?.isNotEmpty ?? false);
if (hasUserQuickLoginData) {
authVM.checkLastLoginStatus(() {
if (mounted) {
showQuickLogin(context);
}
});
}
}
_scrollController.addListener(() {
@ -154,7 +165,7 @@ class _LandingPageState extends State<LandingPage> {
immediateLiveCareViewModel.initImmediateLiveCare();
immediateLiveCareViewModel.getPatientLiveCareHistory();
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: true);
emergencyServicesViewModel.checkPatientERAdvanceBalance();
// myAppointmentsViewModel.getPatientAppointmentQueueDetails();
notificationsViewModel.initNotificationsViewModel();
@ -172,12 +183,19 @@ class _LandingPageState extends State<LandingPage> {
appointmentNo,
projectID,
onSuccess: ((response) {
// Only open dialog if details are loaded AND widget is mounted
if (!mounted || appointmentRatingViewModel.appointmentDetails == null) return;
appointmentRatingViewModel.setClinicOrDoctor(false);
appointmentRatingViewModel.setTitle(LocaleKeys.rateDoctor.tr(context: context));
appointmentRatingViewModel.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr(context: context));
appointmentRatingViewModel.setTitle(LocaleKeys.rateDoctor.tr());
appointmentRatingViewModel.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr());
openLastRating();
appState.setRatedVisible(true);
}),
onError: (String errorMessage) {
// Silently mark as rated to prevent retry loops
if (mounted) appState.setRatedVisible(true);
},
);
}
},
@ -212,7 +230,7 @@ class _LandingPageState extends State<LandingPage> {
// Refresh Appointments Data
myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true);
myAppointmentsViewModel.initAppointmentsViewModel();
myAppointmentsViewModel.getPatientAppointments(true, false);
myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: true);
// Refresh Appointments Data
habibWalletVM.initHabibWalletProvider();
@ -253,8 +271,8 @@ class _LandingPageState extends State<LandingPage> {
padding: EdgeInsets.only(
top: (appState.isAuthenticated && !insuranceVM.isInsuranceLoading && insuranceVM.isInsuranceExpired && insuranceVM.isInsuranceExpiryBannerShown)
? (MediaQuery.paddingOf(context).top + 70.h)
: kToolbarHeight + 0.h,
bottom: 24),
: kToolbarHeight + 16.h,
bottom: 24.h),
child: Column(
spacing: 16.h,
children: [
@ -493,8 +511,8 @@ class _LandingPageState extends State<LandingPage> {
).paddingSymmetrical(24.h, 0.h).onPress(() {
myAppointmentsViewModel.onTabChange(0);
myAppointmentsViewModel.updateListWRTTab(0);
Navigator.of(context).push(CustomPageRoute(page: MyAppointmentsPage()));
Navigator.of(context).push(CustomPageRoute(page: MyAppointmentsPage()));
// getIt.get<PayTabsViewModel>().setPaymentConfiguration("Appointment Payment", 15.5);
// getIt.get<PayTabsViewModel>().startCardPayment();
}),
@ -1458,6 +1476,7 @@ class _LandingPageState extends State<LandingPage> {
}
openLastRating() {
if (!mounted) return;
showCommonBottomSheetWithoutHeight(
context,
titleWidget: Selector<AppointmentRatingViewModel, String?>(

@ -128,7 +128,9 @@ class LargeServiceCard extends StatelessWidget {
),
],
),
);
).onPress(() {
handleOnTap();
});
}
void handleOnTap() {
@ -275,7 +277,9 @@ class FadedLargeServiceCard extends StatelessWidget {
),
],
),
);
).onPress(() {
handleOnTap();
});
}
void handleOnTap() {

@ -41,9 +41,13 @@ class WelcomeWidget extends StatelessWidget {
borderRadius: BorderRadius.circular(30),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8.h,
// spacing: 8.h,
children: [
Icon(Icons.menu, color: AppColors.textColor).onPress(() {
SizedBox(
height: 40.h,
width: 20.w,
child: Icon(Icons.menu, color: AppColors.textColor),
).onPress(() {
getIt.get<ProfileSettingsViewModel>().getProfileSettings();
Navigator.of(context).push(
CustomPageRoute(
@ -92,7 +96,7 @@ class WelcomeWidget extends StatelessWidget {
],
),
],
).expanded,
).paddingOnly(left: 8.h, right: 8.h).expanded,
],
),
),

@ -84,7 +84,7 @@ class InsuranceApprovalDetailsPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.buildImgWithNetwork(
url: insuranceApprovalResponseModel.doctorImageURL!,
url: insuranceApprovalResponseModel.doctorImageURL,
iconColor: AppColors.blackBgColor,
width: 63.h,
height: 63.h,
@ -102,7 +102,7 @@ class InsuranceApprovalDetailsPage extends StatelessWidget {
spacing: 3.h,
runSpacing: 4.h,
children: [
AppCustomChipWidget(labelText: insuranceApprovalResponseModel.clinicName!),
AppCustomChipWidget(labelText: insuranceApprovalResponseModel.clinicName ?? ""),
AppCustomChipWidget(labelText: "${LocaleKeys.approvalNo.tr(context: context)} ${insuranceApprovalResponseModel.approvalNo}", isEnglishOnly: true),
AppCustomChipWidget(labelText: "${LocaleKeys.unusedCount.tr(context: context)} ${insuranceApprovalResponseModel.unUsedCount}", isEnglishOnly: true),
AppCustomChipWidget(labelText: "${LocaleKeys.companyName.tr(context: context)} ${insuranceApprovalResponseModel.companyName}"),

@ -107,7 +107,7 @@ class InsuranceApprovalCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.buildImgWithNetwork(
url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/OALAY/1439.png" : insuranceApprovalResponseModel.doctorImageURL!,
url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/OALAY/1439.png" : insuranceApprovalResponseModel.doctorImageURL,
iconColor: AppColors.blackBgColor,
width: 63.h,
height: 63.h,

@ -133,7 +133,6 @@ class _AlphabetScrollPageState extends State<AlphabeticScroll> {
@override
Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.sizeOf(context).width,
child: Row(
@ -149,7 +148,6 @@ class _AlphabetScrollPageState extends State<AlphabeticScroll> {
],
),
),
SizedBox(
width: 24.w,
height: MediaQuery.sizeOf(context).height-(70.h+ kToolbarHeight+100.h),

File diff suppressed because one or more lines are too long

@ -56,7 +56,7 @@ class LabResultItemView extends StatelessWidget {
Row(
children: [
Utils.buildImgWithNetwork(
url: isLoading ? "" : labOrder!.doctorImageURL!,
url: isLoading ? "" : labOrder?.doctorImageURL??"",
iconColor: AppColors.transparent,
width: 24.h,
height: 24.h,

@ -1,45 +1,275 @@
import 'dart:ui' as ui;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/core/app_state.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/lab/lab_view_model.dart';
import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart';
import 'package:provider/provider.dart' show Selector, Provider, ReadContext;
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart';
import 'package:provider/provider.dart';
import '../../../core/dependencies.dart' show getIt;
import '../../../core/utils/size_utils.dart' show ResponsiveExtension;
import '../../../generated/locale_keys.g.dart' show LocaleKeys;
import '../../../widgets/date_range_selector/viewmodel/date_range_view_model.dart';
class LabResultList extends StatefulWidget {
@override
State<LabResultList> createState() => _LabResultListState();
}
class LabResultList extends StatelessWidget {
class _LabResultListState extends State<LabResultList> {
late LabViewModel model;
int expandedIndex = -1;
Map<int, bool> descriptionExpandedMap = {};
@override
Widget build(BuildContext context) {
model = Provider.of<LabViewModel>(context);
return Selector<LabViewModel, List<LabResult>>(
selector: (_, model) => model.mainLabResultsByHospitals,
builder: (__, list, ___) {
if (list.isEmpty && context.read<LabViewModel>().labSpecialResult.isEmpty) {
return Selector<LabViewModel, Map<String, List<LabResult>>>(
selector: (_, model) => model.mainLabResultsByPackageName,
builder: (__, groupedResults, ___) {
if (groupedResults.isEmpty && context.read<LabViewModel>().labSpecialResult.isEmpty) {
// return Utils.getNoDataWidget(context,
// noDataText: LocaleKeys.noLabResults.tr(context: context));
return Container();
} else {
// Convert map entries to a list for indexed access
final packageEntries = groupedResults.entries.toList();
return ListView.builder(
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
shrinkWrap: true,itemCount: list.length,itemBuilder: (____, index) {
var labItem = list[index];
return LabOrderResultItem(onTap: () {
context.read<DateRangeSelectorRangeViewModel>().flush();
model.getPatientLabResult(model.currentlySelectedPatientOrder!, labItem.description ?? "", labItem.testShortDescription ?? "", labItem.uOM ?? "");
},
tests: labItem,
index: index,
iconColor: model.getColor(labItem.calculatedResultFlag ?? "N"),
severityText: model.getSeverityText(labItem.calculatedResultFlag ?? "N"));
});
shrinkWrap: true,
itemCount: packageEntries.length,
itemBuilder: (context, packageIndex) {
final packageName = packageEntries[packageIndex].key;
final labResults = packageEntries[packageIndex].value;
final isExpanded = expandedIndex == packageIndex;
return AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
margin: EdgeInsets.symmetric(vertical: 8.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Package name as title/header
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.0.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8.h,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: packageName.toText18(
isBold: true
),
),
AnimatedRotation(
turns: isExpanded ? 0.5 : 0,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: Icon(Icons.expand_more),
),
],
),
if(labResults.first.packageShortDescription != null)
_ExpandableDescriptionText(
description: labResults.first.packageShortDescription!,
isExpanded: descriptionExpandedMap[packageIndex] ?? false,
onToggle: () {
setState(() {
descriptionExpandedMap[packageIndex] = !(descriptionExpandedMap[packageIndex] ?? false);
});
},
)
else
SizedBox.shrink()
],
),),
// Lab results under this package with animation
AnimatedSize(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: isExpanded
? Column(
children: labResults.asMap().entries.map((entry) {
final index = entry.key;
final labItem = entry.value;
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 300 + (index * 50)),
curve: Curves.easeOut,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(0, 20 * (1 - value)),
child: child,
),
);
},
child: Column(
children: [
LabOrderResultItem(
onTap: () {
context.read<DateRangeSelectorRangeViewModel>().flush();
model.getPatientLabResult(
model.currentlySelectedPatientOrder!,
labItem.description ?? "",
labItem.testShortDescription ?? "",
labItem.uOM ?? ""
);
},
tests: labItem,
index: index,
iconColor: model.getColor(labItem.calculatedResultFlag ?? "N"),
severityText: model.getSeverityText(labItem.calculatedResultFlag ?? "N")
),
if(labResults.length>1)
...[SizedBox(
height: 8.h,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.h),
child: Divider(
color: AppColors.dividerColor,
thickness: 1.h,
),
)]
],
),
);
}).toList(),
)
: SizedBox.shrink(),
),
],
),
).onPress((){
setState(() {
expandedIndex = isExpanded ? -1 : packageIndex;
});
});
}
);
}
},
);
}
}
class _ExpandableDescriptionText extends StatelessWidget {
final String description;
final bool isExpanded;
final VoidCallback onToggle;
const _ExpandableDescriptionText({
required this.description,
required this.isExpanded,
required this.onToggle,
});
static TextStyle get textStyle => TextStyle(
fontSize: 14.f,
color: AppColors.textColorLight,
// or Colors.black87
// fontFamily: 'Poppins',
fontWeight: FontWeight.w600,
fontStyle: FontStyle.normal,
height: 1.4,
letterSpacing: 0.0,
decoration: TextDecoration.none,
);
@override
Widget build(BuildContext context) {
final readMoreText = isExpanded
? ' ${LocaleKeys.hide.tr()}'
: ' ${LocaleKeys.readMore.tr(context: context)}';
final readMoreStyle = TextStyle(
fontSize: 12.f,
color: AppColors.primaryRedColor,
// fontFamily: 'Poppins',
fontWeight: FontWeight.w500,
);
return LayoutBuilder(
builder: (context, constraints) {
// Check if description alone overflows 2 lines
final descriptionPainter = TextPainter(
text: TextSpan(text: description, style: textStyle),
maxLines: 2,
textDirection: Directionality.of(context),
)..layout(maxWidth: constraints.maxWidth);
final isOverflowing = descriptionPainter.didExceedMaxLines;
// No overflow, or already expanded -> show full text
if (!isOverflowing || isExpanded) {
return GestureDetector(
onTap: isOverflowing ? onToggle : null,
child: Text.rich(
TextSpan(
style: textStyle,
children: [
TextSpan(text: description, style: textStyle),
if (isOverflowing)
TextSpan(text: readMoreText, style: readMoreStyle),
],
),
),
);
}
// Binary search for max chars that fit in 2 lines with "Read more" appended
int low = 0, high = description.length;
while (low < high) {
final mid = (low + high + 1) ~/ 2;
final testPainter = TextPainter(
text: TextSpan(
style: textStyle,
children: [
TextSpan(text: '${description.substring(0, mid)}...'),
TextSpan(text: readMoreText, style: readMoreStyle),
],
),
maxLines: 2,
textDirection: Directionality.of(context),
)..layout(maxWidth: constraints.maxWidth);
if (testPainter.didExceedMaxLines) {
high = mid - 1;
} else {
low = mid;
}
}
final truncated = description.substring(0, low).trimRight();
return GestureDetector(
onTap: onToggle,
child: Text.rich(
TextSpan(
style: textStyle,
children: [
TextSpan(text: '$truncated...'),
TextSpan(text: readMoreText, style: readMoreStyle),
],
),
),
);
},
);
}
}
}

@ -4,6 +4,7 @@ import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/enums.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
@ -12,6 +13,7 @@ import 'package:hmg_patient_app_new/features/lab/models/resp_models/lab_result.d
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:provider/provider.dart';
import 'dart:ui' as ui;
@ -24,7 +26,16 @@ class LabOrderResultItem extends StatelessWidget {
final bool isExpanded;
final Color iconColor;
const LabOrderResultItem({super.key, required this.onTap, this.tests, required this.index, this.isLoading = false, this.isExpanded = false, required this.iconColor, required this.severityText});
const LabOrderResultItem({
super.key,
required this.onTap,
this.tests,
required this.index,
this.isLoading = false,
this.isExpanded = false,
required this.iconColor,
required this.severityText
});
@override
build(BuildContext context) {
@ -32,10 +43,10 @@ class LabOrderResultItem extends StatelessWidget {
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
margin: EdgeInsets.symmetric(vertical: 8.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true),
// decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: false),
child: Container(
key: ValueKey<int>(index),
padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 16.h),
padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 4.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -84,7 +95,20 @@ class LabOrderResultItem extends StatelessWidget {
),
softWrap: true,
),
)
),
Visibility(
visible: tests?.notes != null && tests?.notes?.isNotEmpty == true,
child: Text(
tests?.notes??"",
style: TextStyle(
fontSize: 12.f,
fontWeight: FontWeight.w600,
fontFamily: getIt.get<AppState>().isArabic() ? 'CairoArabic' : 'Poppins',
color: AppColors.greyTextColor,
),
softWrap: true,
),
),
],
),
),
@ -104,7 +128,9 @@ class LabOrderResultItem extends StatelessWidget {
severityText.tr().toText10(isBold: true, color: AppColors.greyTextColor),
Utils.buildSvgWithAssets(icon: AppAssets.lab_result_indicator, width: 21, height: 23, iconColor: iconColor),
],
),
).onPress((){
_buildSeverityDetailsBottomsheet(severity: tests?.calculatedResultFlag ?? "N", context: context);
}),
),
),
Flexible(
@ -136,4 +162,77 @@ class LabOrderResultItem extends StatelessWidget {
),
));
}
_buildSeverityDetailsBottomsheet({required String severity,required BuildContext context}) {
return showCommonBottomSheetWithoutHeight(
title: LocaleKeys.colorScience.tr(context: context),
context,
child: Container(
padding: EdgeInsets.all(16.w),
width: double.infinity,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true),
child: Column(
children: [
_buildInfoTile(severityValue: severity, context: context),
],
),
),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}
Widget _buildInfoTile({required String severityValue, required BuildContext context}) {
String title = LocaleKeys.emergency.tr(context: context);
String description = LocaleKeys.emergencyDescription.tr(context: context);
Color dotColor = AppColors.chipColorEmergency;
switch (severityValue) {
case 'CL':
case 'LCL':
title = LocaleKeys.emergency.tr(context: context);
description = LocaleKeys.emergencyDescription.tr(context: context);
dotColor = AppColors.chipColorEmergency;
case 'L':
title = LocaleKeys.seekMedicalAdvice.tr(context: context);
description = LocaleKeys.seekMedicalAdviceDescription.tr(context: context);
dotColor = AppColors.chipColorSeekMedicalAdvice;
case 'N':
title = LocaleKeys.monitor.tr(context: context);
description = LocaleKeys.monitorDescription.tr(context: context);
dotColor = AppColors.chipColorMonitor;
case 'H':
title = LocaleKeys.seekMedicalAdvice.tr(context: context);
description = LocaleKeys.seekMedicalAdviceDescription.tr(context: context);
dotColor = AppColors.chipColorSeekMedicalAdvice;
case 'CH':
case 'HCH':
title = LocaleKeys.emergency.tr(context: context);
description = LocaleKeys.emergencyDescription.tr(context: context);
dotColor = AppColors.chipColorEmergency;
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 14.h,
width: 14.w,
decoration: BoxDecoration(shape: BoxShape.circle, color: dotColor),
).paddingOnly(top: 3.h),
SizedBox(width: 8.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title.toText14(isBold: true, color: AppColors.textColor),
SizedBox(height: 4.h),
description.toText12(isBold: true, color: AppColors.greyTextColor),
],
),
),
],
);
}
}

@ -110,16 +110,19 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
appState = getIt.get<AppState>();
scheduleMicrotask(() {
if (appState.isAuthenticated) {
myAppointmentsViewModel.getPatientMyDoctors();
prescriptionsViewModel.initPrescriptionsViewModel();
insuranceViewModel.initInsuranceProvider();
medicalFileViewModel.setIsPatientSickLeaveListLoading(true);
medicalFileViewModel.getPatientSickLeaveList();
medicalFileViewModel.onTabChanged(0);
medicalFileViewModel.setIsPatientMedicalReportsLoading(true);
medicalFileViewModel.getPatientMedicalReportList();
// Load vital signs
hmgServicesViewModel.getPatientVitalSign();
// myAppointmentsViewModel.getPatientMyDoctors(isTop8: true);
// prescriptionsViewModel.initPrescriptionsViewModel();
// medicalFileViewModel.setIsPatientSickLeaveListLoading(true);
// medicalFileViewModel.getPatientSickLeaveList();
// medicalFileViewModel.setIsPatientMedicalReportsLoading(true);
// medicalFileViewModel.getPatientMedicalReportList();
medicalFileViewModel.onTabChanged(0);
}
});
super.initState();
@ -236,8 +239,8 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.6,
child: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}"
.toText18(isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 2),
child:
"${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText18(isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 2),
),
SizedBox(height: 4.h),
Wrap(
@ -481,6 +484,20 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
expansionMode: ExpansionMode.exactlyOne,
dividerColor: AppColors.dividerColor,
itemPadding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 14.h),
onItemToggled: (index, isExpanded) {
debugPrint("Item at index $index is now ${isExpanded ? 'expanded' : 'collapsed'}");
if (index == 0 && isExpanded) {
myAppointmentsViewModel.getPatientMyDoctors(isTop8: true);
prescriptionsViewModel.initPrescriptionsViewModel();
}
if (index == 1 && isExpanded) {
medicalFileViewModel.setIsPatientSickLeaveListLoading(true);
medicalFileViewModel.getPatientSickLeaveList();
medicalFileViewModel.setIsPatientMedicalReportsLoading(true);
medicalFileViewModel.getPatientMedicalReportList();
}
},
items: [
ExpandableListItem(
title: LocaleKeys.medicalServices.tr(context: context).toText18(isBold: true),
@ -909,7 +926,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
child: Row(
children: [
Utils.buildImgWithNetwork(
url: prescriptionVM.patientPrescriptionOrders[index].doctorImageURL!,
url: prescriptionVM.patientPrescriptionOrders[index].doctorImageURL,
iconColor: AppColors.transparent,
width: 40.h,
height: 40.h,
@ -1046,6 +1063,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
],
).onPress(() {
// myAppointmentsViewModel.getPatientMyDoctors();
myAppointmentsViewModel.setIsMyDoctorsDataToBeLoaded(true);
Navigator.of(context).push(
CustomPageRoute(
page: MyDoctorsPage(),
@ -1099,7 +1117,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
itemBuilder: (context, index) {
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 1000),
duration: const Duration(milliseconds: 250),
child: SlideAnimation(
horizontalOffset: 100.0,
child: FadeInAnimation(
@ -1109,7 +1127,7 @@ class _MedicalFilePageState extends State<MedicalFilePage> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Utils.buildImgWithNetwork(
url: myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!,
url: myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL,
iconColor: AppColors.transparent,
width: 64.h,
height: 64.h,

@ -142,7 +142,7 @@ class _PatientSickleavesListPageState extends State<PatientSickleavesListPage> {
mainAxisSize: MainAxisSize.min,
children: [
Utils.buildImgWithNetwork(
url: model.patientSickLeaveList[index].doctorImageURL!,
url: model.patientSickLeaveList[index].doctorImageURL,
iconColor: AppColors.transparent,
width: 24.h,
height: 24.h,

@ -92,7 +92,7 @@ class MedicalReportCard extends StatelessWidget {
// fontWeight: FontWeight.w600,
isBold: true,
borderRadius: 12.r,
height: 48.h,
height: 40.h,
).toShimmer2(isShow: isLoading),
],
),

@ -57,7 +57,7 @@ class PatientSickLeaveCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.buildImgWithNetwork(
url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientSickLeavesResponseModel.doctorImageURL!,
url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientSickLeavesResponseModel.doctorImageURL,
iconColor: AppColors.transparent,
width: 30.h,
height: 30.h,

@ -242,7 +242,7 @@ class _MedicalReportsPageState extends State<MedicalReportsPage> {
mainAxisSize: MainAxisSize.min,
children: [
Utils.buildImgWithNetwork(
url: report.doctorImageURL!,
url: report.doctorImageURL ?? "",
iconColor: AppColors.transparent,
width: 24.h,
height: 24.h,

@ -43,7 +43,7 @@ class PatientMedicalReportCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.buildImgWithNetwork(
url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientMedicalReportResponseModel.doctorImageURL!,
url: isLoading ? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" : patientMedicalReportResponseModel.doctorImageURL,
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,

@ -16,6 +16,7 @@ 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/custom_tab_bar.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:provider/provider.dart';
class FamilyMedicalScreen extends StatefulWidget {
@ -32,9 +33,15 @@ class _FamilyMedicalScreenState extends State<FamilyMedicalScreen> {
void initState() {
super.initState();
medicalVM = context.read<MedicalFileViewModel>();
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
if(medicalVM!.isFamilyFilesLoading) {
LoaderBottomSheet.showLoader();
} else {
LoaderBottomSheet.hideLoader();
}
medicalVM?.onFamilyFileTabChange(0);
medicalVM?.getAllPendingRecordsByResponseId();
});
}
@ -90,14 +97,22 @@ class _FamilyMedicalScreenState extends State<FamilyMedicalScreen> {
),
appState.isChildLoggedIn ? SizedBox() : SizedBox(height: 25.h),
Selector<MedicalFileViewModel, ({int selectedIndex, List<FamilyFileResponseModelLists> patientFiles, List<FamilyFileResponseModelLists> pendingFiles})>(
selector: (_, model) => (selectedIndex: model.getSelectedFamilyFileTabIndex, patientFiles: model.patientFamilyFiles, pendingFiles: model.pendingFamilyFiles),
selector: (_, model) => (
selectedIndex: model.getSelectedFamilyFileTabIndex,
patientFiles: model.patientFamilyFiles,
pendingFiles: model.pendingFamilyFiles,
),
shouldRebuild: (previous, next) {
// Only rebuild if something actually changed
return previous.selectedIndex != next.selectedIndex ||
!identical(previous.patientFiles, next.patientFiles) ||
!identical(previous.pendingFiles, next.pendingFiles);
},
builder: (context, data, child) => getFamilyTabs(index: data.selectedIndex, patientFiles: data.patientFiles, pendingFiles: data.pendingFiles),
builder: (context, data, child) => getFamilyTabs(
index: data.selectedIndex,
patientFiles: data.patientFiles,
pendingFiles: data.pendingFiles,
),
),
SizedBox(height: 20.h),
],
@ -112,6 +127,7 @@ class _FamilyMedicalScreenState extends State<FamilyMedicalScreen> {
}) {
switch (index) {
case 0:
return FamilyCards(
profiles: patientFiles,
onSelect: (FamilyFileResponseModelLists profile) {

@ -1,10 +1,13 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenshot_blocker/flutter_screenshot_blocker.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/size_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/notifications/models/resp_models/notification_response_model.dart';
import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
@ -20,28 +23,39 @@ class NotificationDetailsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CollapsingListView(
title: LocaleKeys.notificationDetails.tr(),
// trailing: IconButton(
// icon: Icon(
// Icons.share_outlined,
// size: 24.h,
// color: AppColors.textColor,
// ),
// onPressed: () {
// _shareNotification();
// },
// ),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24.h),
// Notification content card
_buildNotificationCard(context),
SizedBox(height: 24.h),
],
).paddingSymmetrical(24.w, 0.h),
getIt.get<NotificationsViewModel>().markAsRead(notificationID: notification.id ?? 0);
return ScreenshotBlockerWidget(
blockScreenshots: true, // makes this widget appear black in screenshots
detectScreenshots: true, // listen for screenshot attempts
onScreenshotDetected: (event) {
// event.type → 'screenshot_taken' or 'screenshot_blocked'
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(content: Text('Screenshots are not allowed!')),
// );
},
child: CollapsingListView(
title: LocaleKeys.notificationDetails.tr(),
// trailing: IconButton(
// icon: Icon(
// Icons.share_outlined,
// size: 24.h,
// color: AppColors.textColor,
// ),
// onPressed: () {
// _shareNotification();
// },
// ),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24.h),
// Notification content card
_buildNotificationCard(context),
SizedBox(height: 24.h),
],
).paddingSymmetrical(24.w, 0.h),
),
),
);
}
@ -109,9 +123,9 @@ class NotificationDetailsPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.messageNotification.tr().toText14(
weight: FontWeight.w600,
color: AppColors.greyTextColor,
),
weight: FontWeight.w600,
color: AppColors.greyTextColor,
),
SizedBox(height: 8.h),
notification.message!.toText16(
weight: FontWeight.w400,
@ -123,8 +137,7 @@ class NotificationDetailsPage extends StatelessWidget {
),
// Notification image (if MessageType is "image")
if (notification.messageType != null &&
notification.messageType!.toLowerCase() == "image")
if (notification.messageType != null && notification.messageType!.toLowerCase() == "image")
Builder(
builder: (context) {
// Try to get image URL from videoURL or messageTypeData
@ -147,9 +160,9 @@ class NotificationDetailsPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.attachedImage.tr().toText14(
weight: FontWeight.w600,
color: AppColors.greyTextColor,
),
weight: FontWeight.w600,
color: AppColors.greyTextColor,
),
SizedBox(height: 8.h),
ClipRRect(
borderRadius: BorderRadius.circular(12.h),
@ -177,8 +190,8 @@ class NotificationDetailsPage extends StatelessWidget {
),
SizedBox(height: 8.h),
LocaleKeys.failedToLoadImage.tr().toText12(
color: AppColors.greyTextColor,
),
color: AppColors.greyTextColor,
),
SizedBox(height: 4.h),
Text(
imageUrl!,
@ -205,10 +218,7 @@ class NotificationDetailsPage extends StatelessWidget {
),
child: Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null,
),
),
);
@ -249,7 +259,8 @@ ${notification.message ?? 'Notification'}
Time: ${_formatTime(notification.isSentOn)}
Date: ${_formatDate(notification.isSentOn)}
${notification.notificationType != null ? '\nType: ${notification.notificationType}' : ''}
'''.trim();
'''
.trim();
await Share.share(shareText);
}
@ -274,4 +285,3 @@ ${notification.notificationType != null ? '\nType: ${notification.notificationTy
}
}
}

@ -49,186 +49,176 @@ class _NotificationsListPageState extends State<NotificationsListPage> {
return CollapsingListView(
title: LocaleKeys.notification.tr(context: context),
child: Consumer<NotificationsViewModel>(builder: (context, notificationsVM, child) {
return Column(
children: [
Container(
margin: EdgeInsets.symmetric(vertical: 24.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
),
child: ListView.builder(
itemCount: notificationsVM.isNotificationsLoading
? 4
: notificationsVM.notificationsList.length + (notificationsVM.hasMoreNotifications ? 1 : 0),
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsetsGeometry.zero,
itemBuilder: (context, index) {
if (notificationsVM.isNotificationsLoading) {
return LabResultItemView(
onTap: () {},
labOrder: null,
index: index,
isLoading: true,
);
}
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: GestureDetector(
onTap: () {
notificationsVM.markAsRead(notificationID: notificationsVM.notificationsList[index].id ?? 0);
return Column(
children: [
Container(
margin: EdgeInsets.symmetric(vertical: 24.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.h,
hasShadow: false,
),
child: ListView.builder(
itemCount: notificationsVM.isNotificationsLoading
? 4
// : notificationsVM.notificationsList.length + (notificationsVM.hasMoreNotifications ? 1 : 0),
: notificationsVM.notificationsList.length,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsetsGeometry.zero,
itemBuilder: (context, index) {
if (notificationsVM.isNotificationsLoading) {
return LabResultItemView(
onTap: () {},
labOrder: null,
index: index,
isLoading: true,
);
}
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: GestureDetector(
onTap: () {
notificationsVM.markAsRead(notificationID: notificationsVM.notificationsList[index].id ?? 0);
getIt.get<TodoSectionViewModel>().getPatientDashboard();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NotificationDetailsPage(
notification: notificationsVM.notificationsList[index],
context,
MaterialPageRoute(
builder: (context) => NotificationDetailsPage(
notification: notificationsVM.notificationsList[index],
),
),
);
},
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
// Message row with red dot for unread
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: notificationsVM.notificationsList[index].message!.toText16(
isBold: (notificationsVM.notificationsList[index].isRead == false),
weight: (notificationsVM.notificationsList[index].isRead == false) ? FontWeight.w600 : FontWeight.w400,
),
),
SizedBox(width: 8.w),
// Red dot for unread notifications ONLY
if (notificationsVM.notificationsList[index].isRead == false)
Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
);
},
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
// Message row with red dot for unread
Row(
crossAxisAlignment: CrossAxisAlignment.start,
],
),
SizedBox(height: 12.h),
// First row: Time and Date chips with arrow
Row(
children: [
// Time chip with clock icon
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: AppColors.greyColor,
borderRadius: BorderRadius.circular(8),
),
child: Directionality(
textDirection: ui.TextDirection.ltr,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: notificationsVM.notificationsList[index].message!.toText16(
isBold: (notificationsVM.notificationsList[index].isRead == false),
weight: (notificationsVM.notificationsList[index].isRead == false)
? FontWeight.w600
: FontWeight.w400,
),
),
SizedBox(width: 8.w),
// Red dot for unread notifications ONLY
if (notificationsVM.notificationsList[index].isRead == false)
Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
Icon(Icons.access_time, size: 12.w, color: AppColors.textColor),
SizedBox(width: 4.w),
_formatTime(notificationsVM.notificationsList[index].isSentOn).toText10(weight: FontWeight.w600, color: AppColors.textColor, isEnglishOnly: true),
],
),
SizedBox(height: 12.h),
// First row: Time and Date chips with arrow
Row(
),
),
SizedBox(width: 8.w),
// Date chip with calendar icon
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: AppColors.greyColor,
borderRadius: BorderRadius.circular(8),
),
child: Directionality(
textDirection: ui.TextDirection.ltr,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Time chip with clock icon
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: AppColors.greyColor,
borderRadius: BorderRadius.circular(8),
),
child: Directionality(
textDirection: ui.TextDirection.ltr,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.access_time, size: 12.w, color: AppColors.textColor),
SizedBox(width: 4.w),
_formatTime(notificationsVM.notificationsList[index].isSentOn).toText10(
weight: FontWeight.w600,
color: AppColors.textColor,
isEnglishOnly: true
),
],
),
),
),
SizedBox(width: 8.w),
// Date chip with calendar icon
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: AppColors.greyColor,
borderRadius: BorderRadius.circular(8),
),
child: Directionality(
textDirection: ui.TextDirection.ltr,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.calendar_today, size: 12.w, color: AppColors.textColor),
SizedBox(width: 4.w),
_formatDate(notificationsVM.notificationsList[index].isSentOn).toText10(
weight: FontWeight.w600,
color: AppColors.textColor,
isEnglishOnly: true
),
],
),
),
),
Spacer(),
// Arrow icon
Transform.flip(
flipX: getIt.get<AppState>().isArabic(),
child: Utils.buildSvgWithAssets(
icon: AppAssets.arrow_forward,
width: 16.w,
height: 16.h,
iconColor: AppColors.greyTextColor,
),
),
Icon(Icons.calendar_today, size: 12.w, color: AppColors.textColor),
SizedBox(width: 4.w),
_formatDate(notificationsVM.notificationsList[index].isSentOn).toText10(weight: FontWeight.w600, color: AppColors.textColor, isEnglishOnly: true),
],
),
// Second row: Contains Image chip (if MessageType is "image")
if (notificationsVM.notificationsList[index].messageType != null &&
notificationsVM.notificationsList[index].messageType!.toLowerCase() == "image")
Padding(
padding: EdgeInsets.only(top: 8.h),
child: Row(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: AppColors.greyColor,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.image_outlined, size: 12.w, color: AppColors.textColor),
SizedBox(width: 4.w),
LocaleKeys.containsImage.toText10(
weight: FontWeight.w600,
color: AppColors.textColor,
),
],
),
),
],
),
),
),
Spacer(),
// Arrow icon
Transform.flip(
flipX: getIt.get<AppState>().isArabic(),
child: Utils.buildSvgWithAssets(
icon: AppAssets.arrow_forward,
width: 16.w,
height: 16.h,
iconColor: AppColors.greyTextColor,
),
),
],
),
// Second row: Contains Image chip (if MessageType is "image")
if (notificationsVM.notificationsList[index].messageType != null && notificationsVM.notificationsList[index].messageType!.toLowerCase() == "image")
Padding(
padding: EdgeInsets.only(top: 8.h),
child: Row(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: AppColors.greyColor,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.image_outlined, size: 12.w, color: AppColors.textColor),
SizedBox(width: 4.w),
LocaleKeys.containsImage.toText10(
weight: FontWeight.w600,
color: AppColors.textColor,
),
],
),
SizedBox(height: 16.h),
1.divider,
),
],
),
),
),
),
SizedBox(height: 16.h),
1.divider,
],
),
);
}).paddingSymmetrical(16.w, 0.h),
).paddingSymmetrical(24.w, 0.h),
SizedBox(height: 16.h),
),
),
),
),
);
}).paddingSymmetrical(16.w, 0.h),
).paddingSymmetrical(24.w, 0.h),
SizedBox(height: 16.h),
// SizedBox(
// key: _bottomKey,
// child: "Show more notifications".toText16(
@ -241,9 +231,9 @@ class _NotificationsListPageState extends State<NotificationsListPage> {
// _scrollToBottom();
// }),
SizedBox(height: 24.h),
],
);
}),
],
);
}),
);
}

@ -94,7 +94,7 @@ class OffersAndDiscountsDetailedPage extends StatelessWidget {
ClipRRect(
borderRadius: BorderRadius.circular(24.h),
child: Utils.buildImgWithNetwork(
url: offer.imageUrl!,
url: offer.imageUrl,
iconColor: AppColors.blackBgColor,
width: double.infinity,
height: 250.h,

@ -159,7 +159,7 @@ class _OffersAndDiscountsPageState extends State<OffersAndDiscountsPage> {
borderRadius: BorderRadius.vertical(top: Radius.circular(24.h)),
child: SizedBox.expand(
child: Utils.buildImgWithNetwork(
url: offersAndDiscountVM.filteredOffers[index].imageUrl!,
url: offersAndDiscountVM.filteredOffers[index].imageUrl,
iconColor: AppColors.blackBgColor,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {

@ -62,7 +62,7 @@ class PrescriptionDeliveryOrderSummaryPage extends StatelessWidget {
Radius.circular(5.r),
),
child: Utils.buildImgWithNetwork(
url: prescriptionsViewModel.prescriptionDetailsList[index].imageSRCUrl!,
url: prescriptionsViewModel.prescriptionDetailsList[index].imageSRCUrl,
iconColor: AppColors.blackBgColor,
fit: BoxFit.cover,
width: 60.w,

@ -126,7 +126,7 @@ class _PrescriptionDetailPageState extends State<PrescriptionDetailPage> {
mainAxisSize: MainAxisSize.min,
children: [
Utils.buildImgWithNetwork(
url: widget.prescriptionsResponseModel.doctorImageURL!,
url: widget.prescriptionsResponseModel.doctorImageURL,
iconColor: AppColors.blackBgColor,
width: 24.h,
height: 24.h,

@ -43,7 +43,7 @@ class PrescriptionItemView extends StatelessWidget {
spacing: 8.h,
children: [
Utils.buildImgWithNetwork(
url: isLoading ? "" : prescriptionVM.prescriptionDetailsList[index].imageThumbUrl!,
url: isLoading ? "" : prescriptionVM.prescriptionDetailsList[index].imageThumbUrl,
iconColor: AppColors.blackBgColor,
width: 60.h,
height: 60.h,

@ -151,7 +151,7 @@ class _PrescriptionsListPageState extends State<PrescriptionsListPage> {
mainAxisSize: MainAxisSize.min,
children: [
Utils.buildImgWithNetwork(
url: model.patientPrescriptionOrders[index].doctorImageURL!,
url: model.patientPrescriptionOrders[index].doctorImageURL,
iconColor: AppColors.blackBgColor,
width: 24.h,
height: 24.h,

@ -42,6 +42,8 @@ import 'package:in_app_review/in_app_review.dart';
import 'dart:ui' as ui;
import 'widgets/patient_qr_code_widget.dart';
class ProfileSettings extends StatefulWidget {
const ProfileSettings({super.key});
@ -232,6 +234,17 @@ class ProfileSettingsState extends State<ProfileSettings> {
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false),
child: Column(
children: [
actionItem(AppAssets.patientQRCodeIcon, LocaleKeys.viewPatientQR.tr(context: context), () {
showCommonBottomSheetWithoutHeight(
title: LocaleKeys.patientQR.tr(context: context),
context,
child: PatientQrCodeWidget(),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
}),
1.divider,
actionItem(AppAssets.language_change, LocaleKeys.language.tr(context: context), () {
showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.language.tr(context: context), child: AppLanguageChange(), callBackFunc: () {}, isFullScreen: false);
}, trailingLabel: Utils.appState.isArabic() ? "العربية" : "English"),

@ -0,0 +1,56 @@
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/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart';
import '../../../generated/locale_keys.g.dart';
class PatientQrCodeWidget extends StatelessWidget {
PatientQrCodeWidget({super.key});
late AppState appState;
@override
Widget build(BuildContext context) {
appState = getIt.get<AppState>();
return Column(
children: [
SizedBox(height: 8.h),
"${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText18(isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 2),
SizedBox(height: 8.h),
Wrap(
direction: Axis.horizontal,
spacing: 4.w,
runSpacing: 6.w,
children: [
AppCustomChipWidget(
icon: AppAssets.file_icon,
labelText: "${LocaleKeys.fileno.tr(context: context)}: ${appState.getAuthenticatedUser()!.patientId}",
labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w),
),
AppCustomChipWidget(
icon: AppAssets.checkmark_icon,
labelText: LocaleKeys.verified.tr(context: context),
iconColor: AppColors.successColor,
labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w),
),
],
),
SizedBox(height: 16.h),
Image.network(
"https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${appState.getAuthenticatedUser()!.patientId.toString()}",
fit: BoxFit.fill,
height: 156.h,
width: 156.h,
),
SizedBox(height: 16.h),
],
);
}
}

@ -74,7 +74,7 @@ class _PreferredLanguageWidgetState extends State<PreferredLanguageWidget> {
callBackFunc: () async {
Navigator.of(GetIt.instance<NavigationService>().navigatorKey.currentContext!).pop();
profileSettingsViewModel.getProfileSettings();
}, isFullScreen: false, isAutoDismiss: true);
}, isFullScreen: false, isAutoDismiss: true, isCloseButtonVisible: false);
},
onError: (error) {
LoaderBottomSheet.hideLoader();

@ -134,7 +134,7 @@ class _UpdateEmailDialogState extends State<UpdateEmailDialog> {
callBackFunc: () async {
Navigator.of(getIt<NavigationService>().navigatorKey.currentContext!).pop();
profileSettingsViewModel!.getProfileSettings();
}, isFullScreen: false, isAutoDismiss: true);
}, isFullScreen: false, isAutoDismiss: true, isCloseButtonVisible: false);
},
onError: (error) {
LoaderBottomSheet.hideLoader();

@ -41,7 +41,7 @@ class _UpdateEmergencyContactDialogState extends State<UpdateEmergencyContactDia
// Set the text
setState(() {
textController!.text = viewModel.getPatientInfoForUpdate!.emergencyContactNo ?? "";
textController!.text = viewModel.getPatientInfoForUpdate != null ? viewModel.getPatientInfoForUpdate!.emergencyContactNo ?? "" : "";
});
});

@ -25,7 +25,7 @@ class BuildDoctorRow extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Utils.buildImgWithNetwork(
url: isForClinic ? 'https://hmgwebservices.com/Images/Hospitals/${appointmentDetails!.projectID}.jpg' : appointmentDetails!.doctorImageURL ,
url: isForClinic ? 'https://hmgwebservices.com/Images/Hospitals/${appointmentDetails?.projectID}.jpg' : appointmentDetails?.doctorImageURL ,
iconColor: AppColors.transparent,
width: 63.h,
height: 63.h,

@ -29,11 +29,19 @@ class OrganSelectorPage extends StatefulWidget {
State<OrganSelectorPage> createState() => _OrganSelectorPageState();
}
class _OrganSelectorPageState extends State<OrganSelectorPage> {
class _OrganSelectorPageState extends State<OrganSelectorPage> with SingleTickerProviderStateMixin {
static const double _expandedBodyZoomFactor = 1.08;
static const double _expandedSheetHeightFactor = 0.3;
static const bool _enableTutorialOverlay = false;
static const bool _enableSwipeToFlip = false;
late final AppState _appState;
late final DialogService dialogService;
late final CacheService cacheService;
late final AnimationController _introZoomHintController;
late final Animation<double> _introZoomHintAnimation;
bool _showTutorial = false;
bool _hasPlayedIntroZoomHint = false;
@override
void initState() {
@ -41,10 +49,52 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
_appState = getIt.get<AppState>();
dialogService = getIt<DialogService>();
cacheService = getIt<CacheService>();
_introZoomHintController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
)..addListener(() {
if (mounted) setState(() {});
});
_introZoomHintAnimation = TweenSequence<double>([
TweenSequenceItem(
tween: Tween<double>(begin: 1.0, end: 1.25),
weight: 45,
),
TweenSequenceItem(
tween: Tween<double>(begin: 1.25, end: 1.0),
weight: 55,
),
]).animate(CurvedAnimation(
parent: _introZoomHintController,
curve: Curves.easeInOut,
));
_checkAndShowTutorial();
_playIntroZoomHint();
}
@override
void dispose() {
_introZoomHintController.dispose();
super.dispose();
}
Future<void> _playIntroZoomHint() async {
if (_hasPlayedIntroZoomHint) return;
_hasPlayedIntroZoomHint = true;
await Future.delayed(const Duration(milliseconds: 700));
if (!mounted) return;
_introZoomHintController.forward(from: 0);
}
Future<void> _checkAndShowTutorial() async {
if (!_enableTutorialOverlay) {
_showTutorial = false;
return;
}
// final hasSeenTutorial = cacheService.getBool(key: CacheConst.organSelectorTutorialShown) ?? false;
// if (!hasSeenTutorial) {
// Show tutorial after a short delay to ensure the screen is fully built
@ -118,7 +168,11 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
Expanded(
child: Stack(
children: [
_buildBodyViewer(viewModel),
SafeArea(
top: false,
bottom: false,
child: _buildBodyViewer(viewModel),
),
_buildViewToggleButtons(viewModel),
// _buildViewZoomButtons(viewModel),
_buildBottomSheet(viewModel),
@ -131,7 +185,7 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
),
),
// Tutorial overlay
if (_showTutorial)
if (_enableTutorialOverlay && _showTutorial)
PinchZoomTutorialOverlay(
onComplete: _onTutorialComplete,
),
@ -152,7 +206,13 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
height: 24.h,
),
padding: EdgeInsetsDirectional.only(start: 12, end: 12),
onPressed: () => Navigator.pop(context),
onPressed: () {
// Clear selected organs and sheet status when going back
final viewModel = context.read<SymptomsCheckerViewModel>();
viewModel.clearAllSelections();
viewModel.setBottomSheetExpanded(false);
Navigator.pop(context);
},
highlightColor: Colors.transparent,
),
),
@ -175,45 +235,82 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
Widget _buildBodyViewer(SymptomsCheckerViewModel viewModel) {
return GestureDetector(
onHorizontalDragEnd: (details) {
// Swipe left or right to toggle view
if (details.primaryVelocity != null) {
if (details.primaryVelocity! < -200 || details.primaryVelocity! > 200) {
viewModel.toggleView();
}
}
},
child: Padding(
padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 60.h),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 600),
transitionBuilder: (child, animation) => _build3DFlipTransition(child, animation),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
child: Builder(
key: ValueKey<BodyView>(viewModel.currentView),
builder: (context) {
// Detect female gender from viewModel; allow Arabic value as fallback
final bool isFemale =
(viewModel.selectedGender != null && (viewModel.selectedGender!.toLowerCase() == 'female' || viewModel.selectedGender == 'أنثى'));
final String bodyAsset = viewModel.currentView == BodyView.front
? (isFemale ? AppAssets.fullBodyFrontFemale : AppAssets.fullBodyFrontMale)
: (isFemale ? AppAssets.fullBodyBackFemale : AppAssets.fullBodyBackMale);
return InteractiveBodyWidget(
bodyImageAsset: bodyAsset,
organs: viewModel.currentOrgans,
selectedOrganIds: viewModel.selectedOrganIds,
onOrganTap: viewModel.toggleOrganSelection,
isBodyHidden: viewModel.isBodyHidden,
tooltipOrganId: viewModel.tooltipOrganId,
isArabic: _appState.isArabic(),
zoomScale: viewModel.currentZoomScale,
);
onHorizontalDragEnd: !_enableSwipeToFlip
? null
: (details) {
// Swipe left or right to toggle view
if (details.primaryVelocity != null) {
if (details.primaryVelocity! < -200 || details.primaryVelocity! > 200) {
viewModel.toggleView();
}
}
},
),
),
child: LayoutBuilder(
builder: (context, constraints) {
final bool isExpanded = viewModel.isBottomSheetExpanded;
final double screenHeight = MediaQuery.of(context).size.height;
final double extraBottomInset = isExpanded ? screenHeight * _expandedSheetHeightFactor : 0;
final bool isUserZoomedIn = viewModel.currentZoomScale > 1.01;
Widget buildInteractiveBody({required bool useExpandedZoom}) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 600),
transitionBuilder: (child, animation) => _build3DFlipTransition(child, animation),
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
child: Builder(
key: ValueKey<BodyView>(viewModel.currentView),
builder: (context) {
final bool isFemale = (viewModel.selectedGender != null && (viewModel.selectedGender!.toLowerCase() == 'female' || viewModel.selectedGender == 'أنثى'));
final double effectiveZoomScale = viewModel.currentZoomScale * _introZoomHintAnimation.value;
final String bodyAsset = viewModel.currentView == BodyView.front
? (isFemale ? AppAssets.fullBodyFrontFemale : AppAssets.fullBodyFrontMale)
: (isFemale ? AppAssets.fullBodyBackFemale : AppAssets.fullBodyBackMale);
final body = InteractiveBodyWidget(
bodyImageAsset: bodyAsset,
organs: viewModel.currentOrgans,
selectedOrganIds: viewModel.selectedOrganIds,
onOrganTap: viewModel.toggleOrganSelection,
isBodyHidden: viewModel.isBodyHidden,
tooltipOrganId: viewModel.tooltipOrganId,
isArabic: _appState.isArabic(),
zoomScale: effectiveZoomScale,
);
return body;
},
),
);
}
if (!isExpanded) {
return Padding(
padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 60.h),
child: buildInteractiveBody(useExpandedZoom: false),
);
}
return SingleChildScrollView(
physics: isExpanded && !isUserZoomedIn ? const ClampingScrollPhysics() : const NeverScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight + extraBottomInset,
),
child: Padding(
padding: EdgeInsets.only(
bottom: extraBottomInset,
top: 24.h,
),
child: Padding(
padding: EdgeInsets.fromLTRB(16.h, 16.h, 16.h, 60.h),
child: buildInteractiveBody(useExpandedZoom: true),
),
),
),
);
},
),
);
}
@ -408,37 +505,73 @@ class _OrganSelectorPageState extends State<OrganSelectorPage> {
}
Widget _buildExpandCollapseButton(SymptomsCheckerViewModel viewModel) {
final organCount = viewModel.selectedOrgans.length;
final showBadge = !viewModel.isBottomSheetExpanded && organCount > 0;
return PositionedDirectional(
end: 24.w,
top: -24.h,
child: GestureDetector(
onTap: viewModel.toggleBottomSheet,
behavior: HitTestBehavior.opaque,
child: Container(
child: SizedBox(
width: 70.w,
height: 70.h,
alignment: Alignment.center,
child: Container(
width: 48.w,
height: 48.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 11.r,
),
child: Center(
child: Transform.flip(
flipX: _appState.isArabic(),
child: AnimatedRotation(
duration: const Duration(milliseconds: 300),
turns: viewModel.isBottomSheetExpanded ? 0.25 : -0.25,
child: Utils.buildSvgWithAssets(
icon: AppAssets.arrowRight,
width: 25.w,
height: 25.h,
child: Stack(
clipBehavior: Clip.none,
children: [
Align(
alignment: Alignment.center,
child: Container(
width: 48.w,
height: 48.h,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 11.r,
),
child: Center(
child: Transform.flip(
flipX: _appState.isArabic(),
child: AnimatedRotation(
duration: const Duration(milliseconds: 300),
turns: viewModel.isBottomSheetExpanded ? 0.25 : -0.25,
child: Utils.buildSvgWithAssets(
icon: AppAssets.arrowRight,
width: 25.w,
height: 25.h,
),
),
),
),
),
),
),
if (showBadge)
PositionedDirectional(
top: 8.h,
end: 8.w,
child: Container(
width: 22.w,
height: 22.h,
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppColors.primaryRedColor,
shape: BoxShape.circle,
border: Border.all(
color: AppColors.whiteColor,
width: 1.5,
),
),
child: Text(
'$organCount',
style: TextStyle(
color: AppColors.whiteColor,
fontSize: 10.f,
fontWeight: FontWeight.w700,
),
),
),
),
],
),
),
),

@ -1,3 +1,4 @@
import 'dart:developer';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
@ -7,7 +8,6 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/symptoms_checker/models/organ_model.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/organ_dot_widget.dart';
import 'package:hmg_patient_app_new/presentation/symptoms_checker/widgets/organ_tooltip_widget.dart';
import 'package:vector_math/vector_math_64.dart' show Vector3;
class InteractiveBodyWidget extends StatefulWidget {
final String bodyImageAsset;
@ -63,9 +63,12 @@ class _InteractiveBodyWidgetState extends State<InteractiveBodyWidget> {
@override
void dispose() {
_transformationController.dispose();
currentZoom = 0.0;
super.dispose();
}
double currentZoom = 0.0;
Future<void> _loadImageAspectRatio() async {
final ByteData data = await rootBundle.load(widget.bodyImageAsset);
final ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
@ -77,17 +80,16 @@ class _InteractiveBodyWidgetState extends State<InteractiveBodyWidget> {
_imageAspectRatio = image.width / image.height;
});
}
_transformationController.addListener(() {
currentZoom = _transformationController.value.getMaxScaleOnAxis();
setState(() {});
});
}
void _updateZoom(double scale) {
// Get current translation
final currentTransform = _transformationController.value;
final currentTranslation = currentTransform.getTranslation();
// Create new transformation with updated scale while preserving translation
final newTransform = Matrix4.identity()
..setTranslation(currentTranslation)
..scaleByVector3(Vector3(scale, scale, 1.0));
// Keep programmatic zoom centered to avoid drifting the body off-screen.
final newTransform = Matrix4.identity()..scaleByDouble(scale, scale, 1.0, 1.0);
_transformationController.value = newTransform;
}
@ -103,8 +105,12 @@ class _InteractiveBodyWidgetState extends State<InteractiveBodyWidget> {
return Center(
child: InteractiveViewer(
transformationController: _transformationController,
minScale: 0.5,
maxScale: 4.0,
alignment: Alignment.center,
panEnabled: true,
scaleEnabled: true,
boundaryMargin: currentZoom > 1.2 ? EdgeInsets.all(200.h) : EdgeInsets.zero,
minScale: 1.0,
maxScale: 9.0,
clipBehavior: Clip.none,
child: AspectRatio(
aspectRatio: _imageAspectRatio!,
@ -135,7 +141,7 @@ class _InteractiveBodyWidgetState extends State<InteractiveBodyWidget> {
// Organ dots
...widget.organs.map((organ) {
final isSelected = widget.selectedOrganIds.contains(organ.id);
final dotSize = 16.0;
final dotSize = 18.0;
final leftPos = (organ.position.x * imageConstraints.maxWidth) - (dotSize / 2);
final topPos = (organ.position.y * imageConstraints.maxHeight) - (dotSize / 2);

@ -132,12 +132,29 @@ class _AncillaryOrderPaymentPageState extends State<AncillaryOrderPaymentPage> {
if (!todoVM.isProcessingPayment) {
selectedPaymentMethod = "MADA";
if (appState.isPaytabsEnabled) {
transID = Utils.getAdvancePaymentTransID(
widget.projectID,
appState.getAuthenticatedUser()!.patientId!,
);
paytabsViewModel.setPaymentConfiguration(
"Ancillary Orders Payment",
widget.totalAmount,
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
final paymentAmount = widget.totalAmount;
paytabsViewModel.startCardPayment(
paymentOption: selectedPaymentMethod,
liveServiceID: "0",
isScheduleLiveCare: "false",
appointmentDate: DateUtil.convertDateToString(DateTime.now()),
appointmentNo: "0",
doctorID: "0",
transactionID: transID,
amount: widget.totalAmount.toString(),
fileNumber: appState.getAuthenticatedUser()!.patientId!.toString(),
orderDescription: "Ancillary Orders Payment",
serviceID: ServiceTypeEnum.advancePayment.getIdFromServiceEnum().toString(),
projectID: widget.projectID.toString(),
onSuccess: (PaytabsTransactionResponseModel transactionData) async {
final paymentAmount = widget.totalAmount;
final fortId = transactionData.transactionReference;
final paymentMethod = selectedPaymentMethod;
@ -254,11 +271,27 @@ class _AncillaryOrderPaymentPageState extends State<AncillaryOrderPaymentPage> {
if (!todoVM.isProcessingPayment) {
selectedPaymentMethod = "VISA";
if (appState.isPaytabsEnabled) {
transID = Utils.getAdvancePaymentTransID(
widget.projectID,
appState.getAuthenticatedUser()!.patientId!,
);
paytabsViewModel.setPaymentConfiguration(
"Ancillary Orders Payment",
widget.totalAmount,
);
paytabsViewModel.startCardPayment(onSuccess: (PaytabsTransactionResponseModel transactionData) async {
paytabsViewModel.startCardPayment(paymentOption: selectedPaymentMethod,
liveServiceID: "0",
isScheduleLiveCare: "false",
appointmentDate: DateUtil.convertDateToString(DateTime.now()),
appointmentNo: "0",
doctorID: "0",
transactionID: transID,
amount: widget.totalAmount.toString(),
fileNumber: appState.getAuthenticatedUser()!.patientId!.toString(),
orderDescription: "Ancillary Orders Payment",
serviceID: ServiceTypeEnum.advancePayment.getIdFromServiceEnum().toString(),
projectID: widget.projectID.toString(),
onSuccess: (PaytabsTransactionResponseModel transactionData) async {
final paymentAmount = widget.totalAmount;
final fortId = transactionData.transactionReference;
final paymentMethod = selectedPaymentMethod;

@ -23,7 +23,8 @@ abstract class DialogService {
Future<void> showExceptionBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed});
Future<void> showCommonBottomSheetWithoutH({String? label, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed});
Future<void> showCommonBottomSheetWithoutH(
{String? label, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed});
Future<void> showSuccessBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed});
@ -119,21 +120,15 @@ class DialogServiceImp implements DialogService {
}
@override
Future<void> showCommonBottomSheetWithoutH({String? label, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}) async {
Future<void> showCommonBottomSheetWithoutH(
{String? label, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}) async {
final context = navigationService.navigatorKey.currentContext;
if (context == null) return;
showCommonBottomSheetWithoutHeight(
context,
title: label ?? "",
child: exceptionBottomSheetWidget(
context: context,
message: message,
okLabel: okLabel,
cancelLabel: cancelLabel,
onOkPressed: onOkPressed,
onCancelPressed: onCancelPressed,
isConfirmButton: isConfirmButton
),
context: context, message: message, okLabel: okLabel, cancelLabel: cancelLabel, onOkPressed: onOkPressed, onCancelPressed: onCancelPressed, isConfirmButton: isConfirmButton),
callBackFunc: () {},
);
}
@ -240,7 +235,8 @@ class DialogServiceImp implements DialogService {
}
}
Widget exceptionBottomSheetWidget({required BuildContext context, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}) {
Widget exceptionBottomSheetWidget(
{required BuildContext context, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}) {
return Column(
children: [
(message).toText16(isBold: false, color: AppColors.textColor),

@ -6,6 +6,7 @@ import 'package:hmg_patient_app_new/routes/app_routes.dart';
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
final RouteObserver<PageRoute<dynamic>> routeObserver = RouteObserver<PageRoute<dynamic>>();
BuildContext? get context => navigatorKey.currentContext;

@ -16,10 +16,7 @@ class AppTheme {
visualDensity: VisualDensity.adaptivePlatformDensity,
brightness: Brightness.light,
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: ZoomPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder()
},
builders: {TargetPlatform.android: ZoomPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder()},
),
hintColor: Colors.grey[400],
disabledColor: Colors.grey[300],
@ -52,10 +49,7 @@ class AppTheme {
visualDensity: VisualDensity.adaptivePlatformDensity,
brightness: Brightness.dark,
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: ZoomPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder()
},
builders: {TargetPlatform.android: ZoomPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder()},
),
hintColor: Colors.grey[600],
disabledColor: Colors.grey[700],
@ -64,8 +58,7 @@ class AppTheme {
scaffoldBackgroundColor: AppColors.dark.scaffoldBgColor,
highlightColor: Colors.grey[800]!.withOpacity(0.4),
splashColor: Colors.transparent,
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: Colors.black.withOpacity(0)),
bottomSheetTheme: BottomSheetThemeData(backgroundColor: Colors.black.withOpacity(0)),
floatingActionButtonTheme: const FloatingActionButtonThemeData(highlightElevation: 2, disabledElevation: 0, elevation: 2),
appBarTheme: AppBarTheme(
color: AppColors.dark.scaffoldBgColor,

@ -43,7 +43,8 @@ class AttachmentOptions extends StatelessWidget {
).onPress(() async {
await onGalleryTap();
}),
if (!showFilesOption) SizedBox(height: 16.h),
// if (!showFilesOption) commented because the ui issue.
SizedBox(height: 16.h),
if (showFilesOption)
checkInOptionCard(
appState,

@ -6,7 +6,7 @@ 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/theme/colors.dart';
class CustomButton extends StatelessWidget {
class CustomButton extends StatefulWidget {
final String text;
final String? icon;
final VoidCallback? onPressed;
@ -54,25 +54,61 @@ class CustomButton extends StatelessWidget {
this.applyThemeColor = true,
});
@override
State<CustomButton> createState() => _CustomButtonState();
}
class _CustomButtonState extends State<CustomButton> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 80),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
Future<void> _playTapAnimation() async {
if (!widget.isDisabled && widget.onPressed != null) {
await _animationController.forward();
_animationController.reverse();
}
}
@override
Widget build(BuildContext context) {
final radius = borderRadius ?? (12.r);
final iconS = iconSize ?? (24.h);
final fontS = fontSize ?? (16.f);
final isButtonDisabled = isDisabled || onPressed == null;
final radius = widget.borderRadius ?? (12.r);
final iconS = widget.iconSize ?? (24.h);
final fontS = widget.fontSize ?? (16.f);
final isButtonDisabled = widget.isDisabled || widget.onPressed == null;
return Semantics(
button: true,
enabled: !isButtonDisabled,
label: text,
hint: isButtonDisabled ? 'Button is disabled' : 'Tap to $text',
child: GestureDetector(
label: widget.text,
hint: isButtonDisabled ? 'Button is disabled' : 'Tap to ${widget.text}',
child: GestureDetector(
onTap: isButtonDisabled
? null
: () {
? null
: () async {
// Play tap animation and wait for it to complete
await _playTapAnimation();
// Log button tap to Clarity before executing callback
try {
Clarity.setCustomTag('button_text', text.length > 100 ? text.substring(0, 100) : text);
Clarity.setCustomTag('button_text', widget.text.length > 100 ? widget.text.substring(0, 100) : widget.text);
Clarity.setCustomTag('button_action', 'tap');
Clarity.setCustomTag('button_disabled', 'false');
Clarity.sendCustomEvent('button_tapped');
@ -81,67 +117,73 @@ class CustomButton extends StatelessWidget {
}
// Execute the actual button callback
onPressed?.call();
widget.onPressed?.call();
},
child: Container(
height: height == null
? 56.h
child: AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) => Transform.scale(
scale: _scaleAnimation.value,
child: child,
),
child: Container(
height: widget.height == null
? 56.h
: (isFoldable || isTablet)
? height! + 10.h
: height,
width: width,
padding: padding,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: isButtonDisabled ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : backgroundColor,
borderRadius: radius,
? widget.height! + 10.h
: widget.height,
width: widget.width,
padding: widget.padding,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: isButtonDisabled ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : widget.backgroundColor,
borderRadius: radius,
customBorder: BorderRadius.circular(radius),
side: borderSide ?? BorderSide(width: borderWidth.h, color: isButtonDisabled ? Colors.transparent : borderColor)),
child: Row(
side: widget.borderSide ?? BorderSide(width: widget.borderWidth.h, color: isButtonDisabled ? Colors.transparent : widget.borderColor)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (icon != null)
Padding(
padding: text.isNotEmpty ? EdgeInsets.only(right: 6.w, left: 6.w) : EdgeInsets.zero,
child: Utils.buildSvgWithAssets(
icon: icon!,
iconColor: isButtonDisabled ? AppColors.greyTextColor : iconColor,
isDisabled: isButtonDisabled,
if (widget.icon != null)
Padding(
padding: widget.text.isNotEmpty ? EdgeInsets.only(right: 6.w, left: 6.w) : EdgeInsets.zero,
child: Utils.buildSvgWithAssets(
icon: widget.icon!,
iconColor: isButtonDisabled ? AppColors.greyTextColor : widget.iconColor,
isDisabled: isButtonDisabled,
width: iconS,
height: iconS,
applyThemeColor: applyThemeColor),
),
if (text.isNotEmpty && icon != null)
Flexible(
applyThemeColor: widget.applyThemeColor),
),
if (widget.text.isNotEmpty && widget.icon != null)
Flexible(
child: Text(
text,
overflow: textOverflow ?? TextOverflow.ellipsis,
maxLines: 1,
widget.text,
overflow: widget.textOverflow ?? TextOverflow.ellipsis,
maxLines: 1,
textAlign: TextAlign.center,
style: context.dynamicTextStyle(
fontSize: fontS,
color: isButtonDisabled ? AppColors.greyTextColor : textColor,
letterSpacing: 0,
fontWeight: isBold ? FontWeight.w700 : fontWeight,
),
color: isButtonDisabled ? AppColors.greyTextColor : widget.textColor,
letterSpacing: 0,
fontWeight: widget.isBold ? FontWeight.w700 : widget.fontWeight,
),
),
),
if (text.isNotEmpty && icon == null)
Text(
text,
overflow: textOverflow ?? TextOverflow.ellipsis,
maxLines: 1,
if (widget.text.isNotEmpty && widget.icon == null)
Text(
widget.text,
overflow: widget.textOverflow ?? TextOverflow.ellipsis,
maxLines: 1,
textAlign: TextAlign.center,
style: context.dynamicTextStyle(
fontSize: fontS,
color: isButtonDisabled ? AppColors.greyTextColor : textColor,
letterSpacing: 0,
fontWeight: isBold ? FontWeight.w700 : fontWeight,
),
color: isButtonDisabled ? AppColors.greyTextColor : widget.textColor,
letterSpacing: 0,
fontWeight: widget.isBold ? FontWeight.w700 : widget.fontWeight,
),
),
],
),
)),
);
));
}
}

@ -257,8 +257,7 @@ void showCommonBottomSheetWithoutHeight(
duration: Duration(milliseconds: 500),
reverseDuration: Duration(milliseconds: 300),
),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width //MediaQuery.of(context).size.width, // Full width
),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width),
context: context,
isScrollControlled: true,
showDragHandle: false,
@ -273,9 +272,9 @@ void showCommonBottomSheetWithoutHeight(
if (navContext != null && Navigator.canPop(navContext)) {
Navigator.of(navContext).pop();
}
if (callBackFunc != null) {
callBackFunc();
}
// if (callBackFunc != null) {
// callBackFunc();
// }
});
}
return SafeArea(

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
@ -9,7 +11,6 @@ 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/generated/locale_keys.g.dart';
import 'dart:ui' as ui;
class CustomCountryDropdown extends StatefulWidget {
final List<CountryEnum> countryList;
@ -35,7 +36,9 @@ class CustomCountryDropdown extends StatefulWidget {
class CustomCountryDropdownState extends State<CustomCountryDropdown> {
CountryEnum? selectedCountry;
late OverlayEntry _overlayEntry;
OverlayEntry? _overlayEntry;
Timer? _showDropdownTimer;
Timer? _refocusTimer;
bool _isDropdownOpen = false;
FocusNode textFocusNode = FocusNode();
@ -55,6 +58,9 @@ class CustomCountryDropdownState extends State<CustomCountryDropdown> {
@override
void dispose() {
_showDropdownTimer?.cancel();
_refocusTimer?.cancel();
_removeOverlayEntry();
textFocusNode.dispose();
super.dispose();
}
@ -74,9 +80,18 @@ class CustomCountryDropdownState extends State<CustomCountryDropdown> {
},
child: Row(
children: [
Utils.buildSvgWithAssets(icon: selectedCountry != null ? selectedCountry!.iconPath : AppAssets.ksa, width: 40.h, height: 40.h, applyThemeColor: false),
Utils.buildSvgWithAssets(
icon: selectedCountry != null
? selectedCountry!.iconPath
: AppAssets.ksa,
width: 40.h,
height: 40.h,
applyThemeColor: false),
SizedBox(width: 8.h),
Utils.buildSvgWithAssets(icon: _isDropdownOpen ? AppAssets.dropdow_icon : AppAssets.dropdow_icon),
Utils.buildSvgWithAssets(
icon: _isDropdownOpen
? AppAssets.dropdow_icon
: AppAssets.dropdow_icon),
],
),
),
@ -94,7 +109,11 @@ class CustomCountryDropdownState extends State<CustomCountryDropdown> {
children: [
Text(
LocaleKeys.phoneNumber.tr(),
style: TextStyle(fontSize: 12.f, height: 1.5, fontWeight: FontWeight.w600, letterSpacing: -1),
style: TextStyle(
fontSize: 12.f,
height: 1.5,
fontWeight: FontWeight.w600,
letterSpacing: -1),
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
@ -102,9 +121,15 @@ class CustomCountryDropdownState extends State<CustomCountryDropdown> {
if (selectedCountry != CountryEnum.others)
Text(
selectedCountry!.countryCode,
style: TextStyle(fontSize: 12.f, fontWeight: FontWeight.w600, letterSpacing: -0.4, height: 1.5, fontFamily: "Poppins"),
style: TextStyle(
fontSize: 12.f,
fontWeight: FontWeight.w600,
letterSpacing: -0.4,
height: 1.5,
fontFamily: "Poppins"),
),
if (selectedCountry != CountryEnum.others) SizedBox(width: 4.h),
if (selectedCountry != CountryEnum.others)
SizedBox(width: 4.h),
if (widget.isEnableTextField)
SizedBox(
height: 20.h,
@ -113,14 +138,19 @@ class CustomCountryDropdownState extends State<CustomCountryDropdown> {
alignment: Alignment.centerLeft,
child: TextField(
focusNode: textFocusNode,
style: TextStyle(fontSize: 12.f, fontWeight: FontWeight.w600, letterSpacing: -0.4, height: 1.5, fontFamily: "Poppins"),
style: TextStyle(
fontSize: 12.f,
fontWeight: FontWeight.w600,
letterSpacing: -0.4,
height: 1.5,
fontFamily: "Poppins"),
decoration: InputDecoration(
hintText: selectedCountry == CountryEnum.others ? "001*******" : "",
hintText: selectedCountry == CountryEnum.others
? "001*******"
: "",
isDense: true,
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
keyboardType: TextInputType.phone,
onChanged: widget.onPhoneNumberChanged,
@ -136,21 +166,31 @@ class CustomCountryDropdownState extends State<CustomCountryDropdown> {
Text(
selectedCountry != null
? appState.getLanguageCode() == "ar"
? selectedCountry!.nameArabic
: selectedCountry!.displayName
? selectedCountry!.nameArabic
: selectedCountry!.displayName
: LocaleKeys.selectCountry.tr(),
style: TextStyle(fontSize: 14.f, height: 21 / 14, fontWeight: FontWeight.w600, letterSpacing: -0.2),
style: TextStyle(
fontSize: 14.f,
height: 21 / 14,
fontWeight: FontWeight.w600,
letterSpacing: -0.2),
),
],
);
}
void _openDropdown() {
if (!mounted || _isDropdownOpen || _overlayEntry != null) return;
_showDropdownTimer?.cancel();
if (textFocusNode.hasFocus) {
textFocusNode.unfocus();
// Wait for keyboard to close before calculating position
Future.delayed(Duration(milliseconds: 300), () {
_showDropdown();
_showDropdownTimer = Timer(const Duration(milliseconds: 300), () {
_showDropdownTimer = null;
if (mounted) {
_showDropdown();
}
});
} else {
_showDropdown();
@ -158,8 +198,18 @@ class CustomCountryDropdownState extends State<CustomCountryDropdown> {
}
void _showDropdown() {
if (!mounted || _isDropdownOpen || _overlayEntry != null) return;
AppState appState = getIt.get<AppState>();
RenderBox renderBox = context.findRenderObject() as RenderBox;
final renderObject = context.findRenderObject();
final overlay = Overlay.maybeOf(context);
if (renderObject is! RenderBox ||
!renderObject.attached ||
overlay == null) {
return;
}
final renderBox = renderObject;
Offset offset = renderBox.localToGlobal(Offset.zero);
bool isRtl = appState.getLanguageCode() == "ar";
double leftPosition;
@ -169,7 +219,7 @@ class CustomCountryDropdownState extends State<CustomCountryDropdown> {
leftPosition = offset.dx;
}
_overlayEntry = OverlayEntry(
final overlayEntry = OverlayEntry(
builder: (context) => Stack(
children: [
Positioned.fill(
@ -185,32 +235,41 @@ class CustomCountryDropdownState extends State<CustomCountryDropdown> {
width: !widget.isFromBottomSheet ? renderBox.size.width : 60.h,
child: Material(
child: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: Colors.white, borderRadius: 12),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: Colors.white, borderRadius: 12),
child: Column(
children: widget.countryList
.map(
(country) => GestureDetector(
onTap: () {
setState(() {
selectedCountry = country;
});
widget.onCountryChange?.call(country);
_closeDropdown();
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(borderRadius: 16.h),
child: Row(
children: [
Utils.buildSvgWithAssets(icon: country.iconPath, width: 38.h, height: 38.h, applyThemeColor: false),
if (!widget.isFromBottomSheet) SizedBox(width: 12.h),
if (!widget.isFromBottomSheet)
Text(appState.getLanguageCode() == "ar" ? country.nameArabic : country.displayName,
style: TextStyle(fontSize: 14.f, height: 21 / 14, fontWeight: FontWeight.w600, letterSpacing: -0.2)),
],
),
)),
)
onTap: () => _selectCountry(country),
child: Container(
padding: EdgeInsets.symmetric(
vertical: 8.h, horizontal: 8.h),
decoration: RoundedRectangleBorder()
.toSmoothCornerDecoration(borderRadius: 16.h),
child: Row(
children: [
Utils.buildSvgWithAssets(
icon: country.iconPath,
width: 38.h,
height: 38.h,
applyThemeColor: false),
if (!widget.isFromBottomSheet)
SizedBox(width: 12.h),
if (!widget.isFromBottomSheet)
Text(
appState.getLanguageCode() == "ar"
? country.nameArabic
: country.displayName,
style: TextStyle(
fontSize: 14.f,
height: 21 / 14,
fontWeight: FontWeight.w600,
letterSpacing: -0.2)),
],
),
)),
)
.toList(),
),
),
@ -220,7 +279,8 @@ class CustomCountryDropdownState extends State<CustomCountryDropdown> {
),
);
Overlay.of(context)?.insert(_overlayEntry);
overlay.insert(overlayEntry);
_overlayEntry = overlayEntry;
setState(() {
_isDropdownOpen = true;
});
@ -291,18 +351,61 @@ class CustomCountryDropdownState extends State<CustomCountryDropdown> {
// });
// }
void _closeDropdown() {
_overlayEntry.remove();
void _selectCountry(CountryEnum country) {
if (!mounted) {
_removeOverlayEntry();
return;
}
_removeOverlayEntry();
setState(() {
selectedCountry = country;
_isDropdownOpen = false;
});
// Notify the parent only after this widget has finished updating and
// removing its overlay. The callback may synchronously close the parent.
widget.onCountryChange?.call(country);
_scheduleTextFieldRefocus();
}
void _closeDropdown() {
_showDropdownTimer?.cancel();
_showDropdownTimer = null;
_removeOverlayEntry();
if (!mounted) {
_isDropdownOpen = false;
return;
}
if (_isDropdownOpen) {
setState(() {
_isDropdownOpen = false;
});
}
_scheduleTextFieldRefocus();
}
void _removeOverlayEntry() {
final overlayEntry = _overlayEntry;
_overlayEntry = null;
if (overlayEntry == null) return;
overlayEntry.remove();
overlayEntry.dispose();
}
void _scheduleTextFieldRefocus() {
_refocusTimer?.cancel();
if (widget.isEnableTextField && widget.isFromBottomSheet) {
Future.delayed(Duration(milliseconds: 100), () {
_refocusTimer = Timer(const Duration(milliseconds: 100), () {
_refocusTimer = null;
if (mounted && textFocusNode.canRequestFocus) {
FocusScope.of(context).requestFocus(textFocusNode);
}
});
}
}
}
}

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

Loading…
Cancel
Save