diff --git a/assets/images/svg/bank_icon.svg b/assets/images/svg/bank_icon.svg new file mode 100644 index 00000000..5c5df589 --- /dev/null +++ b/assets/images/svg/bank_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/svg/bank_icon_only.svg b/assets/images/svg/bank_icon_only.svg new file mode 100644 index 00000000..169731d6 --- /dev/null +++ b/assets/images/svg/bank_icon_only.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/svg/card_user.svg b/assets/images/svg/card_user.svg index ee8bb0e2..bf42ee14 100644 --- a/assets/images/svg/card_user.svg +++ b/assets/images/svg/card_user.svg @@ -1,3 +1,4 @@ + diff --git a/assets/images/svg/description_icon.svg b/assets/images/svg/description_icon.svg new file mode 100644 index 00000000..d3f17c33 --- /dev/null +++ b/assets/images/svg/description_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/svg/habib logo_only.svg b/assets/images/svg/habib logo_only.svg new file mode 100644 index 00000000..c034b9dd --- /dev/null +++ b/assets/images/svg/habib logo_only.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/image_icon.svg b/assets/images/svg/image_icon.svg index 73945847..779f1e93 100644 --- a/assets/images/svg/image_icon.svg +++ b/assets/images/svg/image_icon.svg @@ -1,3 +1,4 @@ + diff --git a/assets/images/svg/refund_icon.svg b/assets/images/svg/refund_icon.svg new file mode 100644 index 00000000..f0eabc53 --- /dev/null +++ b/assets/images/svg/refund_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/select_image_icon.svg b/assets/images/svg/select_image_icon.svg new file mode 100644 index 00000000..df9ef185 --- /dev/null +++ b/assets/images/svg/select_image_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index e0faf4e5..12219c0c 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1158,6 +1158,7 @@ "transportOptions": "خيارات النقل", "selectHospitalForAdvancePayment": "يرجى تحديد المستشفى أولاً.", "recharge": "شحن المحفظة", + "withdraw": "سحب", "activityLevel": "مستوى النشاط", "selectActivityLevel": "حدد مستوى النشاط", "almostInactive": "غير نشط تقريبًا (بدون تمارين)", @@ -1780,11 +1781,11 @@ "illurgyInfomation": "الحساسية", "vaccineInfomation": "التطعيمات", "approvals1": "موافقات", - "convertBloodcholesterolInfo":"حول قيمة كوليسترول الدم بين ملجم/ديسيلتر و مليمول/لتر", + "convertBloodcholesterolInfo": "حول قيمة كوليسترول الدم بين ملجم/ديسيلتر و مليمول/لتر", "carbsAlert": "حساب نسب المغذيات (الكربوهيدرات ،البروتين والدهون) بالسعرات الحرارية والقرامات وفقا لنسب محددة مسبقا", "bodyFatAlert": "يحسب بناءً على الحجم", "ibwAlert": "حساب الوزن المثالي بناءً على الطول، الوزن، وبنية الجسم", - "glucoseAlert":"تغيير وحدات القياس بين مليجرام لكل ديسيلتر و ملي مول لكل لتر", + "glucoseAlert": "تغيير وحدات القياس بين مليجرام لكل ديسيلتر و ملي مول لكل لتر", "selectBloodGroup": "اختر الفصيلة", "routePrescription": "طريقة الاستخدام", "noNearestER": "لا يوجد طوارئ قريب منك", @@ -1820,9 +1821,9 @@ "dragToMoveAround": "اسحب للتحرك", "startExploring": "ابدأ الاستكشاف", "symptomsCheckSuggestions": "الاحتمالات", - "appointmentArrived": "تم الحضور", + "appointmentArrived": "تم الحضور", "calendarPermissionAlertForAppointment": "يرجى منح إذن الوصول إلى التقويم من إعدادات التطبيق لضبط تذكير الموعد", -"rechargePageKey": "شحن المحفظة", + "rechargePageKey": "شحن المحفظة", "selectFamilyFile": "اختيار ", "ancillaryOrdersListNew": "طلبات غير مكتملة", "medicalReportNew": "التقارير الطبية", @@ -1846,9 +1847,50 @@ "fileSizeExceedsLimit": "يجب ألا يتجاوز حجم الملف 1 ميغابايت", "completedPrescriptionOrder": "تمت الخدمة", "submitEReferral": "تقديم الطلب", + "requestRefund": "طلب استرداد", + "uploadImage": "رفع صورة", + "selectImage": "اختر صورة", + "selectOptions": "اختر الخيارات", + "iban": "رقم الآيبان", + "addIbanDetails": "أضف تفاصيل الآيبان", + "description": "الوصف", + "enterDescription": "أدخل الوصف", + "submitRequest": "تقديم الطلب", + "ibanIsRequired": "حقل رقم الآيبان مطلوب", + "descriptionIsRequired": "الوصف مطلوب", + "invalidIbanFormat": "رقم الآيبان غير صالح. يرجى إدخال رقم آيبان صالح (مثال: SA0380000000608010167519)", + "refundHistory": "سجل الاسترداد", + "noRefundHistoryFound": "لا يوجد سجل استرداد حتى الآن.", + "invoiceNo": "رقم الفاتورة", + "totalAmount": "المبلغ الإجمالي", + "patientShare": "نصيب المريض", + "selectProcedure": "حدد العمليات", + "noProceduresAvailableForRefund": "لا توجد عمليات متاحة للاسترداد", + "selectAll": "تحديد الكل", + "selectRefundMethod": "اختر طريقة الاسترداد", + "hmgWallet": "محفظة HMG", + "bankTransfer": "تحويل بنكي", + "selectRefundReason": "اختر سبب الاسترداد", + "refundReason": "سبب الاسترداد", + "done": "تم", + "refundReason1": "تم معالجة مريض الشركة كمدفوع ذاتي وإعادة فوترته", + "refundReason2": "المريض لا يريد إجراء سريرياً", + "refundReason3": "المريض لم ينتظر الاستشارة / يريد طبيباً آخر", + "refundReason4": "المريض أوقف الاشتراك في الباقة", + "refundReason5": "الخدمة لم تُنجز / غير متاحة بسبب عطل الجهاز / رفض المريض", + "refundReason6": "تم إلغاء الإجراء السريري أو تغييره من قِبل الطبيب المحيل", + "refundReason7": "تم إلغاء إجراء وحدة الخدمة / المختبر / الأشعة أو تغييره من قِبل الطبيب المحيل", + "selectProcedures": "اختر العمليات", + "submitEReferral": "تقديم الطلب", "redeem": "استبدال", "points": "نقاط", "transactions": "المعاملات", "pharmacy": "الصيدلية", - "visitsOrders": "الزيارات/الطلبات" + "visitsOrders": "الزيارات/الطلبات", + "cannotOpenThisFile": "لا يمكن فتح هذا الملف", + "refundDetails": "تفاصيل الاسترداد", + "requestNo": "رقم الطلب.", + "referenceNo": "رقم المرجع", + "advanceNumber": "رقم الدفعة المقدمة", + "amountOnly": "المبلغ" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index e8409260..d9ab9789 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -883,7 +883,7 @@ "selectDate": "Select Date", "historyFlowchart": "History FlowChart", "to": "to", - "startDate" : "Start Date", + "startDate": "Start Date", "endDate": "End Date", "hmgHospitals": "HMG Hospitals", "hmcMedicalClinic": "HMC Medical Centers", @@ -896,7 +896,7 @@ "walkin": "Booked", "continueString": "Continue", "laserClinic": "Laser Clinic", - "covid_info" :"Dr. Sulaiman Al Habib hospitals are conducting a test for the emerging corona virus and issuing travel certificates 24/7 in a short time and with high accuracy. Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches to conduct a corona test within few minutes, and obtain the result within several hours. Corona Virus Covid 19 testing service with PCR technology to detect the virus according to the highest international standards and with the latest high-precision RT-PCR devices (American GeneXpert and others), That is approved by the Food and Drug Authority as well as by the Saudi Center for Infectious Diseases Prevention.", + "covid_info": "Dr. Sulaiman Al Habib hospitals are conducting a test for the emerging corona virus and issuing travel certificates 24/7 in a short time and with high accuracy. Those wishing to benefit from this service can visit one of Dr. Sulaiman Al Habib branches to conduct a corona test within few minutes, and obtain the result within several hours. Corona Virus Covid 19 testing service with PCR technology to detect the virus according to the highest international standards and with the latest high-precision RT-PCR devices (American GeneXpert and others), That is approved by the Food and Drug Authority as well as by the Saudi Center for Infectious Diseases Prevention.", "appointmentDetails": "Appointment Details", "checkingDoctorAvailability": "Checking doctor availability...", "cancellingAppointmentPleaseWait": "Cancelling Appointment, Please Wait...", @@ -1149,6 +1149,7 @@ "transportOptions": "Transport Options", "selectHospitalForAdvancePayment": "Please select the hospital you want to make an advance payment for.", "recharge": "Add Funds", + "withdraw": "Withdraw", "activityLevel": "Activity Level", "selectActivityLevel": "Select Activity Level", "almostInactive": "Almost Inactive (no exercise)", @@ -1262,6 +1263,8 @@ "selectLocationPrescriptionDelivery": "Please select the location for prescription delivery", "noRadiologyOrders": "No Radiology Orders Found", "ageIsRequired": "Age is required", + "ibanIsRequired": "IBAN field is required", + "descriptionIsRequired": "Description is required", "invalidAge": "Invalid age", "ageMustBeBetween11And120": "Age must be between 11 and 120", "heightIsRequired": "Height is required", @@ -1747,7 +1750,7 @@ "onlineCheckIn24HoursOnly": "Online Check-In is only available within 4 hours of the appointment time.", "searchFor": "Search For", "m": "m", -"inch": "in", + "inch": "in", "updateEmailAddressTitle": "Enter the new email address to be updated: ", "enterNewNewContactNumber": "Enter the new emergency contact number to be updated: ", "nationalIdFileNumber": "National ID/File Number", @@ -1765,10 +1768,10 @@ "selectCriteria": "Select Criteria", "locationServicesDisabled": "Weather is unavailable because location services are disabled. Please enable location services to see the weather information.", "selectDateTimeKey": "Select", - "carbsAlert":"Calculate carbohydrate based protein and fat ration in calories and grams according to a pre set ratio.", + "carbsAlert": "Calculate carbohydrate based protein and fat ration in calories and grams according to a pre set ratio.", "bodyFatAlert": "Estimate the total body fat based on the size.", "ibwAlert": "Calculates the ideal body weight based on height, weight and body size.", - "glucoseAlert":"Convert blood glucose values between mg/dL and mmol/L. (1 mmol/L ≈ 18.0182 mg/dL)", + "glucoseAlert": "Convert blood glucose values between mg/dL and mmol/L. (1 mmol/L ≈ 18.0182 mg/dL)", "selectBloodGroup": "Select Blood Group", "routePrescription": "Route", "noNearestER": "No nearest Er Around you", @@ -1810,9 +1813,9 @@ "dragToMoveAround": "Drag to move around", "startExploring": "Start exploring", "symptomsCheckSuggestions": "Suggestions", -"appointmentArrived": "Arrived", + "appointmentArrived": "Arrived", "calendarPermissionAlertForAppointment": "Please grant calendar access permission from app settings to set appointment reminder.", - "rechargePageKey": "Recharge Wallet", + "rechargePageKey": "Recharge Wallet", "selectFamilyFile": "Select", "ancillaryOrdersListNew": "Ancillary Orders List", "medicalReportNew": "Medical Report", @@ -1836,11 +1839,54 @@ "fileSizeExceedsLimit": "File size must not exceed 1 MB", "completedPrescriptionOrder": "Completed", "submitEReferral": "Submit", + "requestRefund": "Request Refund", + "uploadImage": "Upload Image", + "selectImage": "Select Image", + "selectOptions": "Select Options", + "iban": "IBAN", + "addIbanDetails": "Add IBAN Details", + "description": "Description", + "enterDescription": "Enter Description", + "submitRequest": "Submit Request", + "ibanIsRequired": "IBAN is required", + "descriptionIsRequired": "Description is required", + "invalidIbanFormat": "Invalid IBAN. Please enter a valid IBAN (e.g. SA0380000000608010167519)", + "refundHistory": "Refund History", + "noRefundHistoryFound": "You don't have any refund history yet.", + "invoiceNo": "Invoice No.", + "totalAmount": "Total Amount", + "patientShare": "Patient Share", + "selectProcedure": "Select Procedure", + "noProceduresAvailableForRefund": "No procedures available for refund", + "selectAll": "Select all", + "selectRefundMethod": "Select Refund Method", + "hmgWallet": "HMG Wallet", + "bankTransfer": "Bank Transfer", + "selectRefundReason": "Select Refund Reason", + "refundReason": "Refund Reason", + "done": "Done", + "refundReason1": "Company patient treated as self-paying and re-invoiced", + "refundReason2": "Patient does not want a clinical procedure", + "refundReason3": "Patient does not wait for consultation/wants another doctor", + "refundReason4": "Patient discontinued the package", + "refundReason5": "Service not done/not available due to machine breakdown/patient refusal", + "refundReason6": "Clinical procedure cancelled or changed by requesting doctor", + "refundReason7": "Service unit/lab/radiology procedure cancelled or changed by requesting doctor", + "selectProcedures": "Select Procedures", "redeem": "Redeem", "points": "Points", "transactions": "Transactions", "pharmacy": "Pharmacy", - "visitsOrders": "Visits/Orders" + "visitsOrders": "Visits/Orders", + "cannotOpenThisFile": "Cannot open this file", + "thisInvoiceIsNotEligibleForRefund": "This invoice is not eligible for refund", + "refundDetails": "Refund Details", + "requestNo": "Request No.", + "referenceNo": "Reference No.", + "refund": "Refund", + "advanceNumber": "Advance Number", + "amountOnly": "Amount" + } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b796a719..601f8fb0 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -601,7 +601,7 @@ SPEC CHECKSUMS: OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba - PayFortSDK: 233eabe9a45601fdbeac67fa6e5aae46ed8faf82 + PayFortSDK: e12f79051ae15b2e29536771a45354900180bfcb permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 Polyline: 2a1f29f87f8d9b7de868940f4f76deb8c678a5b1 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 @@ -612,7 +612,7 @@ SPEC CHECKSUMS: shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 Solar-dev: 4612dc9878b9fed2667d23b327f1d4e54e16e8d0 sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - SwiftProtobuf: e1b437c8e31a4c5577b643249a0bb62ed4f02153 + SwiftProtobuf: 9e106a71456f4d3f6a3b0c8fd87ef0be085efc38 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 Turf: aa2ede4298009639d10db36aba1a7ebaad072a5e url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 40cea44b..37dd6989 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* 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 */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 478CFA942E638C8E0064F3D7 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 478CFA932E638C8E0064F3D7 /* GoogleService-Info.plist */; }; @@ -25,7 +26,6 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - DE1EF78253E64BE86845D2CC /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D562310E31D1DDEFA02A6C12 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -52,11 +52,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 05E1B2A0E6844572ADA81174 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3EAE1F31D8D0AA94CA6C11EB /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 478CFA932E638C8E0064F3D7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 478CFA952E6E20A60064F3D7 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 61243B3D2EC5FA3700D46FA0 /* HMGPenguinInPlatformBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HMGPenguinInPlatformBridge.swift; sourceTree = ""; }; @@ -70,11 +72,10 @@ 687F8A672F828A6300B0FFC1 /* PenNavUI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = PenNavUI.xcframework; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7595037DD52211B91157B0F3 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 769C9BF82E6F106D009F68A9 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = ""; }; 76AA18AC2F3B2A4D00DC8DFC /* ring_30Sec.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = ring_30Sec.caf; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 8E12CEEB8E334EE22D5259D7 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 7BEF44ABFEF585D64D3F266C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -82,8 +83,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - D562310E31D1DDEFA02A6C12 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D6BB17A036DF7FCE75271203 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + A83C76CFF9D3F92EDE3B22B4 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -91,10 +91,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DE1EF78253E64BE86845D2CC /* Pods_Runner.framework in Frameworks */, 687F8A682F828A6300B0FFC1 /* PenguinINRenderer.xcframework in Frameworks */, 687F8A692F828A6300B0FFC1 /* Penguin.xcframework in Frameworks */, 687F8A6A2F828A6300B0FFC1 /* PenNavUI.xcframework in Frameworks */, + 1587365701FF0D2AAAFED0D8 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -135,7 +135,7 @@ 687F8A652F828A6300B0FFC1 /* Penguin.xcframework */, 687F8A662F828A6300B0FFC1 /* PenguinINRenderer.xcframework */, 687F8A672F828A6300B0FFC1 /* PenNavUI.xcframework */, - D562310E31D1DDEFA02A6C12 /* Pods_Runner.framework */, + A83C76CFF9D3F92EDE3B22B4 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -143,9 +143,9 @@ 79DD2093A1D9674C94359FC8 /* Pods */ = { isa = PBXGroup; children = ( - 8E12CEEB8E334EE22D5259D7 /* Pods-Runner.debug.xcconfig */, - 7595037DD52211B91157B0F3 /* Pods-Runner.release.xcconfig */, - D6BB17A036DF7FCE75271203 /* Pods-Runner.profile.xcconfig */, + 3EAE1F31D8D0AA94CA6C11EB /* Pods-Runner.debug.xcconfig */, + 05E1B2A0E6844572ADA81174 /* Pods-Runner.release.xcconfig */, + 7BEF44ABFEF585D64D3F266C /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -227,15 +227,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - BFED6CCFE59BB148875A533B /* [CP] Check Pods Manifest.lock */, + EA10FC42B68E41E75A052FAA /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, - 8372B02399CDF54531650AD4 /* [CP] Embed Pods Frameworks */, - 81DE7C26F41956799E954FCE /* [CP] Copy Pods Resources */, 765A5A922F35CD8B0003FF7D /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 5D78F615DF2D966539B34DE2 /* [CP] Embed Pods Frameworks */, + 4AA9AE39AD56A64A1F16E548 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -325,7 +325,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 81DE7C26F41956799E954FCE /* [CP] Copy Pods Resources */ = { + 4AA9AE39AD56A64A1F16E548 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -342,7 +342,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 8372B02399CDF54531650AD4 /* [CP] Embed Pods Frameworks */ = { + 5D78F615DF2D966539B34DE2 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -374,7 +374,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - BFED6CCFE59BB148875A533B /* [CP] Check Pods Manifest.lock */ = { + EA10FC42B68E41E75A052FAA /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 64074ff8..d7717b54 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:developer'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; @@ -95,8 +96,7 @@ class ApiClientImp implements ApiClient { required AppState appState, HttpClientManager? httpClient, }) : _appState = appState, - _httpClient = httpClient ?? - HttpClientManager(lifecycleService: getIt()); + _httpClient = httpClient ?? HttpClientManager(lifecycleService: getIt()); @override post( @@ -209,7 +209,8 @@ class ApiClientImp implements ApiClient { } // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 1018977; + // body['PatientID'] = 4770714; + // body['PatientID'] = 89754444; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; // body['SessionID'] = "45786230487560q"; @@ -244,7 +245,8 @@ class ApiClientImp implements ApiClient { final dynamic requestBody = isBodyPlainText ? body : json.encode(body); debugPrint("uri: ${Uri.parse(url.trim())}"); var requestBodyJSON = json.encode(body); - debugPrint("body: $requestBodyJSON", wrapWidth: 2048); + // debugPrint("body: $requestBodyJSON", wrapWidth: 2048); + log("body: $requestBodyJSON"); http.Response response; try { @@ -253,7 +255,7 @@ class ApiClientImp implements ApiClient { body: requestBody, headers: headers, ); - // debugPrint("response: ${response.body}", wrapWidth: 2048); + log("response: ${response.body.toString()}"); } on SocketException catch (e) { final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr(); onFailure(message, -1, failureType: ConnectivityFailure(message)); diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index bf4ebbed..4a71fa9b 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -269,6 +269,15 @@ class ApiConsts { // static String getAiOverViewLabOrders = "Services/Patients.svc/REST/HMGAI_Lab_Analyze_Orders_API"; // static String getAiOverViewLabOrder = "Services/Patients.svc/REST/HMGAI_Lab_Analyzer_API"; + //Refund Request + static String getRefundableInvoices = "Services/Outps.svc/REST/getRefundableInvoices"; + static String getRefundableInvoicesDetails = "Services/Outps.svc/REST/getRefundableInvoicesDetails"; + static String getRefundableAdvances = "Services/Outps.svc/REST/getRefundableAdvances"; + static String getRefundStatus = "Services/COCWS.svc/REST/GetRefundStatus"; + static String getRefundInvoices = "Services/COCWS.svc/REST/GetRefundInvoices"; + static String submitRefund = "Services/COCWS.svc/REST/SubmitRefund"; + static String isRefundableInvoice = "Services/Outps.svc/REST/isRefundableInvoice"; + static String getAiAnalysisURL = "https://api.labpredict.com"; static String getAiRadiologyAnalysis = "$getAiAnalysisURL/radiology"; static String getAiOverViewLabOrdersNew = "$getAiAnalysisURL/analyze_lab_orders"; diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index d6e041da..37f9c4fd 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -260,16 +260,15 @@ class AppAssets { //smartwatch static const String watchActivity = '$svgBasePath/watch_activity.svg'; static const String watchActivityTrailing = '$svgBasePath/watch_activity_trailing.svg'; - static const String watchSteps= '$svgBasePath/watch_steps.svg'; - static const String watchStepsTrailing= '$svgBasePath/watch_steps_trailing.svg'; - static const String watchSleep= '$svgBasePath/watch_sleep.svg'; - static const String watchSleepTrailing= '$svgBasePath/watch_sleep_trailing.svg'; - static const String watchBmi= '$svgBasePath/watch_bmi.svg'; - static const String watchBmiTrailing= '$svgBasePath/watch_bmi_trailing.svg'; - static const String watchWeight= '$svgBasePath/watch_weight.svg'; - static const String watchWeightTrailing= '$svgBasePath/watch_weight_trailing.svg'; - static const String watchHeight= '$svgBasePath/watch_height.svg'; - + static const String watchSteps = '$svgBasePath/watch_steps.svg'; + static const String watchStepsTrailing = '$svgBasePath/watch_steps_trailing.svg'; + static const String watchSleep = '$svgBasePath/watch_sleep.svg'; + static const String watchSleepTrailing = '$svgBasePath/watch_sleep_trailing.svg'; + static const String watchBmi = '$svgBasePath/watch_bmi.svg'; + static const String watchBmiTrailing = '$svgBasePath/watch_bmi_trailing.svg'; + static const String watchWeight = '$svgBasePath/watch_weight.svg'; + static const String watchWeightTrailing = '$svgBasePath/watch_weight_trailing.svg'; + static const String watchHeight = '$svgBasePath/watch_height.svg'; //bottom navigation// static const String homeBottom = '$svgBasePath/home_bottom.svg'; @@ -392,6 +391,13 @@ class AppAssets { static const String bmicalc = '$svgBasePath/bmi1.svg'; static const String bmrcalc = '$svgBasePath/bmr1.svg'; + static const String refundIcon = '$svgBasePath/refund_icon.svg'; + static const String bankIcon = '$svgBasePath/bank_icon.svg'; + static const String bankIconOnly = '$svgBasePath/bank_icon_only.svg'; + 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'; + // PNGS // static const String hmgLogo = '$pngBasePath/hmg_logo.png'; static const String liveCareService = '$pngBasePath/livecare_service.png'; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index a45e9e6d..3c6e923e 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -4,6 +4,7 @@ import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/location_util.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/ask_doctor/ask_doctor_repo.dart'; import 'package:hmg_patient_app_new/features/ask_doctor/ask_doctor_view_model.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_repo.dart'; @@ -12,8 +13,6 @@ import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_repo. 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_repo.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; -import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart'; -import 'package:hmg_patient_app_new/features/active_prescriptions/models/active_prescriptions_response_model.dart'; import 'package:hmg_patient_app_new/features/common/common_repo.dart'; import 'package:hmg_patient_app_new/features/contact_us/contact_us_repo.dart'; import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; @@ -23,6 +22,7 @@ import 'package:hmg_patient_app_new/features/emergency_services/emergency_servic import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_repo.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_repo.dart'; +import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_view_model.dart'; import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_repo.dart'; import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_repo.dart'; @@ -59,6 +59,8 @@ import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_v import 'package:hmg_patient_app_new/features/qr_parking/qr_parking_repo.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_repo.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/refund_request_repo.dart'; +import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart'; import 'package:hmg_patient_app_new/features/services_price_list/services_price_list_repo.dart'; import 'package:hmg_patient_app_new/features/services_price_list/services_price_list_view_model.dart'; import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart'; @@ -70,7 +72,6 @@ import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_repo.da import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; import 'package:hmg_patient_app_new/features/weather/weather_repo.dart'; import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart'; -import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_view_model.dart'; import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; @@ -88,13 +89,11 @@ import 'package:local_auth/local_auth.dart'; import 'package:logger/web.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../features/monthly_reports/monthly_reports_repo.dart'; -import '../features/qr_parking/qr_parking_view_model.dart'; -import '../presentation/health_calculators_and_converts/health_calculator_view_model.dart'; - import '../features/active_prescriptions/active_prescriptions_repo.dart'; import '../features/monthly_reports/terms_conditions_repo.dart'; import '../features/monthly_reports/terms_conditions_view_model.dart'; +import '../features/qr_parking/qr_parking_view_model.dart'; +import '../presentation/health_calculators_and_converts/health_calculator_view_model.dart'; GetIt getIt = GetIt.instance; @@ -183,18 +182,14 @@ class AppDependencies { getIt.registerLazySingleton(() => MonthlyReportRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => ActivePrescriptionsRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => TermsConditionsRepoImp(loggerService: getIt(), apiClient: getIt())); - getIt.registerFactory( - () => TermsConditionsViewModel( - termsConditionsRepo: getIt(), - errorHandlerService: getIt(), - ), - ); + getIt.registerFactory(() => TermsConditionsViewModel(termsConditionsRepo: getIt(), errorHandlerService: getIt())); getIt.registerLazySingleton(() => MonthlyReportsRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => QrParkingRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => NotificationsRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => AskDoctorRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => ServicesPriceListRepoImp(loggerService: getIt(), apiClient: getIt())); getIt.registerLazySingleton(() => ProfileSettingsRepoImp(loggerService: getIt(), apiClient: getIt())); + getIt.registerLazySingleton(() => RefundRequestRepoImp(apiClient: getIt(), loggerService: getIt())); getIt.registerLazySingleton(() => OffersAndDiscountsRepoImp(loggerService: getIt(), apiClient: getIt())); // ViewModels @@ -206,20 +201,23 @@ class AppDependencies { () => RadiologyViewModel(radiologyRepo: getIt(), errorHandlerService: getIt(), navigationService: getIt()), ); - getIt.registerLazySingleton(() => PrescriptionsViewModel(prescriptionsRepo: getIt(), errorHandlerService: getIt(), navServices: getIt())); + getIt.registerLazySingleton( + () => PrescriptionsViewModel(prescriptionsRepo: getIt(), errorHandlerService: getIt(), navServices: getIt())); getIt.registerLazySingleton(() => InsuranceViewModel(insuranceRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton(() => MyAppointmentsViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt())); + getIt.registerLazySingleton( + () => MyAppointmentsViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt())); - getIt.registerLazySingleton(() => AppointmentRatingViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt())); + getIt.registerLazySingleton( + () => AppointmentRatingViewModel(myAppointmentsRepo: getIt(), errorHandlerService: getIt(), appState: getIt())); getIt.registerLazySingleton( () => PayfortViewModel(payfortRepo: getIt(), errorHandlerService: getIt()), ); getIt.registerLazySingleton( - () => HabibWalletViewModel(habibWalletRepo: getIt(), errorHandlerService: getIt()), + () => HabibWalletViewModel(habibWalletRepo: getIt(), errorHandlerService: getIt(), refundRequestRepo: getIt()), ); getIt.registerLazySingleton( @@ -231,7 +229,12 @@ class AppDependencies { getIt.registerLazySingleton( () => BookAppointmentsViewModel( - bookAppointmentsRepo: getIt(), errorHandlerService: getIt(), navigationService: getIt(), myAppointmentsViewModel: getIt(), locationUtils: getIt(), dialogService: getIt()), + bookAppointmentsRepo: getIt(), + errorHandlerService: getIt(), + navigationService: getIt(), + myAppointmentsViewModel: getIt(), + locationUtils: getIt(), + dialogService: getIt()), ); getIt.registerLazySingleton( @@ -245,7 +248,13 @@ class AppDependencies { getIt.registerLazySingleton( () => AuthenticationViewModel( - authenticationRepo: getIt(), cacheService: getIt(), navigationService: getIt(), dialogService: getIt(), appState: getIt(), errorHandlerService: getIt(), localAuthService: getIt()), + authenticationRepo: getIt(), + cacheService: getIt(), + navigationService: getIt(), + dialogService: getIt(), + appState: getIt(), + errorHandlerService: getIt(), + localAuthService: getIt()), ); getIt.registerLazySingleton(() => ProfileSettingsViewModel( @@ -323,26 +332,38 @@ class AppDependencies { getIt.registerLazySingleton(() => WaterMonitorViewModel(waterMonitorRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton(() => WeatherMonitorViewModel(weatherRepo: getIt(), errorHandlerService: getIt(), locationUtils: getIt())); + getIt.registerLazySingleton( + () => WeatherMonitorViewModel(weatherRepo: getIt(), errorHandlerService: getIt(), locationUtils: getIt())); //commenting this because its already define there was on run time error because of this. - getIt.registerLazySingleton(() => MyInvoicesViewModel(myInvoicesRepo: getIt(), errorHandlerService: getIt(), navServices: getIt())); + getIt.registerLazySingleton( + () => MyInvoicesViewModel(myInvoicesRepo: getIt(), errorHandlerService: getIt(), navServices: getIt())); getIt.registerLazySingleton(() => MonthlyReportViewModel(errorHandlerService: getIt(), monthlyReportRepo: getIt())); getIt.registerLazySingleton(() => NotificationsViewModel(notificationsRepo: getIt(), errorHandlerService: getIt())); getIt.registerLazySingleton(() => HealthTrackersViewModel(healthTrackersRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton(() => ActivePrescriptionsViewModel(errorHandlerService: getIt(), activePrescriptionsRepo: getIt())); - getIt.registerFactory(() => QrParkingViewModel(qrParkingRepo: getIt(), errorHandlerService: getIt(), cacheService: getIt())); + getIt.registerLazySingleton( + () => ActivePrescriptionsViewModel(errorHandlerService: getIt(), activePrescriptionsRepo: getIt())); + getIt.registerFactory(() => QrParkingViewModel( + qrParkingRepo: getIt(), errorHandlerService: getIt(), cacheService: getIt())); getIt.registerLazySingleton(() => AskDoctorViewModel(askDoctorRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton(() => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt())); + getIt.registerLazySingleton( + () => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton(() => OffersAndDiscountsViewModel(offersAndDiscountsRepo: getIt(), errorHandlerService: getIt())); + getIt.registerLazySingleton( + () => OffersAndDiscountsViewModel(offersAndDiscountsRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton(() => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt())); + getIt.registerLazySingleton( + () => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt())); + getIt.registerLazySingleton(() => RefundRequestViewModel( + appState: getIt(), + refundRequestRepo: getIt(), + errorHandlerService: getIt(), + )); getIt.registerLazySingleton(() => PayTabsViewModel(appState: getIt())); } diff --git a/lib/core/utils/validation_utils.dart b/lib/core/utils/validation_utils.dart index b5651648..ca644e0c 100644 --- a/lib/core/utils/validation_utils.dart +++ b/lib/core/utils/validation_utils.dart @@ -33,7 +33,7 @@ class ValidationUtils { isCorrectID = false; } - if(nationalId!.length == 10) { + if (nationalId!.length == 10) { if (Utils.isSAUDIIDValid(nationalId!) == false) { _dialogService.showExceptionBottomSheet(message: LocaleKeys.enterValidNationalId.tr(), onOkPressed: onOkPress); isCorrectID = false; @@ -77,10 +77,7 @@ class ValidationUtils { // Check if "Others" country is selected and phone number starts with restricted codes if (selectedCountry == CountryEnum.others) { if (phoneNumber.startsWith('00966') || phoneNumber.startsWith('00971')) { - _dialogService.showExceptionBottomSheet( - message: LocaleKeys.cannotEnterSaudiOrUAENumber.tr(), - onOkPressed: onOkPress - ); + _dialogService.showExceptionBottomSheet(message: LocaleKeys.cannotEnterSaudiOrUAENumber.tr(), onOkPressed: onOkPress); return false; } } @@ -121,7 +118,8 @@ class ValidationUtils { return regex.hasMatch(id); } - static bool validateUaeRegistration({String? name, GenderTypeEnum? gender, NationalityCountries? country, MaritalStatusTypeEnum? maritalStatus, required Function() onOkPress}) { + static bool validateUaeRegistration( + {String? name, GenderTypeEnum? gender, NationalityCountries? country, MaritalStatusTypeEnum? maritalStatus, required Function() onOkPress}) { if (name == null || name.isEmpty) { _dialogService.showExceptionBottomSheet(message: LocaleKeys.pleaseEnterAValidName.tr(), onOkPressed: onOkPress); return false; @@ -160,7 +158,8 @@ class ValidationUtils { return true; } - static bool isValidatedIdAndPhoneWithCountryValidation({String? nationalId, String? phoneNumber, required Function() onOkPress, CountryEnum? selectedCountry}) { + static bool isValidatedIdAndPhoneWithCountryValidation( + {String? nationalId, String? phoneNumber, required Function() onOkPress, CountryEnum? selectedCountry}) { bool isCorrectID = true; if (nationalId == null || nationalId.isEmpty) { _dialogService.showExceptionBottomSheet(message: LocaleKeys.pleaseEnterAnationalID.tr(), onOkPressed: onOkPress); @@ -191,8 +190,40 @@ class ValidationUtils { } static bool isNullOrEmpty(String? value) { - return value == null || value - .trim() - .isEmpty; + return value == null || value.trim().isEmpty; } -} \ No newline at end of file + + static bool validateIban(String iban) { + final normalized = iban.replaceAll(RegExp(r'\s+'), '').toUpperCase(); + + // General IBAN structure: country code + check digits + BBAN + if (!RegExp(r'^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$').hasMatch(normalized)) { + return false; + } + + // IBAN total length must be between 15 and 34 characters + if (normalized.length < 15 || normalized.length > 34) { + return false; + } + + // Move first 4 chars to the end + final rearranged = normalized.substring(4) + normalized.substring(0, 4); + + // Convert letters to numbers: A=10, B=11, ..., Z=35 + final numeric = rearranged.split('').map((char) { + final code = char.codeUnitAt(0); + if (code >= 65 && code <= 90) { + return (code - 55).toString(); + } + return char; + }).join(); + + // MOD-97 check without parsing huge integer at once + int remainder = 0; + for (final ch in numeric.split('')) { + remainder = (remainder * 10 + int.parse(ch)) % 97; + } + + return remainder == 1; + } +} diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 469745cb..91fc3ef4 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -18,13 +18,20 @@ extension CapExtension on String { String get needTranslation => this; String get capitalizeFirstofEach => trim().isNotEmpty ? trim().toLowerCase().split(" ").map((str) => str.inCaps).join(" ") : ""; - } extension EmailValidator on String { Widget get toWidget => Text(this); - Widget toText8({bool isEnglishOnly = false, Color? color, FontWeight? fontWeight, bool isBold = false, int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow}) => Text( + Widget toText8( + {bool isEnglishOnly = false, + Color? color, + FontWeight? fontWeight, + bool isBold = false, + int? maxlines, + FontStyle? fontStyle, + TextOverflow? textOverflow}) => + Text( this, maxLines: maxlines, overflow: textOverflow, @@ -74,7 +81,8 @@ extension EmailValidator on String { int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow, - double letterSpacing = 0, bool isEnglishOnly = false}) => + double letterSpacing = 0, + bool isEnglishOnly = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, @@ -91,7 +99,17 @@ extension EmailValidator on String { decorationColor: color ?? AppColors.blackColor), ); - Widget toText11({Color? color, FontWeight? weight, bool isUnderLine = false, bool isCenter = false, bool isBold = false, int maxLine = 0, double letterSpacing = 0, bool isEnglishOnly = false,}) => Text( + Widget toText11({ + Color? color, + FontWeight? weight, + bool isUnderLine = false, + bool isCenter = false, + bool isBold = false, + int maxLine = 0, + double letterSpacing = 0, + bool isEnglishOnly = false, + }) => + Text( this, textAlign: isCenter ? TextAlign.center : null, maxLines: (maxLine > 0) ? maxLine : null, @@ -107,7 +125,8 @@ extension EmailValidator on String { ); Widget toText12( - {bool isEnglishOnly = false, Color? color, + {bool isEnglishOnly = false, + Color? color, bool isUnderLine = false, TextAlign textAlignment = TextAlign.start, bool isBold = false, @@ -176,7 +195,15 @@ extension EmailValidator on String { ), ); - Widget toText13({Color? color, bool isUnderLine = false, bool isBold = false, bool isCenter = false, int maxLine = 0, FontWeight? weight, double? letterSpacing = 0}) => Text( + Widget toText13( + {Color? color, + bool isUnderLine = false, + bool isBold = false, + bool isCenter = false, + int maxLine = 0, + FontWeight? weight, + double? letterSpacing = 0}) => + Text( this, textAlign: isCenter ? TextAlign.center : null, maxLines: (maxLine > 0) ? maxLine : null, @@ -213,11 +240,24 @@ extension EmailValidator on String { height: height, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), decoration: isUnderLine ? TextDecoration.underline : null, - fontFamily: fontFamily ?? (isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'), + fontFamily: fontFamily ?? + (isEnglishOnly + ? "Poppins" + : getIt.get().getLanguageCode() == "ar" + ? 'CairoArabic' + : 'Poppins'), decorationColor: color ?? AppColors.blackColor), ); - Widget toText15({Color? color, bool isUnderLine = false, bool isBold = false, bool isCenter = false, FontWeight? weight, int? maxlines, double? letterSpacing = -1}) => Text( + Widget toText15( + {Color? color, + bool isUnderLine = false, + bool isBold = false, + bool isCenter = false, + FontWeight? weight, + int? maxlines, + double? letterSpacing = -1}) => + Text( this, textAlign: isCenter ? TextAlign.center : null, maxLines: maxlines, @@ -229,7 +269,9 @@ extension EmailValidator on String { decoration: isUnderLine ? TextDecoration.underline : null), ); - Widget toText16({bool isEnglishOnly = false, Color? color, + Widget toText16( + {bool isEnglishOnly = false, + Color? color, bool isUnderLine = false, bool isBold = false, bool isCenter = false, @@ -254,85 +296,194 @@ extension EmailValidator on String { overflow: textOverflow, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), decoration: isUnderLine ? TextDecoration.underline : null, - fontFamily: fontFamily ?? (isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'), + fontFamily: fontFamily ?? + (isEnglishOnly + ? "Poppins" + : getIt.get().getLanguageCode() == "ar" + ? 'CairoArabic' + : 'Poppins'), decorationColor: decorationColor ?? AppColors.errorColor), ); Widget toText17({bool isEnglishOnly = false, Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, - style: TextStyle(color: color ?? AppColors.blackColor, fontSize: 17.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, fontFamily: getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'), + style: TextStyle( + color: color ?? AppColors.blackColor, + fontSize: 17.f, + letterSpacing: -1, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + fontFamily: getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'), ); - Widget toText18({Color? color, FontWeight? weight, bool isBold = false, bool isCenter = false, int? maxlines, TextOverflow? textOverflow, bool isEnglishOnly = false,}) => Text( + Widget toText18({ + Color? color, + FontWeight? weight, + bool isBold = false, + bool isCenter = false, + int? maxlines, + TextOverflow? textOverflow, + bool isEnglishOnly = false, + }) => + Text( maxLines: maxlines, textAlign: isCenter ? TextAlign.center : null, this, overflow: textOverflow, - style: TextStyle(fontSize: 18.f, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: -0.4, fontFamily: (isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),), + style: TextStyle( + fontSize: 18.f, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), + color: color ?? AppColors.blackColor, + letterSpacing: -0.4, + fontFamily: (isEnglishOnly + ? "Poppins" + : getIt.get().getLanguageCode() == "ar" + ? 'CairoArabic' + : 'Poppins'), + ), ); Widget toText19({Color? color, bool isBold = false}) => Text( this, - style: TextStyle(fontSize: 19.f, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, color: color ?? AppColors.blackColor, letterSpacing: -0.4), + style: TextStyle( + fontSize: 19.f, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, color: color ?? AppColors.blackColor, letterSpacing: -0.4), ); Widget toText20({ Color? color, FontWeight? weight, bool isBold = false, + bool isEnglishOnly = false, }) => Text( this, - style: TextStyle(fontSize: 20.f, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: -0.4), + style: TextStyle( + fontFamily: (isEnglishOnly + ? "Poppins" + : getIt.get().getLanguageCode() == "ar" + ? 'CairoArabic' + : 'Poppins'), + fontSize: 20.f, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), + color: color ?? AppColors.blackColor, + letterSpacing: -0.4), ); Widget toText21({Color? color, bool isBold = false, FontWeight? weight, int? maxlines}) => Text( this, maxLines: maxlines, - style: TextStyle(color: color ?? AppColors.blackColor, fontSize: 21.f, letterSpacing: -1, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), + style: TextStyle( + color: color ?? AppColors.blackColor, + fontSize: 21.f, + letterSpacing: -1, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), ); Widget toText22({Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, - style: TextStyle(height: 1, color: color ?? AppColors.blackColor, fontSize: 22.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), + style: TextStyle( + height: 1, + color: color ?? AppColors.blackColor, + fontSize: 22.f, + letterSpacing: -1, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); - Widget toText24({Color? color, bool isBold = false, bool isCenter = false, FontWeight? fontWeight, double? letterSpacing, bool isEnglishOnly = false,}) => Text( + Widget toText24({ + Color? color, + bool isBold = false, + bool isCenter = false, + FontWeight? fontWeight, + double? letterSpacing, + bool isEnglishOnly = false, + }) => + Text( this, textAlign: isCenter ? TextAlign.center : null, style: TextStyle( - fontFamily: (isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'), - height: 23 / 24, color: color ?? AppColors.blackColor, fontSize: 24.f, letterSpacing: letterSpacing ?? -1, fontWeight: isBold ? FontWeight.bold : fontWeight ?? FontWeight.normal), + fontFamily: (isEnglishOnly + ? "Poppins" + : getIt.get().getLanguageCode() == "ar" + ? 'CairoArabic' + : 'Poppins'), + height: 23 / 24, + color: color ?? AppColors.blackColor, + fontSize: 24.f, + letterSpacing: letterSpacing ?? -1, + fontWeight: isBold ? FontWeight.bold : fontWeight ?? FontWeight.normal), ); - Widget toText26({Color? color, bool isBold = false, double? height, bool isCenter = false, FontWeight? weight, double? letterSpacing, bool isEnglishOnly = false,}) => Text( + Widget toText26({ + Color? color, + bool isBold = false, + double? height, + bool isCenter = false, + FontWeight? weight, + double? letterSpacing, + bool isEnglishOnly = false, + }) => + Text( this, textAlign: isCenter ? TextAlign.center : null, style: TextStyle( - fontFamily: (isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'), - height: height ?? 23 / 26, color: color ?? AppColors.blackColor, fontSize: 26.f, letterSpacing: letterSpacing ?? -1, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), + fontFamily: (isEnglishOnly + ? "Poppins" + : getIt.get().getLanguageCode() == "ar" + ? 'CairoArabic' + : 'Poppins'), + height: height ?? 23 / 26, + color: color ?? AppColors.blackColor, + fontSize: 26.f, + letterSpacing: letterSpacing ?? -1, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), ); - Widget toText28({bool isEnglishOnly = false, Color? color, bool isBold = false, double? height, bool isCenter = false, double? letterSpacing}) => Text( + Widget toText28({bool isEnglishOnly = false, Color? color, bool isBold = false, double? height, bool isCenter = false, double? letterSpacing}) => + Text( this, textAlign: isCenter ? TextAlign.center : null, - style: TextStyle(height: height ?? 23 / 28, color: color ?? AppColors.blackColor, fontSize: 28.f, letterSpacing: letterSpacing ?? -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, fontFamily: getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'), + style: TextStyle( + height: height ?? 23 / 28, + color: color ?? AppColors.blackColor, + fontSize: 28.f, + letterSpacing: letterSpacing ?? -1, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + fontFamily: getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'), ); Widget toText32({bool isEnglishOnly = false, FontWeight? weight, Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, - style: TextStyle( - - height: getIt.get().getLanguageCode() == "ar" ? 1.2 : 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.f, letterSpacing: -1, fontFamily: getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins', fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal,), + height: getIt.get().getLanguageCode() == "ar" ? 1.2 : 32 / 32, + color: color ?? AppColors.blackColor, + fontSize: 32.f, + letterSpacing: -1, + fontFamily: getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins', + fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal, + ), ); - Widget toText44({Color? color, bool isBold = false, bool isEnglishOnly = false,}) => Text( + Widget toText44({ + Color? color, + bool isBold = false, + bool isEnglishOnly = false, + }) => + Text( this, - style: TextStyle(height: 32 / 32, color: color ?? AppColors.blackColor, fontSize: 44.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, fontFamily: (isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),), + style: TextStyle( + height: 32 / 32, + color: color ?? AppColors.blackColor, + fontSize: 44.f, + letterSpacing: -1, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + fontFamily: (isEnglishOnly + ? "Poppins" + : getIt.get().getLanguageCode() == "ar" + ? 'CairoArabic' + : 'Poppins'), + ), ); Widget toSectionHeading({String upperHeading = "", String lowerHeading = ""}) { @@ -368,7 +519,9 @@ extension EmailValidator on String { } bool isValidEmail() { - return RegExp(r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$').hasMatch(this); + return RegExp( + r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$') + .hasMatch(this); } String toFormattedDate() { diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart index 21139c80..e1e14ecf 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -3,14 +3,29 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_repo.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/models/lakum_inquiry_information_response_model.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/models/patient_advance_balance_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/req_models/submit_refund_request_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/refund_request_repo.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; class HabibWalletViewModel extends ChangeNotifier { + HabibWalletRepo habibWalletRepo; + RefundRequestRepo refundRequestRepo; + ErrorHandlerService errorHandlerService; + + HabibWalletViewModel({ + required this.habibWalletRepo, + required this.errorHandlerService, + required this.refundRequestRepo, + }); + bool isWalletAmountLoading = false; num habibWalletAmount = 0; num walletRechargeAmount = 0; @@ -18,6 +33,12 @@ class HabibWalletViewModel extends ChangeNotifier { bool isBottomSheetContentLoading = false; + // ── Refundable Advances ─────────────────────────────────────────────────── + bool isAdvancesLoading = false; + double totalAdvancesBalance = 0.0; + List refundableAdvancesList = []; + bool isSubmittingWithdrawal = false; + bool isSearchedFileNumberDataShown = false; int currentIndex = 0; @@ -26,9 +47,6 @@ class HabibWalletViewModel extends ChangeNotifier { HospitalsModel? selectedHospital; - HabibWalletRepo habibWalletRepo; - ErrorHandlerService errorHandlerService; - String fileNumber = ''; String depositorName = ''; String mobileNumber = ''; @@ -44,6 +62,7 @@ class HabibWalletViewModel extends ChangeNotifier { // Getters for field errors String? get amountError => _amountError; + String? get hospitalError => _hospitalError; String yahalaAccountNumber = ''; @@ -77,13 +96,13 @@ class HabibWalletViewModel extends ChangeNotifier { allTransactions.sort((a, b) { // Null dates go to the end if (a.transactionDate == null && b.transactionDate == null) return 0; - if (a.transactionDate == null) return 1; // a goes after b + if (a.transactionDate == null) return 1; // a goes after b if (b.transactionDate == null) return -1; // a goes before b - + try { DateTime dateA = DateUtil.convertStringToDate(a.transactionDate!); DateTime dateB = DateUtil.convertStringToDate(b.transactionDate!); - + // Compare dateB to dateA (not dateA to dateB) for DESCENDING order // This puts newer dates (larger values) at the top of the list return dateB.compareTo(dateA); @@ -115,6 +134,48 @@ class HabibWalletViewModel extends ChangeNotifier { notifyListeners(); } + // ==================== WITHDRAW VALIDATION ERROR TRACKING ==================== + String? _ibanError; + String? _withdrawDescriptionError; + + String? get ibanError => _ibanError; + + String? get withdrawDescriptionError => _withdrawDescriptionError; + + void clearIbanError() { + _ibanError = null; + notifyListeners(); + } + + void clearWithdrawDescriptionError() { + _withdrawDescriptionError = null; + notifyListeners(); + } + + bool validateWithdrawForm({required String iban, required String description}) { + bool isValid = true; + _ibanError = null; + _withdrawDescriptionError = null; + + if (iban.trim().isEmpty) { + _ibanError = LocaleKeys.ibanIsRequired.tr(); + isValid = false; + } else { + if (!ValidationUtils.validateIban(iban.trim())) { + _ibanError = LocaleKeys.invalidIbanFormat.tr(); + isValid = false; + } + } + + if (description.trim().isEmpty) { + _withdrawDescriptionError = LocaleKeys.descriptionIsRequired.tr(); + isValid = false; + } + + notifyListeners(); + return isValid; + } + // Validate recharge form - shows only first error bool validateRechargeForm(String amount) { // Clear previous errors @@ -150,7 +211,114 @@ class HabibWalletViewModel extends ChangeNotifier { return true; } - HabibWalletViewModel({required this.habibWalletRepo, required this.errorHandlerService}); + Future fetchAdvancesForHospital() async { + isAdvancesLoading = true; + totalAdvancesBalance = 0.0; + refundableAdvancesList = []; + notifyListeners(); + + final result = await refundRequestRepo.getRefundableAdvances(); + result.fold( + (failure) { + isAdvancesLoading = false; + notifyListeners(); + errorHandlerService.handleError(failure: failure); + }, + (apiResponse) { + isAdvancesLoading = false; + if (apiResponse.data != null) { + refundableAdvancesList = apiResponse.data!.refundableAdvancesItems; + totalAdvancesBalance = refundableAdvancesList.fold(0.0, (sum, item) => sum + (item.balanceAmount ?? 0.0)); + } + notifyListeners(); + }, + ); + } + + /// Converts a WCF /Date(ms+timezone)/ string to "YYYY-MM-DD" format expected by the API. + String _formatAdvanceDate(String? rawDate) { + if (rawDate == null || rawDate.isEmpty) return ''; + try { + final date = DateUtil.convertStringToDate(rawDate); + return DateFormat('yyyy-MM-dd').format(date); + } catch (_) { + return rawDate; + } + } + + Future submitWalletWithdrawalRequest({ + required String iban, + required String remarks, + required String patientName, + required String patientId, + required String patientMobileNumber, + required String patientNationalId, + required int projectId, + required String projectName, + Function(SubmitRefundResponseModel)? onSuccess, + Function(String)? onError, + }) async { + isSubmittingWithdrawal = true; + notifyListeners(); + + final advances = refundableAdvancesList + .map((a) => SubmitRefundAdvanceItem( + advanceNo: (a.advanceNo ?? 0).toString(), + paidAmount: a.paidAmount ?? 0.0, + balanceAmount: a.balanceAmount ?? 0.0, + advanceDate: a.advanceDate ?? "", + // advanceDate: _formatAdvanceDate(a.advanceDate), + )) + .toList(); + + final request = SubmitRefundRequestModel( + projectId: projectId, + refundType: '2', + // wallet withdrawal + refundProject: projectName, + refundProjectId: projectId, + refundPatientId: patientId, + patientMobileNumber: patientMobileNumber, + refundPatientName: patientName, + refundNationalId: patientNationalId, + refundInvoiceNo: '', + refundAppointmentNo: '', + refundDoctorId: 0, + refundDoctorName: '', + refundClinicId: 0, + refundClinicName: '', + refundDestination: '2', // only bank transfer + refundIban: iban, + refundRemarks: remarks, + refundTotalAmount: totalAdvancesBalance, + refundProceduresList: const [], + refundAdvancesList: advances, + ); + + final result = await refundRequestRepo.submitRefund(request: request); + result.fold( + (failure) { + isSubmittingWithdrawal = false; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (apiResponse) { + isSubmittingWithdrawal = false; + notifyListeners(); + if (apiResponse.data != null) { + if (onSuccess != null) onSuccess(apiResponse.data!); + } else { + if (onError != null) { + onError(apiResponse.errorMessage ?? LocaleKeys.failed.tr()); + } + } + }, + ); + } initHabibWalletProvider() { isWalletAmountLoading = true; @@ -180,6 +348,20 @@ class HabibWalletViewModel extends ChangeNotifier { notifyListeners(); } + /// Resets all state related to the wallet withdrawal form. + /// Pass [silent] = true when calling from dispose() to avoid notifyListeners + /// while the widget tree is locked. + void resetWithdrawForm({bool silent = false}) { + selectedHospital = null; + totalAdvancesBalance = 0.0; + refundableAdvancesList = []; + isAdvancesLoading = false; + isSubmittingWithdrawal = false; + _ibanError = null; + _withdrawDescriptionError = null; + if (!silent) notifyListeners(); + } + setWalletRechargeAmount(num amount) { walletRechargeAmount = amount; notifyListeners(); @@ -277,12 +459,17 @@ class HabibWalletViewModel extends ChangeNotifier { Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await habibWalletRepo.HISCreateAdvancePayment( - paymentMethodName: paymentMethodName, paidAmount: paidAmount, paymentReference: paymentReference, patientID: patientID, projectID: projectID, depositorName: depositorName); + paymentMethodName: paymentMethodName, + paidAmount: paidAmount, + paymentReference: paymentReference, + patientID: patientID, + projectID: projectID, + depositorName: depositorName); result.fold( // (failure) async => await errorHandlerService.handleError(failure: failure), (failure) async { - if(onError != null) { + if (onError != null) { onError(failure.message); } }, @@ -299,7 +486,8 @@ class HabibWalletViewModel extends ChangeNotifier { ); } - Future addAdvanceNumberRequest({required String advanceNumber, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async { + Future addAdvanceNumberRequest( + {required String advanceNumber, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await habibWalletRepo.addAdvanceNumberRequest(advanceNumber: advanceNumber, paymentReference: paymentReference); result.fold( @@ -347,7 +535,8 @@ class HabibWalletViewModel extends ChangeNotifier { lakumAccountInfo = LakumInquiryInformationResponseModel(); notifyListeners(); - final result = await habibWalletRepo.getLakumAccountInformation(identificationNumber: getIt.get().getAuthenticatedUser()!.patientIdentificationNo ?? ""); + final result = await habibWalletRepo.getLakumAccountInformation( + identificationNumber: getIt.get().getAuthenticatedUser()!.patientIdentificationNo ?? ""); result.fold( (failure) async => await errorHandlerService.handleError(failure: failure), diff --git a/lib/features/my_invoices/models/get_invoices_list_response_model.dart b/lib/features/my_invoices/models/get_invoices_list_response_model.dart index 6e515feb..a3c9858c 100644 --- a/lib/features/my_invoices/models/get_invoices_list_response_model.dart +++ b/lib/features/my_invoices/models/get_invoices_list_response_model.dart @@ -18,6 +18,7 @@ class GetInvoicesListResponseModel { double? decimalDoctorRate; String? doctorImageUrl; int? doctorRate; + bool? isAllowRefund; int? patientNumber; String? projectName; @@ -39,6 +40,7 @@ class GetInvoicesListResponseModel { this.decimalDoctorRate, this.doctorImageUrl, this.doctorRate, + this.isAllowRefund, this.patientNumber, this.projectName, }); @@ -48,46 +50,48 @@ class GetInvoicesListResponseModel { String toRawJson() => json.encode(toJson()); factory GetInvoicesListResponseModel.fromJson(Map json) => GetInvoicesListResponseModel( - setupId: json["SetupId"], - projectId: json["ProjectID"], - patientId: json["PatientID"], - appointmentNo: json["AppointmentNo"], - appointmentDate: json["AppointmentDate"], - appointmentDateN: json["AppointmentDateN"], - clinicId: json["ClinicID"], - doctorId: json["DoctorID"], - invoiceNo: json["InvoiceNo"], - status: json["Status"], - arrivedOn: json["ArrivedOn"], - doctorName: json["DoctorName"], - doctorNameN: json["DoctorNameN"], - clinicName: json["ClinicName"], - decimalDoctorRate: json["DecimalDoctorRate"]?.toDouble(), - doctorImageUrl: json["DoctorImageURL"], - doctorRate: json["DoctorRate"], - patientNumber: json["PatientNumber"], - projectName: json["ProjectName"], - ); + setupId: json["SetupId"], + projectId: json["ProjectID"], + patientId: json["PatientID"], + appointmentNo: json["AppointmentNo"], + appointmentDate: json["AppointmentDate"], + appointmentDateN: json["AppointmentDateN"], + clinicId: json["ClinicID"], + doctorId: json["DoctorID"], + invoiceNo: json["InvoiceNo"], + status: json["Status"], + arrivedOn: json["ArrivedOn"], + doctorName: json["DoctorName"], + doctorNameN: json["DoctorNameN"], + clinicName: json["ClinicName"], + decimalDoctorRate: json["DecimalDoctorRate"]?.toDouble(), + doctorImageUrl: json["DoctorImageURL"], + doctorRate: json["DoctorRate"], + isAllowRefund: json["IsAllowRefund"], + patientNumber: json["PatientNumber"], + projectName: json["ProjectName"], + ); Map toJson() => { - "SetupId": setupId, - "ProjectID": projectId, - "PatientID": patientId, - "AppointmentNo": appointmentNo, - "AppointmentDate": appointmentDate, - "AppointmentDateN": appointmentDateN, - "ClinicID": clinicId, - "DoctorID": doctorId, - "InvoiceNo": invoiceNo, - "Status": status, - "ArrivedOn": arrivedOn, - "DoctorName": doctorName, - "DoctorNameN": doctorNameN, - "ClinicName": clinicName, - "DecimalDoctorRate": decimalDoctorRate, - "DoctorImageURL": doctorImageUrl, - "DoctorRate": doctorRate, - "PatientNumber": patientNumber, - "ProjectName": projectName, - }; + "SetupId": setupId, + "ProjectID": projectId, + "PatientID": patientId, + "AppointmentNo": appointmentNo, + "AppointmentDate": appointmentDate, + "AppointmentDateN": appointmentDateN, + "ClinicID": clinicId, + "DoctorID": doctorId, + "InvoiceNo": invoiceNo, + "Status": status, + "ArrivedOn": arrivedOn, + "DoctorName": doctorName, + "DoctorNameN": doctorNameN, + "ClinicName": clinicName, + "DecimalDoctorRate": decimalDoctorRate, + "DoctorImageURL": doctorImageUrl, + "DoctorRate": doctorRate, + "IsAllowRefund": isAllowRefund, + "PatientNumber": patientNumber, + "ProjectName": projectName, + }; } diff --git a/lib/features/paytabs/paytabs_view_model.dart b/lib/features/paytabs/paytabs_view_model.dart index c922c752..0e0273e6 100644 --- a/lib/features/paytabs/paytabs_view_model.dart +++ b/lib/features/paytabs/paytabs_view_model.dart @@ -11,7 +11,6 @@ import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/paytabs_config.dart'; import 'package:hmg_patient_app_new/features/paytabs/models/paytabs_transaction_response_model.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart'; class PayTabsViewModel extends ChangeNotifier { late PaymentSdkConfigurationDetails paymentConfiguration; @@ -33,8 +32,9 @@ class PayTabsViewModel extends ChangeNotifier { cartDescription: paymentDescription, merchantName: PaymentSdkDefaultConfig.defaultMerchantName, locale: appState.isArabic() ? PaymentSdkLocale.AR : PaymentSdkLocale.EN, - merchantApplePayIndentifier: - ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? PaymentSdkDefaultConfig.defaultMerchantAppleBundleIDUAT : PaymentSdkDefaultConfig.defaultMerchantAppleBundleID, + merchantApplePayIndentifier: ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat + ? PaymentSdkDefaultConfig.defaultMerchantAppleBundleIDUAT + : PaymentSdkDefaultConfig.defaultMerchantAppleBundleID, // merchantApplePayIndentifier: PaymentSdkDefaultConfig.defaultMerchantAppleBundleIDUAT, screentTitle: paymentDescription, amount: amount.toDouble(), @@ -43,10 +43,23 @@ class PayTabsViewModel extends ChangeNotifier { currencyCode: PaymentSdkDefaultConfig.defaultCurrency, merchantCountryCode: PaymentSdkDefaultConfig.defaultMerchantCountryCode, billingDetails: BillingDetails( - "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" ?? "HMG Patient", "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com", - appState.getAuthenticatedUser()!.mobileNumber ?? "0000000000", "Riyadh", "SA", "Riyadh", "Riyadh", "12626"), - shippingDetails: ShippingDetails("${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" ?? "HMG Patient", "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com", - appState.getAuthenticatedUser()!.mobileNumber ?? "0000000000", "Riyadh", "SA", "Riyadh", "Riyadh", "12626"), + "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" ?? "HMG Patient", + "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com", + appState.getAuthenticatedUser()!.mobileNumber ?? "0000000000", + "Riyadh", + "SA", + "Riyadh", + "Riyadh", + "12626"), + shippingDetails: ShippingDetails( + "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}" ?? "HMG Patient", + "CustID_${appState.getAuthenticatedUser()!.patientId.toString()}@HMG.com", + appState.getAuthenticatedUser()!.mobileNumber ?? "0000000000", + "Riyadh", + "SA", + "Riyadh", + "Riyadh", + "12626"), alternativePaymentMethods: [], linkBillingNameWithCardHolderName: true, simplifyApplePayValidation: true); @@ -89,9 +102,11 @@ class PayTabsViewModel extends ChangeNotifier { onSuccess!(paytabsTransactionResponseModel); } else { // Transaction was processed but failed - final reason = transactionDetails?["payResponseReturn"] ?? transactionDetails?["responseMessage"] ?? transactionDetails?["message"] ?? "Unknown error"; + 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"; + final errorMessage = + responseCode.toString().isNotEmpty ? "Transaction failed (Code: $responseCode): $reason" : "Transaction failed: $reason"; onError!(errorMessage); } } else if (event["status"] == "error") { @@ -117,9 +132,11 @@ class PayTabsViewModel extends ChangeNotifier { onSuccess!(paytabsTransactionResponseModel); } else { // Transaction was processed but failed - final reason = transactionDetails?["payResponseReturn"] ?? transactionDetails?["responseMessage"] ?? transactionDetails?["message"] ?? "Unknown error"; + 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"; + final errorMessage = + responseCode.toString().isNotEmpty ? "Transaction failed (Code: $responseCode): $reason" : "Transaction failed: $reason"; onError!(errorMessage); } } else if (event["status"] == "error") { diff --git a/lib/features/refund_request/models/req_models/submit_refund_request_model.dart b/lib/features/refund_request/models/req_models/submit_refund_request_model.dart new file mode 100644 index 00000000..3a8b104c --- /dev/null +++ b/lib/features/refund_request/models/req_models/submit_refund_request_model.dart @@ -0,0 +1,142 @@ +class SubmitRefundProcedureItem { + final int lineItemNo; + final String procedureId; + final String procedureName; + final double patientPaidAmount; + final String refundReason; + + const SubmitRefundProcedureItem({ + required this.lineItemNo, + required this.procedureId, + required this.procedureName, + required this.patientPaidAmount, + required this.refundReason, + }); + + Map toJson() => { + "LineItemNo": lineItemNo, + "ProcedureID": procedureId, + "ProcedureName": procedureName, + "PatientPaidAmount": patientPaidAmount, + "RefundReason": refundReason, + }; + + @override + String toString() { + return 'SubmitRefundProcedureItem{lineItemNo: $lineItemNo, procedureId: $procedureId, procedureName: $procedureName, patientPaidAmount: $patientPaidAmount, refundReason: $refundReason}'; + } +} + +/// Represents a single advance item in the wallet withdrawal refund payload. +class SubmitRefundAdvanceItem { + final String advanceNo; + final double paidAmount; + final double balanceAmount; + final String advanceDate; + + const SubmitRefundAdvanceItem({ + required this.advanceNo, + required this.paidAmount, + required this.balanceAmount, + required this.advanceDate, + }); + + Map toJson() => { + "AdvanceNo": advanceNo, + "PaidAmount": paidAmount, + "BalanceAmount": balanceAmount, + "AdvanceDate": advanceDate, + }; + + @override + String toString() { + return 'SubmitRefundAdvanceItem{advanceNo: $advanceNo, paidAmount: $paidAmount, balanceAmount: $balanceAmount, advanceDate: $advanceDate}'; + } +} + +class SubmitRefundRequestModel { + final int projectId; + final String refundType; + final String refundProject; + final int refundProjectId; + final String refundPatientId; + final String patientMobileNumber; + final String refundPatientName; + final String refundNationalId; + final String refundInvoiceNo; + final String refundAppointmentNo; + final int refundDoctorId; + final String refundDoctorName; + final int refundClinicId; + final String refundClinicName; + final String refundDestination; + final String refundIban; + final String refundRemarks; + final double refundTotalAmount; + final List refundProceduresList; + /// Populated only for wallet withdrawal refunds (refundType == '2'). + final List refundAdvancesList; + + const SubmitRefundRequestModel({ + required this.projectId, + required this.refundType, + required this.refundProject, + required this.refundProjectId, + required this.refundPatientId, + required this.patientMobileNumber, + required this.refundPatientName, + required this.refundNationalId, + required this.refundInvoiceNo, + required this.refundAppointmentNo, + required this.refundDoctorId, + required this.refundDoctorName, + required this.refundClinicId, + required this.refundClinicName, + required this.refundDestination, + required this.refundIban, + required this.refundRemarks, + required this.refundTotalAmount, + required this.refundProceduresList, + this.refundAdvancesList = const [], + }); + + /// refundType '1' = invoice refund → sends Refund_ProceduresList + /// refundType '2' = wallet withdrawal → sends Refund_AdvancesList + Map toJson() { + final Map json = { + "ProjectID": projectId, + "Refund_RefundType": refundType, + "Refund_Project": refundProject, + "Refund_ProjectId": refundProjectId, + "Refund_PatientId": refundPatientId, + "PatientMobileNumber": patientMobileNumber, + "Refund_PatientName": refundPatientName, + "Refund_NationalId": refundNationalId, + "Refund_DoctorId": refundDoctorId, + "Refund_DoctorName": refundDoctorName, + "Refund_RefundDestination": refundDestination, + "Refund_IBAN": refundIban, + "Refund_Remarks": refundRemarks, + "Refund_TotalRefundAmount": refundTotalAmount, + }; + + if (refundType == '2') { + // Wallet withdrawal — include advances list, omit invoice-specific fields + json["Refund_AdvancesList"] = refundAdvancesList.map((a) => a.toJson()).toList(); + } else { + // Invoice refund — include invoice-specific fields and procedures list + json["Refund_InvoiceNo"] = refundInvoiceNo; + json["Refund_AppointmentNo"] = refundAppointmentNo; + json["Refund_ClinicId"] = refundClinicId; + json["Refund_ClinicName"] = refundClinicName; + json["Refund_ProceduresList"] = refundProceduresList.map((p) => p.toJson()).toList(); + } + + return json; + } + + @override + String toString() { + return 'SubmitRefundRequestModel{projectId: $projectId, refundType: $refundType, refundAdvancesList: $refundAdvancesList, refundProceduresList: $refundProceduresList}'; + } +} diff --git a/lib/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart b/lib/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart new file mode 100644 index 00000000..8e62b943 --- /dev/null +++ b/lib/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart @@ -0,0 +1,23 @@ +class IsRefundableInvoiceObject { + final String? errorCode; + final bool? isRefundable; + final String? message; + final int? statusCode; + + IsRefundableInvoiceObject({ + this.errorCode, + this.isRefundable, + this.message, + this.statusCode, + }); + + factory IsRefundableInvoiceObject.fromJson(Map json) { + return IsRefundableInvoiceObject( + errorCode: json['ErrorCode'] as String?, + isRefundable: json['IsRefundable'] as bool?, + message: json['Message'] as String?, + statusCode: json['StatusCode'] as int?, + ); + } +} + diff --git a/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart b/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart new file mode 100644 index 00000000..b17dd5aa --- /dev/null +++ b/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart @@ -0,0 +1,312 @@ +class SubmitRefundResponseModel { + final String? message; + final String? refundType; + final String? patientId; + final String? refNo; + + SubmitRefundResponseModel({ + this.message, + this.refundType, + this.patientId, + this.refNo, + }); + + factory SubmitRefundResponseModel.fromJson(Map json) { + return SubmitRefundResponseModel( + message: (json['Message'] ?? json['message']) as String?, + refundType: (json['Refund_RefundTypeDisplay'] ?? json['refundType'])?.toString(), + patientId: (json['Refund_PatientIdDisplay'] ?? json['patientId'])?.toString(), + refNo: (json['Refund_RefNo'] ?? json['ReferenceNumber'] ?? json['refNo'])?.toString(), + ); + } +} + +class RefundStatusModel { + final String? errorMessage; + final String? lastUpdated; + final bool? pendingVerification; + final String? referenceNumber; + final String? requestStatusName; + final String? status; + final bool? verified; + + RefundStatusModel({ + this.errorMessage, + this.lastUpdated, + this.pendingVerification, + this.referenceNumber, + this.requestStatusName, + this.status, + this.verified, + }); + + factory RefundStatusModel.fromJson(Map json) { + return RefundStatusModel( + errorMessage: json['ErrorMessage'] as String?, + lastUpdated: json['LastUpdated'] as String?, + pendingVerification: json['PendingVerification'] as bool?, + referenceNumber: json['ReferenceNumber'] as String?, + requestStatusName: json['RequestStatusName'] as String?, + status: json['Status'] as String?, + verified: json['Verified'] as bool?, + ); + } +} + +class WalletAdvanceItem { + final String? advanceDate; + final String? advanceNo; + final double? balanceAmount; + final double? paidAmount; + + WalletAdvanceItem({ + this.advanceDate, + this.advanceNo, + this.balanceAmount, + this.paidAmount, + }); + + factory WalletAdvanceItem.fromJson(Map json) { + return WalletAdvanceItem( + advanceDate: json['AdvanceDate'] as String?, + advanceNo: json['AdvanceNo'] as String?, + balanceAmount: (json['BalanceAmount'] as num?)?.toDouble(), + paidAmount: (json['PaidAmount'] as num?)?.toDouble(), + ); + } +} + +class WalletRequestPayload { + final List? advancesList; + final int? doctorId; + final String? doctorName; + final String? iban; + final String? nationalId; + final String? patientId; + final String? patientName; + final String? project; + final int? projectId; + final String? refundAmountTypeName; + final String? refundDestination; + final String? refundDestinationName; + final String? refundType; + final String? refundTypeName; + final String? remarks; + final double? totalRefundAmount; + + WalletRequestPayload({ + this.advancesList, + this.doctorId, + this.doctorName, + this.iban, + this.nationalId, + this.patientId, + this.patientName, + this.project, + this.projectId, + this.refundAmountTypeName, + this.refundDestination, + this.refundDestinationName, + this.refundType, + this.refundTypeName, + this.remarks, + this.totalRefundAmount, + }); + + factory WalletRequestPayload.fromJson(Map json) { + return WalletRequestPayload( + advancesList: (json['AdvancesList'] as List?)?.map((e) => WalletAdvanceItem.fromJson(e as Map)).toList(), + doctorId: (json['DoctorId'] as num?)?.toInt(), + doctorName: json['DoctorName'] as String?, + iban: json['IBAN'] as String?, + nationalId: json['NationalId'] as String?, + patientId: json['PatientId'] as String?, + patientName: json['PatientName'] as String?, + project: json['Project'] as String?, + projectId: (json['ProjectId'] as num?)?.toInt(), + refundAmountTypeName: json['RefundAmountTypeName'] as String?, + refundDestination: json['RefundDestination'] as String?, + refundDestinationName: json['RefundDestinationName'] as String?, + refundType: json['RefundType'] as String?, + refundTypeName: json['RefundTypeName'] as String?, + remarks: json['Remarks'] as String?, + totalRefundAmount: (json['TotalRefundAmount'] as num?)?.toDouble(), + ); + } +} + +class InvoiceRefundProcedure { + final bool? isLocked; + final int? lineItemNo; + final double? patientPaidAmount; + final String? procedureId; + final String? procedureName; + final String? refundReason; + + InvoiceRefundProcedure({ + this.isLocked, + this.lineItemNo, + this.patientPaidAmount, + this.procedureId, + this.procedureName, + this.refundReason, + }); + + factory InvoiceRefundProcedure.fromJson(Map json) { + return InvoiceRefundProcedure( + isLocked: json['IsLocked'] as bool?, + lineItemNo: (json['LineItemNo'] as num?)?.toInt(), + patientPaidAmount: (json['PatientPaidAmount'] as num?)?.toDouble(), + procedureId: json['ProcedureID'] as String?, + procedureName: json['ProcedureName'] as String?, + refundReason: json['RefundReason'] as String?, + ); + } +} + +class InvoiceRequestPayload { + final List? advancesList; + final String? appointmentNo; + final int? clinicId; + final String? clinicName; + final int? doctorId; + final String? doctorName; + final String? iban; + final String? invoiceDate; + final String? invoiceNo; + final bool? isFastTrack; + final String? nationalId; + final String? patientId; + final String? patientName; + final List? proceduresList; + final String? project; + final int? projectId; + final String? refundAmountType; + final String? refundAmountTypeName; + final String? refundDestination; + final String? refundDestinationName; + final double? refundTaxAmount; + final String? refundType; + final String? refundTypeName; + final String? remarks; + final double? totalRefundAmount; + + InvoiceRequestPayload({ + this.advancesList, + this.appointmentNo, + this.clinicId, + this.clinicName, + this.doctorId, + this.doctorName, + this.iban, + this.invoiceDate, + this.invoiceNo, + this.isFastTrack, + this.nationalId, + this.patientId, + this.patientName, + this.proceduresList, + this.project, + this.projectId, + this.refundAmountType, + this.refundAmountTypeName, + this.refundDestination, + this.refundDestinationName, + this.refundTaxAmount, + this.refundType, + this.refundTypeName, + this.remarks, + this.totalRefundAmount, + }); + + factory InvoiceRequestPayload.fromJson(Map json) { + return InvoiceRequestPayload( + advancesList: (json['AdvancesList'] as List?)?.map((e) => WalletAdvanceItem.fromJson(e as Map)).toList(), + appointmentNo: json['AppointmentNo']?.toString(), + clinicId: (json['ClinicId'] as num?)?.toInt(), + clinicName: json['ClinicName'] as String?, + doctorId: (json['DoctorId'] as num?)?.toInt(), + doctorName: json['DoctorName'] as String?, + iban: json['IBAN'] as String?, + invoiceDate: json['InvoiceDate'] as String?, + invoiceNo: json['InvoiceNo'] as String?, + isFastTrack: json['IsFastTrack'] as bool?, + nationalId: json['NationalId'] as String?, + patientId: json['PatientId'] as String?, + patientName: json['PatientName'] as String?, + proceduresList: (json['ProceduresList'] as List?)?.map((e) => InvoiceRefundProcedure.fromJson(e as Map)).toList(), + project: json['Project'] as String?, + projectId: (json['ProjectId'] as num?)?.toInt(), + refundAmountType: json['RefundAmountType'] as String?, + refundAmountTypeName: json['RefundAmountTypeName'] as String?, + refundDestination: json['RefundDestination'] as String?, + refundDestinationName: json['RefundDestinationName'] as String?, + refundTaxAmount: (json['RefundTaxAmount'] as num?)?.toDouble(), + refundType: json['RefundType'] as String?, + refundTypeName: json['RefundTypeName'] as String?, + remarks: json['Remarks'] as String?, + totalRefundAmount: (json['TotalRefundAmount'] as num?)?.toDouble(), + ); + } +} + +class RefundInvoiceModel { + final String? invoiceNumber; + final String? lastUpdated; + final String? patientId; + final bool? pendingVerification; + final String? referenceNumber; + final String? refundDestination; + final int? refundDestinationId; + final String? refundType; + final int? refundTypeId; + final String? requestStatusName; + final int? requestStatusId; + final String? verificationInitiatedAt; + final bool? verified; + final String? errorMessage; + final WalletRequestPayload? walletRequestPayload; + final InvoiceRequestPayload? invoiceRequestPayload; + + RefundInvoiceModel({ + this.invoiceNumber, + this.lastUpdated, + this.patientId, + this.pendingVerification, + this.referenceNumber, + this.refundDestination, + this.refundDestinationId, + this.refundType, + this.refundTypeId, + this.requestStatusName, + this.requestStatusId, + this.verificationInitiatedAt, + this.verified, + this.errorMessage, + this.walletRequestPayload, + this.invoiceRequestPayload, + }); + + factory RefundInvoiceModel.fromJson(Map json) { + return RefundInvoiceModel( + invoiceNumber: json['InvoiceNumber'] as String?, + lastUpdated: json['LastUpdated'] as String?, + patientId: json['PatientID'] as String?, + pendingVerification: json['PendingVerification'] as bool?, + referenceNumber: json['ReferenceNumber'] as String?, + refundDestination: json['RefundDestination'] as String?, + refundDestinationId: json['RefundDestinationId'] as int?, + refundType: json['RefundType'] as String?, + refundTypeId: json['RefundTypeId'] as int?, + requestStatusName: json['StatusName'] as String?, + requestStatusId: json['StatusId'] as int?, + verificationInitiatedAt: json['VerificationInitiatedAt'] as String?, + verified: json['Verified'] as bool?, + errorMessage: json['ErrorMessage'] as String?, + walletRequestPayload: + json['WalletRequestPayload'] != null ? WalletRequestPayload.fromJson(json['WalletRequestPayload'] as Map) : null, + invoiceRequestPayload: + json['InvoiceRequestPayload'] != null ? InvoiceRequestPayload.fromJson(json['InvoiceRequestPayload'] as Map) : null, + ); + } +} diff --git a/lib/features/refund_request/models/resp_models/refundable_invoices_response_model.dart b/lib/features/refund_request/models/resp_models/refundable_invoices_response_model.dart new file mode 100644 index 00000000..2409f3ec --- /dev/null +++ b/lib/features/refund_request/models/resp_models/refundable_invoices_response_model.dart @@ -0,0 +1,199 @@ +import 'dart:convert'; + +class RefundableInvoicesResponseModel { + final String? errorCode; + final String? message; + final List? refundableInvoiceItems; + final int? statusCode; + + RefundableInvoicesResponseModel({ + this.errorCode, + this.message, + this.refundableInvoiceItems, + this.statusCode, + }); + + factory RefundableInvoicesResponseModel.fromJson(Map json) { + return RefundableInvoicesResponseModel( + errorCode: json['ErrorCode'] as String?, + message: json['Message'] as String?, + refundableInvoiceItems: json['RefundableInvoiceItems'] != null + ? (json['RefundableInvoiceItems'] as List).map((item) => RefundableInvoiceItem.fromJson(item as Map)).toList() + : null, + statusCode: json['StatusCode'] as int?, + ); + } +} + +class RefundableInvoiceItem { + final int? appointmentNo; + final int? clinicID; + final String? clinicName; + final int? doctorID; + final String? doctorName; + final String? invoiceDate; + final int? invoiceNo; + final String? nationalID; + final int? patientID; + final int? projectID; + + RefundableInvoiceItem({ + this.appointmentNo, + this.clinicID, + this.clinicName, + this.doctorID, + this.doctorName, + this.invoiceDate, + this.invoiceNo, + this.nationalID, + this.patientID, + this.projectID, + }); + + factory RefundableInvoiceItem.fromRawJson(String str) => RefundableInvoiceItem.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory RefundableInvoiceItem.fromJson(Map json) { + return RefundableInvoiceItem( + appointmentNo: json['AppointmentNo'] as int?, + clinicID: json['ClinicID'] as int?, + clinicName: json['ClinicName'] as String?, + doctorID: json['DoctorID'] as int?, + doctorName: json['DoctorName'] as String?, + invoiceDate: json['InvoiceDate'] as String?, + invoiceNo: json['InvoiceNo'] as int?, + nationalID: json['NationalID'] as String?, + patientID: json['PatientID'] as int?, + projectID: json['ProjectID'] as int?, + ); + } + + Map toJson() { + return { + 'AppointmentNo': appointmentNo, + 'ClinicID': clinicID, + 'ClinicName': clinicName, + 'DoctorID': doctorID, + 'DoctorName': doctorName, + 'InvoiceDate': invoiceDate, + 'InvoiceNo': invoiceNo, + 'NationalID': nationalID, + 'PatientID': patientID, + 'ProjectID': projectID, + }; + } +} + +class RefundableInvoiceDetailsItem { + final int? lineItemNo; + final double? patientPaidAmount; + final String? procedureId; + final String? procedureName; + + RefundableInvoiceDetailsItem({ + this.lineItemNo, + this.patientPaidAmount, + this.procedureId, + this.procedureName, + }); + + factory RefundableInvoiceDetailsItem.fromJson(Map json) { + return RefundableInvoiceDetailsItem( + lineItemNo: json['LineItemNo'] as int?, + patientPaidAmount: (json['PatientPaidAmount'] as num?)?.toDouble(), + procedureId: json['ProcedureID'] as String?, + procedureName: json['ProcedureName'] as String?, + ); + } +} + +class RefundableInvoicesDetailsObject { + final String? errorCode; + final int? invoiceNo; + final String? message; + final int? projectId; + final List refundableInvoiceDetailsItems; + final int? statusCode; + + RefundableInvoicesDetailsObject({ + this.errorCode, + this.invoiceNo, + this.message, + this.projectId, + this.refundableInvoiceDetailsItems = const [], + this.statusCode, + }); + + factory RefundableInvoicesDetailsObject.fromJson(Map json) { + return RefundableInvoicesDetailsObject( + errorCode: json['ErrorCode'] as String?, + invoiceNo: json['InvoiceNo'] as int?, + message: json['Message'] as String?, + projectId: json['ProjectID'] as int?, + refundableInvoiceDetailsItems: json['RefundableInvoiceDetailsItems'] != null + ? (json['RefundableInvoiceDetailsItems'] as List) + .map((item) => RefundableInvoiceDetailsItem.fromJson(item as Map)) + .toList() + : [], + statusCode: json['StatusCode'] as int?, + ); + } +} + +class RefundableAdvancesItem { + final String? advanceDate; + final int? advanceNo; + final double? balanceAmount; + final double? paidAmount; + final int? patientId; + final String? patientName; + final int? projectId; + + RefundableAdvancesItem({ + this.advanceDate, + this.advanceNo, + this.balanceAmount, + this.paidAmount, + this.patientId, + this.patientName, + this.projectId, + }); + + factory RefundableAdvancesItem.fromJson(Map json) { + return RefundableAdvancesItem( + advanceDate: json['AdvanceDate'] as String?, + advanceNo: json['AdvanceNo'] as int?, + balanceAmount: (json['BalanceAmount'] as num?)?.toDouble(), + paidAmount: (json['PaidAmount'] as num?)?.toDouble(), + patientId: json['PatientID'] as int?, + patientName: json['PatientName'] as String?, + projectId: json['ProjectID'] as int?, + ); + } +} + +class RefundableAdvancesObject { + final String? errorCode; + final String? message; + final List refundableAdvancesItems; + final int? statusCode; + + RefundableAdvancesObject({ + this.errorCode, + this.message, + this.refundableAdvancesItems = const [], + this.statusCode, + }); + + factory RefundableAdvancesObject.fromJson(Map json) { + return RefundableAdvancesObject( + errorCode: json['ErrorCode'] as String?, + message: json['Message'] as String?, + refundableAdvancesItems: json['RefundableAdvancesItems'] != null + ? (json['RefundableAdvancesItems'] as List).map((item) => RefundableAdvancesItem.fromJson(item as Map)).toList() + : [], + statusCode: json['StatusCode'] as int?, + ); + } +} diff --git a/lib/features/refund_request/refund_request_repo.dart b/lib/features/refund_request/refund_request_repo.dart new file mode 100644 index 00000000..eaf7d9e0 --- /dev/null +++ b/lib/features/refund_request/refund_request_repo.dart @@ -0,0 +1,360 @@ +import 'package:dartz/dartz.dart'; +import 'package:hmg_patient_app_new/core/api/api_client.dart'; +import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; +import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/req_models/submit_refund_request_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_model.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +abstract class RefundRequestRepo { + Future>>> getRefundableInvoices({required int projectID}); + + Future>> getRefundableInvoicesDetails({ + required int invoiceNo, + required int projectId, + }); + + Future>> getRefundableAdvances(); + + Future>> getRefundStatus({required String referenceNumber}); + + Future>>> getRefundInvoices({ + required int refundType, + required String patientId, + String invoiceNumberQuery, + }); + + Future>> submitRefund({required SubmitRefundRequestModel request}); + + Future>> checkIsRefundableInvoice({ + required int invoiceNo, + required int projectId, + }); +} + +class RefundRequestRepoImp implements RefundRequestRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + RefundRequestRepoImp({required this.apiClient, required this.loggerService}); + + @override + Future>>> getRefundableInvoices({required int projectID}) async { + Map body = {"ProjectID": projectID}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getRefundableInvoices, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final refundableInvoicesObject = response['RefundableInvoicesObject']; + + if (refundableInvoicesObject != null) { + final invoiceItemsJson = refundableInvoicesObject['RefundableInvoiceItems']; + + final List invoicesList = invoiceItemsJson != null + ? (invoiceItemsJson as List).map((item) => RefundableInvoiceItem.fromJson(item as Map)).toList() + : []; + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: invoicesList, + ); + } else { + failure = DataParsingFailure("RefundableInvoicesObject is null"); + } + } catch (e) { + loggerService.logError("getRefundableInvoices parsing error: $e"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("getRefundableInvoices exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getRefundableInvoicesDetails({ + required int invoiceNo, + required int projectId, + }) async { + final Map body = { + "InvoiceNo": invoiceNo, + "ProjectID": projectId, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getRefundableInvoicesDetails, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final detailsJson = response['RefundableInvoicesDetailsObject']; + final RefundableInvoicesDetailsObject details = detailsJson != null + ? RefundableInvoicesDetailsObject.fromJson(detailsJson as Map) + : RefundableInvoicesDetailsObject(); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: details, + ); + } catch (e) { + loggerService.logError("getRefundableInvoicesDetails parsing error: $e"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("getRefundableInvoicesDetails exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getRefundableAdvances() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getRefundableAdvances, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final advancesJson = response['RefundableAdvancesObject']; + final RefundableAdvancesObject advances = + advancesJson != null ? RefundableAdvancesObject.fromJson(advancesJson as Map) : RefundableAdvancesObject(); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: advances, + ); + } catch (e) { + loggerService.logError("getRefundableAdvances parsing error: $e"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("getRefundableAdvances exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getRefundStatus({required String referenceNumber}) async { + final Map body = { + "Refund_ReferenceNumber": referenceNumber, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getRefundStatus, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final dataJson = response['Refund_StatusItem']; + final RefundStatusModel model = dataJson != null ? RefundStatusModel.fromJson(dataJson as Map) : RefundStatusModel(); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: model, + ); + } catch (e) { + loggerService.logError("getRefundStatus parsing error: $e"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("getRefundStatus exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getRefundInvoices({ + required int refundType, + required String patientId, + String invoiceNumberQuery = "", + }) async { + final Map body = { + "Refund_RefundType": refundType, + "Refund_PatientId": patientId, + "Refund_InvoiceNumberQuery": invoiceNumberQuery, + }; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getRefundInvoices, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final listJson = response['Refund_InvoicesList']; + final List invoices = + listJson != null ? (listJson as List).map((item) => RefundInvoiceModel.fromJson(item as Map)).toList() : []; + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: invoices, + ); + } catch (e) { + loggerService.logError("getRefundInvoices parsing error: $e"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("getRefundInvoices exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> submitRefund({required SubmitRefundRequestModel request}) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.submitRefund, + body: request.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final model = SubmitRefundResponseModel.fromJson(response as Map); + apiResponse = GenericApiModel( + messageStatus: 1, + statusCode: statusCode, + errorMessage: errorMessage, + data: model, + ); + } catch (e) { + loggerService.logError("submitRefund parsing error: $e"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("submitRefund exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> checkIsRefundableInvoice({ + required int invoiceNo, + required int projectId, + }) async { + final Map body = { + "InvoiceNo": invoiceNo.toString(), + "ProjectID": projectId, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.isRefundableInvoice, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final dataJson = response['IsRefundableInvoiceObject']; + final IsRefundableInvoiceObject model = + dataJson != null ? IsRefundableInvoiceObject.fromJson(dataJson as Map) : IsRefundableInvoiceObject(); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: model, + ); + } catch (e) { + loggerService.logError("checkIsRefundableInvoice parsing error: $e"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("checkIsRefundableInvoice exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } +} diff --git a/lib/features/refund_request/refund_request_view_model.dart b/lib/features/refund_request/refund_request_view_model.dart new file mode 100644 index 00000000..6c9b58dd --- /dev/null +++ b/lib/features/refund_request/refund_request_view_model.dart @@ -0,0 +1,569 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/utils/validation_utils.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/req_models/submit_refund_request_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/refund_request_repo.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; + +enum RefundTabType { invoices, hmgWallet } + +extension RefundTabTypeExtension on RefundTabType { + String toDisplayString() { + switch (this) { + case RefundTabType.hmgWallet: + return LocaleKeys.hmgWallet.tr(); + case RefundTabType.invoices: + return LocaleKeys.bankTransfer.tr(); + } + } +} + +/// Lightweight model representing a refundable procedure line item. +class RefundableProcedure { + final int procedureId; + final String procedureName; + final String? procedureNameN; + final double patientPaidAmount; + bool isSelected; + String? refundReason; + + /// Raw values from the API — used when building the SubmitRefund payload + final int? apiLineItemNo; + final String? apiProcedureId; + + RefundableProcedure({ + required this.procedureId, + required this.procedureName, + this.procedureNameN, + required this.patientPaidAmount, + this.isSelected = false, + this.refundReason, + this.apiLineItemNo, + this.apiProcedureId, + }); +} + +/// Lightweight model for an invoice dropdown item. +class RefundInvoiceItem { + final int invoiceNo; + final int appointmentNo; + final int projectId; + final String displayLabel; + + const RefundInvoiceItem({ + required this.invoiceNo, + required this.appointmentNo, + required this.projectId, + required this.displayLabel, + }); +} + +class RefundRequestViewModel extends ChangeNotifier { + final AppState appState; + final RefundRequestRepo refundRequestRepo; + final ErrorHandlerService errorHandlerService; + + RefundRequestViewModel({ + required this.appState, + required this.refundRequestRepo, + required this.errorHandlerService, + }); + + // ── Tab ────────────────────────────────────────────────────────────────── + RefundTabType selectedTab = RefundTabType.invoices; + + void setTab(RefundTabType tab) { + selectedTab = tab; + notifyListeners(); + } + + // ── Step (0 = select invoice+hospital, 1 = select procedures) ──────────── + int currentStep = 0; + + void goToStep(int step) { + currentStep = step; + notifyListeners(); + } + + void nextStep() { + if (currentStep < 1) { + currentStep++; + notifyListeners(); + } + } + + void previousStep() { + if (currentStep > 0) { + currentStep--; + notifyListeners(); + } + } + + void resetState() { + currentStep = 0; + procedures.clear(); + selectedPaymentMethod = null; + selectedRefundReason = null; + isProceduresLoading = false; + isInvoiceDetailsLoading = false; + invoiceDetails = null; + _ibanError = null; + _descriptionError = null; + // Use Future.microtask to avoid calling notifyListeners while tree is locked (e.g. during dispose) + Future.microtask(() { + if (hasListeners) notifyListeners(); + }); + } + + // ── Hospital selection ──────────────────────────────────────────────────── + HospitalsModel? selectedHospital; + + void setSelectedHospital(HospitalsModel? hospital) { + selectedHospital = hospital; + // Reset invoice and procedures whenever hospital changes + selectedInvoice = null; + procedures.clear(); + refundableInvoicesList.clear(); + notifyListeners(); + + // Fetch refundable invoices for the selected hospital's project + if (hospital != null && hospital.iD != null) { + final projectID = hospital.iD is int ? hospital.iD as int : int.tryParse(hospital.iD.toString()); + if (projectID != null) { + getRefundableInvoices(projectID: projectID); + } + } + } + + void clearHospital() { + selectedHospital = null; + selectedInvoice = null; + procedures.clear(); + notifyListeners(); + } + + // ── Invoice selection ───────────────────────────────────────────────────── + RefundInvoiceItem? selectedInvoice; + List refundableInvoicesList = []; + bool isRefundableInvoicesLoading = false; + + Future getRefundableInvoices({ + required int projectID, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + isRefundableInvoicesLoading = true; + refundableInvoicesList.clear(); + notifyListeners(); + + final result = await refundRequestRepo.getRefundableInvoices(projectID: projectID); + + result.fold( + (failure) { + isRefundableInvoicesLoading = false; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (apiResponse) { + isRefundableInvoicesLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + refundableInvoicesList = apiResponse.data!; + notifyListeners(); + if (onSuccess != null) onSuccess(apiResponse); + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? "Failed to load invoices"); + } + } + }, + ); + } + + void setSelectedInvoice(RefundInvoiceItem invoice) { + selectedInvoice = invoice; + procedures.clear(); + notifyListeners(); + } + + void clearInvoice() { + selectedInvoice = null; + procedures.clear(); + notifyListeners(); + } + + // ── Procedures ──────────────────────────────────────────────────────────── + List procedures = []; + bool isProceduresLoading = false; + + void setProcedures(List list) { + procedures = list; + isProceduresLoading = false; + notifyListeners(); + } + + void toggleProcedureSelection(RefundableProcedure procedure) { + procedure.isSelected = !procedure.isSelected; + notifyListeners(); + } + + void setRefundReasonForProcedure(RefundableProcedure procedure, String reason) { + procedure.refundReason = reason; + notifyListeners(); + } + + bool get isAllSelected => procedures.isNotEmpty && procedures.every((p) => p.isSelected); + + void toggleSelectAll() { + final selectAll = !isAllSelected; + for (final p in procedures) { + p.isSelected = selectAll; + } + notifyListeners(); + } + + List get selectedProcedures => procedures.where((p) => p.isSelected).toList(); + + bool get allSelectedProceduresHaveReasons => + selectedProcedures.isNotEmpty && selectedProcedures.every((p) => p.refundReason != null && p.refundReason!.isNotEmpty); + + // ── Calculation ─────────────────────────────────────────────────────────── + double get totalRefundableAmount { + double total = 0.0; + for (final p in selectedProcedures) { + total += p.patientPaidAmount; + } + return total; + } + + // ── Bank Details & Description ──────────────────────────────────────────── + String bankDetails = ''; + String description = ''; + + // ── Bank Transfer Error Tracking ────────────────────────────────────────── + String? _ibanError; + String? _descriptionError; + + String? get ibanError => _ibanError; + + String? get descriptionError => _descriptionError; + + void clearIbanError() { + _ibanError = null; + notifyListeners(); + } + + void clearDescriptionError() { + _descriptionError = null; + notifyListeners(); + } + + bool validateBankTransferForm({required String iban, required String description}) { + bool isValid = true; + _ibanError = null; + _descriptionError = null; + + if (iban.trim().isEmpty) { + _ibanError = LocaleKeys.ibanIsRequired.tr(); + isValid = false; + } else { + if (!ValidationUtils.validateIban(iban.trim())) { + _ibanError = LocaleKeys.invalidIbanFormat.tr(); + isValid = false; + } + } + + if (description.trim().isEmpty) { + _descriptionError = LocaleKeys.descriptionIsRequired.tr(); + isValid = false; + } + + notifyListeners(); + return isValid; + } + + // ── Payment Method ──────────────────────────────────────────────────────── + RefundTabType? selectedPaymentMethod; + + void setPaymentMethod(RefundTabType method) { + selectedPaymentMethod = method; + notifyListeners(); + } + + // ── Refund Reason ───────────────────────────────────────────────────────── + String? selectedRefundReason; + + void setRefundReason(String? reason) { + selectedRefundReason = reason; + notifyListeners(); + } + + /// Used by text controllers in the UI layer to trigger a rebuild. + void triggerRebuild() => notifyListeners(); + + void setBankDetails(String value) { + bankDetails = value; + notifyListeners(); + } + + void setDescription(String value) { + description = value; + notifyListeners(); + } + + // ── Validation ──────────────────────────────────────────────────────────── + bool get isStep1Valid => selectedHospital != null && selectedInvoice != null; + + bool get isStep2Valid => selectedProcedures.isNotEmpty && selectedPaymentMethod != null; + + // ── Invoice Details ─────────────────────────────────────────────────────── + RefundableInvoicesDetailsObject? invoiceDetails; + bool isInvoiceDetailsLoading = false; + + Future getRefundableInvoicesDetails({ + required int invoiceNo, + required int projectId, + Function(RefundableInvoicesDetailsObject)? onSuccess, + Function(String)? onError, + }) async { + isInvoiceDetailsLoading = true; + isProceduresLoading = true; + invoiceDetails = null; + procedures.clear(); + notifyListeners(); + + final result = await refundRequestRepo.getRefundableInvoicesDetails(invoiceNo: invoiceNo, projectId: projectId); + + result.fold( + (failure) { + isInvoiceDetailsLoading = false; + isProceduresLoading = false; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (apiResponse) { + isInvoiceDetailsLoading = false; + isProceduresLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + invoiceDetails = apiResponse.data; + notifyListeners(); + if (onSuccess != null) onSuccess(apiResponse.data!); + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? LocaleKeys.failed.tr()); + } + } + }, + ); + } + + // ── Refundable Advances ─────────────────────────────────────────────────── + RefundableAdvancesObject? refundableAdvances; + bool isRefundableAdvancesLoading = false; + + Future getRefundableAdvances({ + Function(RefundableAdvancesObject)? onSuccess, + Function(String)? onError, + }) async { + isRefundableAdvancesLoading = true; + refundableAdvances = null; + notifyListeners(); + + final result = await refundRequestRepo.getRefundableAdvances(); + + result.fold( + (failure) { + isRefundableAdvancesLoading = false; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (apiResponse) { + isRefundableAdvancesLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + refundableAdvances = apiResponse.data; + notifyListeners(); + if (onSuccess != null) onSuccess(apiResponse.data!); + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? "Failed to load refundable advances"); + } + } + }, + ); + } + + // ── Refund Status ───────────────────────────────────────────────────────── + RefundStatusModel? refundStatus; + bool isRefundStatusLoading = false; + + Future getRefundStatus({ + required String referenceNumber, + Function(RefundStatusModel)? onSuccess, + Function(String)? onError, + }) async { + isRefundStatusLoading = true; + refundStatus = null; + notifyListeners(); + + final result = await refundRequestRepo.getRefundStatus(referenceNumber: referenceNumber); + + result.fold( + (failure) { + isRefundStatusLoading = false; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (apiResponse) { + isRefundStatusLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + refundStatus = apiResponse.data; + notifyListeners(); + if (onSuccess != null) onSuccess(apiResponse.data!); + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? "Failed to load refund status"); + } + } + }, + ); + } + + // ── Refund Invoices ─────────────────────────────────────────────────────── + List refundInvoicesList = []; + bool isRefundInvoicesLoading = false; + + Future getRefundInvoices({ + required int refundType, + String invoiceNumberQuery = "", + Function(List)? onSuccess, + Function(String)? onError, + }) async { + isRefundInvoicesLoading = true; + refundInvoicesList.clear(); + notifyListeners(); + + final resolvedPatientId = appState.getAuthenticatedUser()?.patientId?.toString() ?? ""; + final result = await refundRequestRepo.getRefundInvoices( + refundType: refundType, + patientId: resolvedPatientId, + invoiceNumberQuery: invoiceNumberQuery, + ); + + result.fold( + (failure) { + isRefundInvoicesLoading = false; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (apiResponse) { + isRefundInvoicesLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + refundInvoicesList = apiResponse.data!; + notifyListeners(); + if (onSuccess != null) onSuccess(apiResponse.data!); + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? "Failed to load refund invoices"); + } + } + }, + ); + } + + // ── Submit Refund ───────────────────────────────────────────────────────── + bool isSubmitting = false; + + Future submitInvoiceRefundRequest({ + required SubmitRefundRequestModel request, + Function(SubmitRefundResponseModel)? onSuccess, + Function(String)? onError, + }) async { + isSubmitting = true; + notifyListeners(); + + final result = await refundRequestRepo.submitRefund(request: request); + + result.fold( + (failure) { + isSubmitting = false; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (apiResponse) { + isSubmitting = false; + notifyListeners(); + if (apiResponse.data != null) { + if (onSuccess != null) onSuccess(apiResponse.data!); + } else { + if (onError != null) { + onError(apiResponse.errorMessage ?? "Failed to submit refund"); + } + } + }, + ); + } + + // ── Check Is Refundable Invoice ─────────────────────────────────────────── + + Future checkIsRefundableInvoice({ + required int invoiceNo, + required int projectId, + Function(IsRefundableInvoiceObject)? onSuccess, + Function(String)? onError, + }) async { + final result = await refundRequestRepo.checkIsRefundableInvoice(invoiceNo: invoiceNo, projectId: projectId); + + result.fold( + (failure) { + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (apiResponse) { + if (apiResponse.data != null) { + if (onSuccess != null) onSuccess(apiResponse.data!); + } else { + if (onError != null) { + onError(apiResponse.errorMessage ?? "Failed to check refundable status"); + } + } + }, + ); + } +} diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 34787232..7700984d 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -2,7 +2,7 @@ // ignore_for_file: constant_identifier_names -abstract class LocaleKeys { +abstract class LocaleKeys { static const english = 'english'; static const arabic = 'arabic'; static const login = 'login'; @@ -1156,6 +1156,7 @@ abstract class LocaleKeys { static const transportOptions = 'transportOptions'; static const selectHospitalForAdvancePayment = 'selectHospitalForAdvancePayment'; static const recharge = 'recharge'; + static const withdraw = 'withdraw'; static const activityLevel = 'activityLevel'; static const selectActivityLevel = 'selectActivityLevel'; static const almostInactive = 'almostInactive'; @@ -1838,10 +1839,44 @@ abstract class LocaleKeys { static const fileSizeExceedsLimit = 'fileSizeExceedsLimit'; static const completedPrescriptionOrder = 'completedPrescriptionOrder'; static const submitEReferral = 'submitEReferral'; + static const requestRefund = 'requestRefund'; + static const uploadImage = 'uploadImage'; + static const selectImage = 'selectImage'; + static const selectOptions = 'selectOptions'; + static const iban = 'iban'; + static const addIbanDetails = 'addIbanDetails'; + static const enterDescription = 'enterDescription'; + static const ibanIsRequired = 'ibanIsRequired'; + static const descriptionIsRequired = 'descriptionIsRequired'; + static const invalidIbanFormat = 'invalidIbanFormat'; + static const refundHistory = 'refundHistory'; + static const noRefundHistoryFound = 'noRefundHistoryFound'; + static const selectProcedures = 'selectProcedures'; + static const noProceduresAvailableForRefund = 'noProceduresAvailableForRefund'; + static const selectAll = 'selectAll'; + static const selectRefundMethod = 'selectRefundMethod'; + static const hmgWallet = 'hmgWallet'; + static const bankTransfer = 'bankTransfer'; + static const selectRefundReason = 'selectRefundReason'; + static const refundReason = 'refundReason'; + static const refundReason1 = 'refundReason1'; + static const refundReason2 = 'refundReason2'; + static const refundReason3 = 'refundReason3'; + static const refundReason4 = 'refundReason4'; + static const refundReason5 = 'refundReason5'; + static const refundReason6 = 'refundReason6'; + static const refundReason7 = 'refundReason7'; static const redeem = 'redeem'; static const points = 'points'; static const transactions = 'transactions'; static const pharmacy = 'pharmacy'; static const visitsOrders = 'visitsOrders'; - + static const cannotOpenThisFile = 'cannotOpenThisFile'; + static const thisInvoiceIsNotEligibleForRefund = 'thisInvoiceIsNotEligibleForRefund'; + static const refundDetails = 'refundDetails'; + static const requestNo = 'requestNo'; + static const referenceNo = 'referenceNo'; + static const refund = 'refund'; + static const advanceNumber = 'advanceNumber'; + static const amountOnly = 'amountOnly'; } diff --git a/lib/main.dart b/lib/main.dart index e2643438..ae7f7036 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,18 +10,16 @@ import 'package:flutter/services.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; -import 'package:hmg_patient_app_new/features/paytabs/paytabs_view_model.dart'; -import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/ask_doctor/ask_doctor_view_model.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; -import 'package:hmg_patient_app_new/features/active_prescriptions/active_prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/contact_us/contact_us_view_model.dart'; import 'package:hmg_patient_app_new/features/doctor_filter/doctor_filter_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/health_trackers/health_trackers_view_model.dart'; import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; @@ -37,27 +35,25 @@ import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model. import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart'; import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; +import 'package:hmg_patient_app_new/features/paytabs/paytabs_view_model.dart'; import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_model.dart'; import 'package:hmg_patient_app_new/features/profile_picture/profile_picture_view_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/features/qr_parking/qr_parking_view_model.dart'; import 'package:hmg_patient_app_new/features/radiology/radiology_view_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart'; import 'package:hmg_patient_app_new/features/services_price_list/services_price_list_view_model.dart'; import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart'; import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; -import 'package:hmg_patient_app_new/features/translations/custom_asset_loader.dart'; -import 'package:hmg_patient_app_new/features/translations/translation_service.dart'; import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; +import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculator_view_model.dart'; -import 'package:hmg_patient_app_new/features/health_trackers/health_trackers_view_model.dart'; import 'package:hmg_patient_app_new/routes/app_routes.dart'; -import 'package:hmg_patient_app_new/services/analytics/analytics_service.dart'; import 'package:hmg_patient_app_new/services/app_lifecycle_service.dart'; 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/theme/app_theme.dart'; -import 'package:hmg_patient_app_new/theme/colors.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' show DateRangeSelectorRangeViewModel; import 'package:provider/provider.dart'; @@ -75,7 +71,6 @@ Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { // flutter3_32 pub run easy_localization:generate -O ./lib/generated -f keys -o locale_keys.g.dart --source-dir ./assets/langs - class MyHttpOverrides extends HttpOverrides { @override HttpClient createHttpClient(SecurityContext? context) { @@ -269,10 +264,15 @@ void main() async { ), ChangeNotifierProvider( create: (_) => getIt.get(), - ), ChangeNotifierProvider( + ), + ChangeNotifierProvider( create: (_) => getIt.get(), - ), ChangeNotifierProvider( + ), + ChangeNotifierProvider( create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), ) ], child: MyApp()), ), // Wrap your app diff --git a/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart b/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart index 0761ab6c..06056764 100644 --- a/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart +++ b/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart @@ -376,7 +376,7 @@ class _ComprehensiveCheckupPageState extends State { borderRadius: 24.r, hasShadow: true, ), - padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 12.h), + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 12.h, bottom: 16.h), child: CustomButton( borderWidth: 0, text: LocaleKeys.next.tr(context: context), diff --git a/lib/presentation/habib_wallet/habib_wallet_page.dart b/lib/presentation/habib_wallet/habib_wallet_page.dart index 42ad5984..db5990d1 100644 --- a/lib/presentation/habib_wallet/habib_wallet_page.dart +++ b/lib/presentation/habib_wallet/habib_wallet_page.dart @@ -3,17 +3,18 @@ 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'; +import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; -import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/recharge_wallet_page.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/presentation/habib_wallet/withdraw_request_create_page.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; @@ -42,7 +43,7 @@ class _HabibWalletState extends State { @override Widget build(BuildContext context) { habibWalletVM = Provider.of(context, listen: false); - AppState _appState = getIt.get(); + AppState appState = getIt.get(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( @@ -57,11 +58,10 @@ class _HabibWalletState extends State { width: double.infinity, height: 180.h, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - // color: AppColors.blackBgColor, - color: Color(0xFF2E3039), - borderRadius: 24.r, - hasShadow: true - ), + // color: AppColors.blackBgColor, + color: Color(0xFF2E3039), + borderRadius: 24.r, + hasShadow: true), child: Padding( padding: EdgeInsets.all(16.h), child: Column( @@ -74,8 +74,9 @@ class _HabibWalletState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - "${_appState.getAuthenticatedUser()!.firstName!} ${_appState.getAuthenticatedUser()!.lastName!}".toText19(isBold: true, color: Colors.white), - "${LocaleKeys.fileno.tr(context: context)}: ${_appState.getAuthenticatedUser()!.patientId!}" + "${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), ], ).expanded, @@ -86,13 +87,8 @@ class _HabibWalletState extends State { LocaleKeys.balanceAmount.tr(context: context).toText14(isBold: true, color: Colors.white), SizedBox(height: 4.h), Consumer(builder: (context, habibWalletVM, child) { - return Utils.getPaymentAmountWithSymbol2( - habibWalletVM.habibWalletAmount, - textColor: Colors.white, - iconColor: Colors.white, - iconSize: 16, - isExpanded: false, - letterSpacing: -1) + return Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletAmount, + textColor: Colors.white, iconColor: Colors.white, iconSize: 16, isExpanded: false, letterSpacing: -1) .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.h, width: 80.h, height: 24.h); }), ], @@ -129,6 +125,23 @@ class _HabibWalletState extends State { }, ), ), + SizedBox(width: 8.w), + Flexible( + child: CustomButton( + height: 40.h, + icon: AppAssets.refundIcon, + iconSize: 24.w, + backgroundColor: AppColors.successColor, + textColor: Colors.white, + text: LocaleKeys.withdraw.tr(context: context), + borderWidth: 0.w, + fontWeight: FontWeight.w600, + borderColor: Colors.transparent, + padding: EdgeInsets.fromLTRB(4, 0, 12, 0), + fontSize: 14.f, + onPressed: () => Navigator.of(context).push(CustomPageRoute(page: WithdrawRequestCreatePage())), + ), + ), ], ), SizedBox(height: 16.h), @@ -148,9 +161,18 @@ class _HabibWalletState extends State { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: habibWalletVM.habibWalletBalanceList[index].projectDescription!.toText16(isBold: true, color: AppColors.textColor)), - Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletBalanceList[index].patientAdvanceBalanceAmount!, //did the changes because of negative value - textColor: AppColors.textColor, iconColor: AppColors.textColor, iconSize: 18.h, isExpanded: false, fontSize: 28.f, fontWeight: FontWeight.w600, letterSpacing: -1), + Expanded( + child: habibWalletVM.habibWalletBalanceList[index].projectDescription! + .toText16(isBold: true, color: AppColors.textColor)), + Utils.getPaymentAmountWithSymbol2( + habibWalletVM.habibWalletBalanceList[index].patientAdvanceBalanceAmount!, //did the changes because of negative value + textColor: AppColors.textColor, + iconColor: AppColors.textColor, + iconSize: 18.h, + isExpanded: false, + fontSize: 28.f, + fontWeight: FontWeight.w600, + letterSpacing: -1), ], ).paddingSymmetrical(0, 12.h); }, diff --git a/lib/presentation/habib_wallet/recharge_wallet_page.dart b/lib/presentation/habib_wallet/recharge_wallet_page.dart index b7feaadc..2810b221 100644 --- a/lib/presentation/habib_wallet/recharge_wallet_page.dart +++ b/lib/presentation/habib_wallet/recharge_wallet_page.dart @@ -11,8 +11,8 @@ import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_mode import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/wallet_payment_confirm_page.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; @@ -44,7 +44,9 @@ class _RechargeWalletPageState extends State { // Clear errors when entering recharge wallet page WidgetsBinding.instance.addPostFrameCallback((_) { habibWalletVM = context.read(); - habibWalletVM.setDepositorDetails(appState.getAuthenticatedUser()!.patientId.toString(), "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}", + habibWalletVM.setDepositorDetails( + appState.getAuthenticatedUser()!.patientId.toString(), + "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}", appState.getAuthenticatedUser()!.mobileNumber!); habibWalletVM.setSelectedRechargeType(0); habibWalletVM.getProjectsList(); @@ -97,262 +99,272 @@ class _RechargeWalletPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Consumer( - builder: (context, viewModel, child) { - return Container( - height: 135.h, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: false, - side: BorderSide( - color: viewModel.amountError != null ? AppColors.primaryRedBorderColor : AppColors.textColor, - width: 2.h, - ), - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - //TODO: Check with hussain to show AED or SAR - LocaleKeys.amount.tr(context: context).toText14(color: AppColors.greyTextColor, isBold: true), - Spacer(), - Row( - crossAxisAlignment: CrossAxisAlignment.end, + Consumer( + builder: (context, viewModel, child) { + return Container( + height: 135.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + side: BorderSide( + color: viewModel.amountError != null ? AppColors.primaryRedBorderColor : AppColors.textColor, + width: 2.h, + ), + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: 150.h, - child: TextInputWidget( - controller: amountTextController, - labelText: "", - hintText: "", - isEnable: true, - prefix: null, - isAllowRadius: true, - isBorderAllowed: false, - isAllowLeadingIcon: true, - autoFocus: true, - fontSize: 25.f, - padding: EdgeInsets.symmetric(horizontal: 8.h, vertical: 0.h), - focusNode: _amountFocusNode, - isWalletAmountInput: true, - keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), - fontFamily: "Poppins", - onChange: (value) { - // Clear error when user starts typing - viewModel.clearAmountError(); - }, - // leadingIcon: AppAssets.student_card, - ), + //TODO: Check with hussain to show AED or SAR + LocaleKeys.amount.tr(context: context).toText14(color: AppColors.greyTextColor, isBold: true), + Spacer(), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SizedBox( + width: 150.h, + child: TextInputWidget( + controller: amountTextController, + labelText: "", + hintText: "", + isEnable: true, + prefix: null, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + autoFocus: true, + fontSize: 25.f, + padding: EdgeInsets.symmetric(horizontal: 8.h, vertical: 0.h), + focusNode: _amountFocusNode, + isWalletAmountInput: true, + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), + fontFamily: "Poppins", + onChange: (value) { + // Clear error when user starts typing + viewModel.clearAmountError(); + }, + // leadingIcon: AppAssets.student_card, + ), + ), + const Spacer(), + LocaleKeys.sar.tr(context: context).toText20(color: AppColors.greyTextColor, isBold: true), + ], ), - const Spacer(), - LocaleKeys.sar.tr(context: context).toText20(color: AppColors.greyTextColor, isBold: true), ], ), - ], - ), - ), - ); - }, - ), - // Show error message if exists - Consumer( - builder: (context, viewModel, child) { - if (viewModel.amountError != null) { - return Padding( - padding: EdgeInsets.only(left: 16.w, top: 8.h), - child: Text( - viewModel.amountError!, - style: TextStyle( - color: AppColors.primaryRedColor, - fontSize: 12.f, ), - ), - ); - } - return SizedBox.shrink(); - }, - ), - SizedBox(height: 24.h), - Consumer(builder: (context, habibWalletVM, child) { - return Container( - decoration: BoxDecoration( - color: AppColors.whiteColor, - borderRadius: BorderRadius.circular(24.h), - border: Border.all( - color: habibWalletVM.hospitalError != null ? AppColors.primaryRedBorderColor : Colors.transparent, - width: 2.h, - ), + ); + }, ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + // Show error message if exists + Consumer( + builder: (context, viewModel, child) { + if (viewModel.amountError != null) { + return Padding( + padding: EdgeInsets.only(left: 16.w, top: 8.h), + child: Text( + viewModel.amountError!, + style: TextStyle( + color: AppColors.primaryRedColor, + fontSize: 12.f, + ), + ), + ); + } + return SizedBox.shrink(); + }, + ), + SizedBox(height: 24.h), + Consumer(builder: (context, habibWalletVM, child) { + return Container( + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.h), + border: Border.all( + color: habibWalletVM.hospitalError != null ? AppColors.primaryRedBorderColor : Colors.transparent, + width: 2.h, + ), + ), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.my_account_icon, width: 40.h, height: 40.h, applyThemeColor: false), - SizedBox(width: 8.h), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + Row( children: [ - (habibWalletVM.getSelectedRechargeTypeValue()).toText12(color: AppColors.greyTextColor, isBold: true), - "${LocaleKeys.medicalFile.tr(context: context)}: ${habibWalletVM.fileNumber}" - .toText14(color: AppColors.textColor, isBold: true, letterSpacing: -0.2, isEnglishOnly: true), + Utils.buildSvgWithAssets( + icon: AppAssets.my_account_icon, width: 40.h, height: 40.h, applyThemeColor: false), + SizedBox(width: 8.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (habibWalletVM.getSelectedRechargeTypeValue()).toText12(color: AppColors.greyTextColor, isBold: true), + "${LocaleKeys.medicalFile.tr(context: context)}: ${habibWalletVM.fileNumber}" + .toText14(color: AppColors.textColor, isBold: true, letterSpacing: -0.2, isEnglishOnly: true), + ], + ), ], ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), ], - ), - Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), - ], - ).onPress(() async { - habibWalletVM.setCurrentIndex(0); - showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.medicalFile.tr(context: context), - titleWidget: Consumer(builder: (context, habibWalletVM, child) { - return habibWalletVM.currentIndex != 0 - ? IconButton( - icon: Transform.flip(flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.arrow_back, width: 24.h, height: 24.h)), - padding: EdgeInsets.zero, - onPressed: () => habibWalletVM.setCurrentIndex(0), - highlightColor: Colors.transparent, - ) - : LocaleKeys.medicalFile.tr(context: context).toText20(isBold: true); - }), child: Consumer(builder: (context, habibWalletVM, child) { - return MultiPageBottomSheet(); - }), callBackFunc: () {}, isFullScreen: false, isCloseButtonVisible: true); - }), - SizedBox(height: 16.h), - Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), - SizedBox(height: 16.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + ).onPress(() async { + habibWalletVM.setCurrentIndex(0); + showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.medicalFile.tr(context: context), + titleWidget: Consumer(builder: (context, habibWalletVM, child) { + return habibWalletVM.currentIndex != 0 + ? IconButton( + icon: Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets(icon: AppAssets.arrow_back, width: 24.h, height: 24.h)), + padding: EdgeInsets.zero, + onPressed: () => habibWalletVM.setCurrentIndex(0), + highlightColor: Colors.transparent, + ) + : LocaleKeys.medicalFile.tr(context: context).toText20(isBold: true); + }), child: Consumer(builder: (context, habibWalletVM, child) { + return MultiPageBottomSheet(); + }), callBackFunc: () {}, isFullScreen: false, isCloseButtonVisible: true); + }), + SizedBox(height: 16.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), + SizedBox(height: 16.h), Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.select_hospital_icon, width: 40.h, height: 40.h, applyThemeColor: false), - SizedBox(width: 8.h), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + Row( children: [ - LocaleKeys.hospital.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), - SizedBox( - width: MediaQuery.of(context).size.width * 0.55, - child: (habibWalletVM.selectedHospital != null ? habibWalletVM.selectedHospital!.name : LocaleKeys.selectHospital.tr(context: context))! - .toText14(color: AppColors.textColor, isBold: true), + Utils.buildSvgWithAssets( + icon: AppAssets.select_hospital_icon, width: 40.h, height: 40.h, applyThemeColor: false), + SizedBox(width: 8.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.hospital.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), + SizedBox( + width: MediaQuery.of(context).size.width * 0.55, + child: (habibWalletVM.selectedHospital != null + ? habibWalletVM.selectedHospital!.name + : LocaleKeys.selectHospital.tr(context: context))! + .toText14(color: AppColors.textColor, isBold: true), + ), + ], ), ], ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), ], - ), - Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), - ], - ).onPress(() { - showCommonBottomSheetWithoutHeight(context, - title: LocaleKeys.selectHospital.tr(context: context), isDismissible: false, child: SelectHospitalBottomSheet(), callBackFunc: () {}); - }), - // Show hospital error message if exists - if (habibWalletVM.hospitalError != null) - Padding( - padding: EdgeInsets.only(top: 8.h), - child: Text( - habibWalletVM.hospitalError!, - style: TextStyle( - color: AppColors.primaryRedColor, - fontSize: 12.f, + ).onPress(() { + showCommonBottomSheetWithoutHeight(context, + title: LocaleKeys.selectHospital.tr(context: context), + isDismissible: false, + child: SelectHospitalBottomSheet(), + callBackFunc: () {}); + }), + // Show hospital error message if exists + if (habibWalletVM.hospitalError != null) + Padding( + padding: EdgeInsets.only(top: 8.h), + child: Text( + habibWalletVM.hospitalError!, + style: TextStyle( + color: AppColors.primaryRedColor, + fontSize: 12.f, + ), + ), ), - ), - ), - SizedBox(height: 16.h), - Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), - SizedBox(height: 16.h), - Row( - children: [ - Utils.buildSvgWithAssets(icon: AppAssets.email_icon, width: 40.h, height: 40.h, applyThemeColor: false), - SizedBox(width: 8.h), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + SizedBox(height: 16.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), + SizedBox(height: 16.h), + Row( children: [ - LocaleKeys.email.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), - "${appState.getAuthenticatedUser()!.emailAddress}".toText14(color: AppColors.textColor, isBold: true, letterSpacing: -0.2, isEnglishOnly: true), + Utils.buildSvgWithAssets(icon: AppAssets.email_icon, width: 40.h, height: 40.h, applyThemeColor: false), + SizedBox(width: 8.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.email.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), + "${appState.getAuthenticatedUser()!.emailAddress}" + .toText14(color: AppColors.textColor, isBold: true, letterSpacing: -0.2, isEnglishOnly: true), + ], + ), ], ), + SizedBox(height: 16.h), + Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), + SizedBox(height: 16.h), + TextInputWidget( + labelText: LocaleKeys.notes.tr(context: context), + hintText: "", + controller: notesTextController, + keyboardType: TextInputType.text, + isEnable: true, + prefix: null, + autoFocus: false, + isAllowRadius: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + leadingIcon: AppAssets.notes_icon, + hasError: false, + ), + SizedBox(height: 8.h), ], ), - SizedBox(height: 16.h), - Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), - SizedBox(height: 16.h), - TextInputWidget( - labelText: LocaleKeys.notes.tr(context: context), - hintText: "", - controller: notesTextController, - keyboardType: TextInputType.text, - isEnable: true, - prefix: null, - autoFocus: false, - isAllowRadius: true, - isBorderAllowed: false, - isAllowLeadingIcon: true, - leadingIcon: AppAssets.notes_icon, - hasError: false, - ), - SizedBox(height: 8.h), - ], - ), - ), - ); - }), - ], + ), + ); + }), + ], + ), + ), ), ), ), - ), - ), - //TODO: Handle Family member selection & Other Account Selection - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: false, - ), - child: CustomButton( - text: LocaleKeys.next.tr(context: context), - onPressed: () { - _amountFocusNode.unfocus(); - FocusScope.of(context).unfocus(); + //TODO: Handle Family member selection & Other Account Selection + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: CustomButton( + text: LocaleKeys.next.tr(context: context), + onPressed: () { + _amountFocusNode.unfocus(); + FocusScope.of(context).unfocus(); - // Use ViewModel validation method - if (habibWalletVM.validateRechargeForm(amountTextController.text)) { - habibWalletVM.setWalletRechargeAmount(num.parse(amountTextController.text.replaceAll(',', ''))); - habibWalletVM.setNotesText(notesTextController.text); - // habibWalletVM.setDepositorDetails(appState.getAuthenticatedUser()!.patientId.toString(), "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}", - // appState.getAuthenticatedUser()!.mobileNumber!); - Navigator.of(context).push( - CustomPageRoute( - page: WalletPaymentConfirmPage(), - ), - ); - } - }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 50.h, - icon: AppAssets.appointment_pay_icon, - iconColor: Colors.white, - iconSize: 18.h, - ).paddingSymmetrical(24.h, 24.h), + // Use ViewModel validation method + if (habibWalletVM.validateRechargeForm(amountTextController.text)) { + habibWalletVM.setWalletRechargeAmount(num.parse(amountTextController.text.replaceAll(',', ''))); + habibWalletVM.setNotesText(notesTextController.text); + // habibWalletVM.setDepositorDetails(appState.getAuthenticatedUser()!.patientId.toString(), "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}", + // appState.getAuthenticatedUser()!.mobileNumber!); + Navigator.of(context).push( + CustomPageRoute( + page: WalletPaymentConfirmPage(), + ), + ); + } + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 50.h, + icon: AppAssets.appointment_pay_icon, + iconColor: Colors.white, + iconSize: 18.h, + ).paddingSymmetrical(24.h, 24.h), + ), + ], ), - ], - ), - ), + ), ), ); } diff --git a/lib/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart b/lib/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart index e93b9cef..11a2ac30 100644 --- a/lib/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart +++ b/lib/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart @@ -19,7 +19,12 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; class SelectHospitalBottomSheet extends StatefulWidget { - const SelectHospitalBottomSheet({super.key}); + /// Optional callback invoked when the user picks a hospital. + /// When provided, the caller handles the selection instead of the default + /// [HabibWalletViewModel.setSelectedHospital] behaviour. + final void Function(HospitalsModel hospital)? onHospitalSelected; + + const SelectHospitalBottomSheet({super.key, this.onHospitalSelected}); @override State createState() => _SelectHospitalBottomSheetState(); @@ -71,6 +76,7 @@ class _SelectHospitalBottomSheetState extends State { _updateDisplayList(); } } + habibWalletVM.addListener(listener); habibWalletVM.getProjectsList(); } @@ -195,22 +201,26 @@ class _SelectHospitalBottomSheetState extends State { child: Center(child: Utils.getLoadingWidget()), ) : SizedBox( - height: MediaQuery.sizeOf(context).height * .4, - child: ListView.separated( - itemBuilder: (_, index) { - return HospitalListItemAdvancePayment( - hospitalModel: displayList[index], - isLocationEnabled: sortByLocation, - ).onPress(() { - habibWalletVM.setSelectedHospital(displayList[index]); - Navigator.of(context).pop(); - }); - }, - separatorBuilder: (_, __) => SizedBox( - height: 16.h, - ), - itemCount: displayList.length), - ) + height: MediaQuery.sizeOf(context).height * .4, + child: ListView.separated( + itemBuilder: (_, index) { + return HospitalListItemAdvancePayment( + hospitalModel: displayList[index], + isLocationEnabled: sortByLocation, + ).onPress(() { + if (widget.onHospitalSelected != null) { + widget.onHospitalSelected!(displayList[index]); + } else { + habibWalletVM.setSelectedHospital(displayList[index]); + } + Navigator.of(context).pop(); + }); + }, + separatorBuilder: (_, __) => SizedBox( + height: 16.h, + ), + itemCount: displayList.length), + ) ], ); } diff --git a/lib/presentation/habib_wallet/withdraw_request_create_page.dart b/lib/presentation/habib_wallet/withdraw_request_create_page.dart new file mode 100644 index 00000000..cc0c9731 --- /dev/null +++ b/lib/presentation/habib_wallet/withdraw_request_create_page.dart @@ -0,0 +1,445 @@ +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/presentation/refund_request/refund_history_screen.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/image_picker.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +class WithdrawRequestCreatePage extends StatefulWidget { + const WithdrawRequestCreatePage({super.key}); + + @override + State createState() => _WithdrawRequestCreatePageState(); +} + +class _WithdrawRequestCreatePageState extends State { + late HabibWalletViewModel _habibWalletViewModel; + late AppState _appState; + + final TextEditingController _bankDetailsController = TextEditingController(); + final TextEditingController _descriptionController = TextEditingController(); + final FocusNode _ibanFocusNode = FocusNode(); + final FocusNode _descriptionFocusNode = FocusNode(); + + @override + void initState() { + super.initState(); + _appState = getIt.get(); + _habibWalletViewModel = context.read(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _habibWalletViewModel.getProjectsList(); + }); + } + + @override + void dispose() { + _habibWalletViewModel.resetWithdrawForm(silent: true); + _bankDetailsController.dispose(); + _descriptionController.dispose(); + _ibanFocusNode.dispose(); + _descriptionFocusNode.dispose(); + super.dispose(); + } + + bool get _isFormValid { + final vm = _habibWalletViewModel; + return vm.selectedHospital != null && + !vm.isAdvancesLoading && + _bankDetailsController.text.trim().isNotEmpty && + _descriptionController.text.trim().isNotEmpty; + } + + // ── Bottom bar ──────────────────────────────────────────────────────────── + + Widget _buildBottomBar() { + return Consumer( + builder: (context, vm, _) { + final isValid = vm.selectedHospital != null && + !vm.isAdvancesLoading && + _bankDetailsController.text.trim().isNotEmpty && + _descriptionController.text.trim().isNotEmpty; + + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CustomButton( + text: LocaleKeys.submitRequest.tr(context: context), + isDisabled: !isValid, + onPressed: () { + final isFormValid = vm.validateWithdrawForm( + iban: _bankDetailsController.text, + description: _descriptionController.text, + ); + if (isFormValid) { + _handleWalletWithdrawalSubmit(vm); + } + }, + ).paddingSymmetrical(24.h, 24.h), + ], + ), + ); + }, + ); + } + + Widget _buildProjectSelectionAndBalance(HabibWalletViewModel habibWalletViewModel) { + final isArabic = _appState.isArabic(); + return Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.select_hospital_icon, + width: 40.h, + height: 40.h, + applyThemeColor: false, + ), + SizedBox(width: 8.h), + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + LocaleKeys.hospital.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), + (habibWalletViewModel.selectedHospital != null + ? ((isArabic + ? (habibWalletViewModel.selectedHospital!.nameN?.toString() ?? habibWalletViewModel.selectedHospital!.name) + : habibWalletViewModel.selectedHospital!.name) ?? + LocaleKeys.selectHospital.tr(context: context)) + : LocaleKeys.selectHospital.tr(context: context)) + .toText14(color: AppColors.textColor, isBold: true), + ]), + ], + ), + Utils.buildSvgWithAssets( + icon: AppAssets.arrow_down, + width: 25.h, + height: 25.h, + ), + ], + ).onPress(() { + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.selectHospital.tr(context: context), + isDismissible: false, + child: SelectHospitalBottomSheet( + onHospitalSelected: (hospital) { + habibWalletViewModel.setSelectedHospital(hospital); + habibWalletViewModel.fetchAdvancesForHospital(); + }, + ), + callBackFunc: () {}, + ); + }), + // Only show divider + amount once hospital is selected + if (habibWalletViewModel.selectedHospital != null) ...[ + Divider(color: AppColors.bottomNAVBorder, thickness: 1).paddingOnly(top: 16.h), + habibWalletViewModel.isAdvancesLoading + ? Container( + height: 60.h, + // width: 160.w, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(12.r), + ), + ).toShimmer2(isShow: true, radius: 12.r) + : Utils.getPaymentAmountWithSymbol( + habibWalletViewModel.totalAdvancesBalance.toStringAsFixed(2).toText44(isBold: true), + AppColors.blackColor, + 18.w, + isSaudiCurrency: true, + isExpanded: false, + ), + ], + ], + ), + ); + } + + Widget _buildBankInputDetails() { + return Consumer( + builder: (context, viewModel, _) { + final hasError = viewModel.ibanError != null || viewModel.withdrawDescriptionError != null; + return Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + border: Border.all( + color: hasError ? AppColors.primaryRedBorderColor : Colors.transparent, + width: 1, + ), + ), + child: Column( + children: [ + TextInputWidget( + labelText: LocaleKeys.addIbanDetails.tr(), + hintText: "SA0000000000000000000000", + controller: _bankDetailsController, + focusNode: _ibanFocusNode, + autoFocus: false, + keyboardType: TextInputType.text, + isEnable: true, + isReadOnly: false, + prefix: null, + isBorderAllowed: false, + isAllowLeadingIcon: true, + fontSize: 14.f, + isCountryDropDown: false, + leadingIcon: AppAssets.bankIcon, + fontFamily: "Poppins", + hasError: false, + errorMessage: viewModel.ibanError, + onChange: (value) { + viewModel.clearIbanError(); + }, + onSubmitted: (value) { + _descriptionFocusNode.requestFocus(); + }, + ), + Divider(color: AppColors.bottomNAVBorder, thickness: 1).withVerticalPadding(16.h), + TextInputWidget( + labelText: LocaleKeys.description.tr(), + hintText: LocaleKeys.enterDescription.tr(), + controller: _descriptionController, + focusNode: _descriptionFocusNode, + autoFocus: false, + keyboardType: TextInputType.text, + // leadingIconNeedsContainer: false, + isEnable: true, + isReadOnly: false, + prefix: null, + isBorderAllowed: false, + isAllowLeadingIcon: true, + fontSize: 14.f, + isCountryDropDown: false, + leadingIcon: AppAssets.descriptionIcon, + fontFamily: "Poppins", + hasError: false, + errorMessage: viewModel.withdrawDescriptionError, + onChange: (value) { + viewModel.clearWithdrawDescriptionError(); + }, + ), + ], + ), + ); + }, + ); + } + + Widget _buildImagePickerDropDown() { + return Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.selectImageIcon, + width: 40.h, + height: 40.h, + applyThemeColor: false, + ), + SizedBox(width: 8.h), + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + LocaleKeys.uploadImage.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), + LocaleKeys.selectImage.tr(context: context).toText14(color: AppColors.textColor, isBold: true), + ]), + ], + ), + Utils.buildSvgWithAssets( + icon: AppAssets.arrow_down, + width: 25.h, + height: 25.h, + ), + ], + ).onPress(() { + ImageOptions.showImageOptionsNew( + context, + false, + title: LocaleKeys.selectOptions.tr(context: context), + (String image, File file) {}, + ); + }), + ); + } + +// ── Submit ──────────────────────────────────────────────────────────────── + + void _handleWalletWithdrawalSubmit(HabibWalletViewModel vm) { + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.notice.tr(context: context), + child: Utils.getWarningWidget( + loadingText: LocaleKeys.confirmSubmitRequest.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.pop(context); + }, + onConfirmTap: () async { + Navigator.pop(context); + LoaderBottomSheet.showLoader(); + final user = _appState.getAuthenticatedUser(); + final hospital = vm.selectedHospital; + await vm.submitWalletWithdrawalRequest( + iban: _bankDetailsController.text.trim(), + remarks: _descriptionController.text.trim(), + patientName: '${user?.firstName ?? ''} ${user?.lastName ?? ''}'.trim(), + patientId: (user?.patientId ?? 0).toString(), + patientMobileNumber: user?.mobileNumber ?? '', + patientNationalId: user?.patientIdentificationNo?.toString() ?? '', + projectId: hospital?.mainProjectID ?? 0, + projectName: hospital?.name ?? '', + onSuccess: (response) { + LoaderBottomSheet.hideLoader(); + _showWithdrawalSuccessBottomSheet(response.refNo ?? ''); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + ); + }, + ); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + void _showWithdrawalSuccessBottomSheet(String refNo) { + showCommonBottomSheetWithoutHeight( + context, + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + Utils.getSuccessWidget(loadingText: LocaleKeys.requestSubmittedSuccessfully.tr(context: context)), + if (refNo.isNotEmpty) ...[ + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + LocaleKeys.hereIsYourRequestNumber.tr().toText16( + color: AppColors.textColorLight, + ), + SizedBox(width: 6.w), + refNo.toText18(isBold: true, color: AppColors.textColor, isEnglishOnly: true), + ], + ).paddingSymmetrical(24.w, 0), + ], + SizedBox(height: 24.h), + Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: LocaleKeys.ok.tr(), + onPressed: () { + Navigator.pop(context); // pop success sheet + Navigator.pop(context); // pop refund page + Navigator.of(context).push( + CustomPageRoute( + page: RefundHistoryScreen( + refundType: 1, // 2 for wallet, 1 for invoice + ), + ), + ); + }, + textColor: AppColors.whiteColor, + ), + ), + ], + ), + ], + ), + ), + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false, + isAutoDismiss: false, + ); + } + +// ── Build ───────────────────────────────────────────────────────────────── + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, habibWalletViewModel, _) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: LocaleKeys.requestRefund.tr(context: context), + requests: () => Navigator.of(context).push( + CustomPageRoute( + page: RefundHistoryScreen( + refundType: 2, // 2 for wallet, 1 for invoice + ), + ), + ), + child: SingleChildScrollView( + padding: EdgeInsets.only(bottom: 32.h, left: 24.w, right: 24.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildProjectSelectionAndBalance(habibWalletViewModel), + if (habibWalletViewModel.selectedHospital != null) ...[ + SizedBox(height: 16.h), + _buildBankInputDetails(), + // SizedBox(height: 16.h), + // _buildImagePickerDropDown(), + ], + ], + ), + ), + ), + ), + _buildBottomBar(), + ], + ), + ); + }, + ); + } +} + +// I/flutter (13750): uri: https://uat.hmgwebservices.com/Services/COCWS.svc/REST/SubmitRefund +// [log] body: {"ProjectID":15,"Refund_RefundType":"2","Refund_Project":"مستشفى الريان","Refund_ProjectId":15,"Refund_PatientId":"4770714","PatientMobileNumber":"0504278212","Refund_PatientName":"FAIZ UR REHMAN REHMAN","Refund_NationalId":"2530976584","Refund_DoctorId":0,"Refund_DoctorName":"","Refund_RefundDestination":"2","Refund_IBAN":"SA0380000000608010167519","Refund_Remarks":"teskchjvs","Refund_TotalRefundAmount":1714.75,"Refund_AdvancesList":[{"AdvanceNo":"2026000444","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1775682000000+0300)/"},{"AdvanceNo":"2026000392","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1774645200000+0300)/"},{"AdvanceNo":"2026000066","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1770152400000+0300)/"},{"AdvanceNo":"2026000047","PaidAmount":30.0,"BalanceAmount":30.0,"AdvanceDate":"/Date(1769547600000+0300)/"},{"AdvanceNo":"2025000751","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1765746000000+0300)/"},{"AdvanceNo":"2025000414","PaidAmount":115.0,"BalanceAmount":115.0,"AdvanceDate":"/Date(1747083600000+0300)/"},{"AdvanceNo":"2025000210","PaidAmount":120.75,"BalanceAmount":120.75,"AdvanceDate":"/Date(1741726800000+0300)/"}],"PatientType":1,"PatientTypeID":1,"TokenID":"@dm!n","PatientID":4770714,"PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNi0wNS0xOFQwNzo1MzoyMS4wMjQ5MDM2WiIsIlBhdGllbnRJRCI6IjQ3NzIxNjMiLCJQYXRpZW50TWVyZ2VkSURzIjoiNDc3MjE2MyIsIlBhdGllbnRPdXRTQSI6ZmFsc2UsIlNoYXJlZElkcyI6IiJ9LCJleHAiOjE3NzkxMzgwMDB9.VHd_kCya33GfK-DywTHwSU1PMewbQB7UhQD1zXS7X5w","VersionID":"21.2","Channel":"3","IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","LanguageID":"1","Latitude":"0.0","Longitude":"0.0","DeviceTypeID":"2","IsNewFlutterApp":true} +// [log] response: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":"Internal server error occurred.","ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":true,"MealOrderStatus":0,"MealType":0,"MessageStatus":2,"NumberOfResultRecords":0,"PatientBlodType":null,"RequestId":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"ENSAT_GetPatientRequestListResponse":null,"ENSAT_Patient_External_Response":null,"ETQAN_CreateIncident_Response":null,"ETQAN_GetEmployeeOVRRequests_Response":null,"ETQAN_GetIncidentRequest_Response":null,"ETQAN_GetProjects_Response":null,"ITGRequest":null,"Itg_FormAttachmentsList":null,"ListCOCItems":null,"Message":"Refunds API returned status 500. Body: {\"message\":\"Internal server error occurred.\"}","Mohemm_ENSAT_GetItgToken_Response":null,"Mohemm_ITG_DepartmentSectionsList":null,"Mohemm_ITG_Pending_Task_ResponseItem":null,"Mohemm_ITG_ProjectDepartmentsList":null,"Mohemm_ITG_ResponseItem":null,"Mohemm_ITG_SectionTopicsList":null,"Mohemm_ITG_TicketDetailsList":null,"Mohemm_ITG_TicketTransactionsList":null,"Mohemm_ITG_TicketsByEmployeeList":null,"Mohemm_Itg_ProjectsList":null,"Mohemm_Itg_TicketTypesList":null,"ReferenceNumber":null,"Refund_InvoicesList":null,"Refund_PatientIdDisplay":null,"Refund_RefNo":null,"Refund_RefundTypeDisplay":null,"Refund_StatusItem":null,"Refund_TotalInvoices":0,"Refund_VerifyResult":null,"RequestType":null,"TotalCount":0,"statuseCode":500} diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index b2028dd8..0dadf584 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -20,6 +20,7 @@ import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/blood_donation/blood_donation_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; +import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/comprehensive_checkup_page.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/contact_us.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/emergency_services_page.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/habib_wallet_page.dart'; @@ -30,7 +31,7 @@ import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dar import 'package:hmg_patient_app_new/presentation/home/service_info_page.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; import 'package:hmg_patient_app_new/presentation/my_family/my_family.dart'; -import 'package:hmg_patient_app_new/presentation/servicesPriceList/services_price_list_page.dart'; +import 'package:hmg_patient_app_new/presentation/service_price/services_price_list_page.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -42,12 +43,12 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../core/dependencies.dart' show getIt; +import '../../core/dependencies.dart'; class ServicesPage extends StatefulWidget { - bool showBackIcon; + final bool showBackIcon; - ServicesPage({super.key, this.showBackIcon = true}); + const ServicesPage({super.key, this.showBackIcon = true}); @override State createState() => _ServicesPageState(); @@ -101,7 +102,7 @@ class _ServicesPageState extends State { HmgServicesComponentModel( 5, LocaleKeys.completeCheckup.tr(), "", AppAssets.comprehensiveCheckup, bgColor: AppColors.bgGreenColor, true, route: null, onTap: () async { if (getIt.get().isAuthenticated) { - getIt.get().pushPageRoute(AppRoutes.comprehensiveCheckupPage); + Navigator.of(getIt().navigatorKey.currentContext!).push(CustomPageRoute(page: ComprehensiveCheckupPage())); } else { Navigator.of(getIt().navigatorKey.currentContext!).push( CustomPageRoute( @@ -560,55 +561,55 @@ class _ServicesPageState extends State { // isExpanded: false, letterSpacing: -1) // .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.r, width: 80.w, height: 24.h); }) - : LocaleKeys.loginToViewWalletBalance.tr().toText12(isBold: true, maxLine: 2), - Spacer(), - getIt.get().isAuthenticated - ? CustomButton( - height: 40.h, - icon: AppAssets.recharge_icon, - iconSize: 24.w, - iconColor: AppColors.infoColor, - textColor: AppColors.infoColor, - text: LocaleKeys.recharge.tr(), - borderWidth: 0.w, - isBold: true, - borderColor: Colors.transparent, - backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08), - padding: EdgeInsets.all(8.w), - fontSize: 14.f, - onPressed: () { - Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage())); - }, - ) - : SizedBox.shrink(), - ], - ).onPress(() async { - if (getIt.get().isAuthenticated) { - Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage())); - } else { - await getIt.get().onLoginPressed(); - } - }), - ), - ), - SizedBox(width: 16.w), - Expanded( - child: Container( - height: 183.h, - width: 183.h, - padding: EdgeInsets.all(16.w), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.r, - hasShadow: false, + : LocaleKeys.loginToViewWalletBalance.tr().toText12(isBold: true, maxLine: 2), + Spacer(), + getIt.get().isAuthenticated + ? CustomButton( + height: 40.h, + icon: AppAssets.recharge_icon, + iconSize: 24.w, + iconColor: AppColors.infoColor, + textColor: AppColors.infoColor, + text: LocaleKeys.recharge.tr(), + borderWidth: 0.w, + isBold: true, + borderColor: Colors.transparent, + backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08), + padding: EdgeInsets.all(8.w), + fontSize: 14.f, + onPressed: () { + Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage())); + }, + ) + : SizedBox.shrink(), + ], + ).onPress(() async { + if (getIt.get().isAuthenticated) { + Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage())); + } else { + await getIt.get().onLoginPressed(); + } + }), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - spacing: 8.w, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ + ), + SizedBox(width: 16.w), + Expanded( + child: Container( + height: 183.h, + width: 183.h, + padding: EdgeInsets.all(16.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 8.w, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ Utils.buildSvgWithAssets( icon: AppAssets.services_medical_file_icon, width: 40.w, height: 40.h, applyThemeColor: false), LocaleKeys.familyTitle.tr().toText16(isBold: true, maxlines: 2).expanded, diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 15bce8b0..89f738b1 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -494,7 +494,9 @@ class _LandingPageState extends State { height: isTablet ? 290.h : 255.h, child: ListView.separated( scrollDirection: Axis.horizontal, - itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length < 3 ? myAppointmentsVM.patientAppointmentsHistoryList.length : 3, + itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length < 3 + ? myAppointmentsVM.patientAppointmentsHistoryList.length + : 3, shrinkWrap: true, padding: EdgeInsets.only(left: 16.h, right: 16.h), itemBuilder: (context, index) { @@ -868,8 +870,7 @@ class _LandingPageState extends State { children: [ LocaleKeys.myBalanceSubtitle.tr(context: context).toText16(isBold: true).paddingSymmetrical(24.h, 0.h), SizedBox( - // height: 200.h, - height: 170.h, + height: 175.h, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: 2, diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index b1495fed..29aa9dc7 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -1 +1 @@ -import 'dart:async'; import 'dart:convert'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/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/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.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/appbar/collapsing_toolbar.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import 'alphabeticScroll.dart'; import 'dart:ui' as ui; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return CollapsingListView( title: LocaleKeys.labResults.tr(context: context), search: () async { if (labProvider.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: labProvider.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; labProvider.filterLabReports(value); } } }, child: Consumer( builder: (context, labViewModel, child) { return SingleChildScrollView( physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.all(24.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.byVisit.tr()), CustomTabBarModel(null, LocaleKeys.byTest.tr()), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), ), ], ), if (activeIndex == 0) Padding( padding: EdgeInsets.symmetric(vertical: 10.h), child: Row( children: [ // CustomButton( // text: LocaleKeys.byClinic.tr(context: context), // onPressed: () { // labViewModel.setIsSortByClinic(true); // }, // backgroundColor: labViewModel.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, // borderColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), // textColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, // fontSize: 12.f, // isBold: true, // borderRadius: 10, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), // SizedBox(width: 8.h), // CustomButton( // text: LocaleKeys.byHospital.tr(context: context), // onPressed: () { // labViewModel.setIsSortByClinic(false); // }, // backgroundColor: labViewModel.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, // borderColor: labViewModel.isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, // textColor: labViewModel.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, // fontSize: 12, // isBold: true, // borderRadius: 10, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), ], ), ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? Column( children: [ AppCustomChipWidget( labelText: selectedFilterText!, backgroundColor: AppColors.alertColor, textColor: AppColors.whiteColor, deleteIcon: AppAssets.close_bottom_sheet_icon, deleteIconColor: AppColors.whiteColor, deleteIconHasColor: true, isEnglishOnly: true, onDeleteTap: () { selectedFilterText = ""; labProvider.filterLabReports(""); }, ), SizedBox(height: 8.h), ], ) : SizedBox(), activeIndex == 0 ? // By Visit - show grouped view when available labViewModel.isLabOrdersLoading ? ListView.builder( shrinkWrap: true, physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: 5, itemBuilder: (context, index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ), ) : (labViewModel.patientLabOrders.isNotEmpty ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: labViewModel.filteredLabOrders.length, itemBuilder: (context, index) { final group = labViewModel.filteredLabOrders[index]; final isExpanded = expandedIndex == index; return AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, margin: EdgeInsets.symmetric(vertical: 8.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), child: InkWell( onTap: () { setState(() { expandedIndex = isExpanded ? null : index; }); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.all(16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ AppCustomChipWidget( richText: Row( children: [ "${group.testDetails!.length} ".toText10(color: AppColors.successColor, isEnglishOnly: true, weight: FontWeight.w600), LocaleKeys.tests.tr(context: context).toText10(color: AppColors.successColor, weight: FontWeight.w600), ], ), labelText: "${group.testDetails!.length} ${LocaleKeys.tests.tr(context: context)}", backgroundColor: AppColors.successColor.withOpacity(0.1), textColor: AppColors.successColor, ), AppCustomChipWidget( labelText: "${_appState.isArabic() ? group.isInOutPatientDescriptionN : group.isInOutPatientDescription}", backgroundColor: AppColors.warningColorYellow.withOpacity(0.1), textColor: AppColors.warningColorYellow, ) ], ), Icon(isExpanded ? Icons.expand_less : Icons.expand_more), ], ), SizedBox(height: 8.h), Row( mainAxisSize: MainAxisSize.min, children: [ Image.network( group.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", width: 24.w, height: 24.h, fit: BoxFit.cover, ).circle(100), SizedBox(width: 8.h), Expanded(child: (group.doctorName ?? group.doctorNameEnglish ?? "").toString().toText14(isBold: true)), ], ), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ Directionality( textDirection: ui.TextDirection.ltr, child: AppCustomChipWidget( labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(group.orderDate ?? ""), false), isEnglishOnly: true, )), AppCustomChipWidget( labelText: (group.projectName ?? ""), ), AppCustomChipWidget( labelText: (group.clinicDescription ?? ""), ), ], ), ], ), ), AnimatedSwitcher( duration: Duration(milliseconds: 500), switchInCurve: Curves.easeIn, switchOutCurve: Curves.easeOut, transitionBuilder: (Widget child, Animation animation) { return FadeTransition( opacity: animation, child: SizeTransition( sizeFactor: animation, axisAlignment: 0.0, child: child, ), ); }, child: isExpanded ? Container( key: ValueKey(index), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 0.h), child: Column( children: [ ListView.separated( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemBuilder: (cxt, index) { PatientLabOrdersResponseModel order = group; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ "• ${order.testDetails![index].description!}".toText14(isBold: true, isEnglishOnly: true), SizedBox(height: 4.h), order.testDetails![index].testDescriptionEn!.toText12(isBold: true, color: AppColors.textColorLight, isEnglishOnly: true), // Row( // mainAxisSize: MainAxisSize.min, // children: [ // Image.network( // order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", // width: 24.w, // height: 24.h, // fit: BoxFit.cover, // ).circle(100), // SizedBox(width: 8.h), // Expanded(child: (order.doctorName ?? order.doctorNameEnglish ?? "").toString().toText14(isBold: true)), // ], // ), // SizedBox(height: 8.h), // Wrap( // direction: Axis.horizontal, // spacing: 4.h, // runSpacing: 4.h, // children: [ // AppCustomChipWidget( // labelText: ("${LocaleKeys.orderNo.tr(context: context)}: ${order.orderNo!}"), isEnglishOnly: true, // ), // Directionality( // textDirection: ui.TextDirection.ltr, // child: AppCustomChipWidget( // labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(order.orderDate ?? ""), false), // isEnglishOnly: true, // )), // AppCustomChipWidget( // labelText: labViewModel.isSortByClinic ? (order.projectName ?? "") : (order.clinicDescription ?? ""), // ), // ], // ), // // Row( // // children: [ // // CustomButton( // // text: ("Order No: ".needTranslation + order.orderNo!), // // onPressed: () {}, // // backgroundColor: AppColors.greyColor, // // borderColor: AppColors.greyColor, // // textColor: AppColors.blackColor, // // fontSize: 10, // // isBold: true, // // borderRadius: 8, // // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // // height: 24.h, // // ), // // SizedBox(width: 8.h), // // CustomButton( // // text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(order.orderDate ?? ""), false), // // onPressed: () {}, // // backgroundColor: AppColors.greyColor, // // borderColor: AppColors.greyColor, // // textColor: AppColors.blackColor, // // fontSize: 10, // // isBold: true, // // borderRadius: 8, // // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // // height: 24.h, // // ), // // ], // // ), // // SizedBox(height: 8.h), // // Row( // // children: [ // // CustomButton( // // text: model.isSortByClinic ? (order.clinicDescription ?? "") : (order.projectName ?? ""), // // onPressed: () {}, // // backgroundColor: AppColors.greyColor, // // borderColor: AppColors.greyColor, // // textColor: AppColors.blackColor, // // fontSize: 10, // // isBold: true, // // borderRadius: 8, // // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // // height: 24.h, // // ), // // ], // // ), // SizedBox(height: 12.h), // Row( // children: [ // Expanded(flex: 2, child: SizedBox()), // // Expanded( // // flex: 1, // // child: Container( // // height: 40.h, // // width: 40.w, // // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( // // color: AppColors.textColor, // // borderRadius: 12, // // ), // // child: Padding( // // padding: EdgeInsets.all(12.h), // // child: Transform.flip( // // flipX: _appState.isArabic(), // // child: Utils.buildSvgWithAssets( // // icon: AppAssets.forward_arrow_icon_small, // // iconColor: AppColors.whiteColor, // // fit: BoxFit.contain, // // ), // // ), // // ), // // ).onPress(() { // // model.currentlySelectedPatientOrder = order; // // labProvider.getPatientLabResultByHospital(order); // // labProvider.getPatientSpecialResult(order); // // Navigator.of(context).push( // // CustomPageRoute(page: LabResultByClinic(labOrder: order)), // // ); // // }), // // ) // // Expanded( // flex: 2, // child: CustomButton( // icon: AppAssets.view_report_icon, // iconColor: AppColors.primaryRedColor, // iconSize: 16.h, // text: LocaleKeys.viewResults.tr(context: context), // onPressed: () { // labViewModel.currentlySelectedPatientOrder = order; // labProvider.getPatientLabResultByHospital(order); // labProvider.getPatientSpecialResult(order); // Navigator.of(context).push( // CustomPageRoute(page: LabResultByClinic(labOrder: order)), // ); // }, // backgroundColor: AppColors.secondaryLightRedColor, // borderColor: AppColors.secondaryLightRedColor, // textColor: AppColors.primaryRedColor, // fontSize: 14, // isBold: true, // borderRadius: 12, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), // ) // ], // ), // SizedBox(height: 12.h), // Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), // SizedBox(height: 12.h), ], ).paddingOnly(top: 16, bottom: 16); }, separatorBuilder: (cxt, index) => Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), itemCount: group.testDetails!.length > 3 ? 3 : group.testDetails!.length), SizedBox(height: 16.h), CustomButton( text: "${LocaleKeys.viewResults.tr()} (${group.testDetails!.length})", onPressed: () { labProvider.currentlySelectedPatientOrder = group; labProvider.getPatientLabResultByHospital(group); labProvider.getPatientSpecialResult(group); Navigator.of(context).push( CustomPageRoute( page: LabResultByClinic(labOrder: group), ), ); }, backgroundColor: AppColors.infoColor.withAlpha(20), borderColor: AppColors.infoColor.withAlpha(0), textColor: AppColors.infoColor, fontSize: (isFoldable || isTablet) ? 12.f : 14.f, isBold: true, borderRadius: 12.r, padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0), height: 40.h, iconSize: 14.h, icon: AppAssets.view_report_icon, iconColor: AppColors.infoColor, ), SizedBox(height: 16.h), ], ), ) : SizedBox.shrink(), ), ], ), ), ), ), )); }, ) : Utils.getNoDataWidget(context, noDataText: LocaleKeys.noLabResults.tr(context: context))) : // By Test or other tabs keep existing behavior (labViewModel.isLabOrdersLoading) ? Column( children: List.generate( 5, (index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, )), ) : AlphabeticScroll( alpahbetsAvailable: labViewModel.filteredIndexedCharacterForUniqueTest, details: labViewModel.filteredUniqueTestsList, labViewModel: labViewModel, rangeViewModel: rangeViewModel, appState: _appState, ) ], )); }, ), ); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file +import 'dart:async'; import 'dart:ui' as ui; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/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/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/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/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import 'alphabeticScroll.dart'; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return CollapsingListView( title: LocaleKeys.labResults.tr(context: context), search: () async { if (labProvider.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: labProvider.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; labProvider.filterLabReports(value); } } }, child: Consumer( builder: (context, labViewModel, child) { return SingleChildScrollView( physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.all(24.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.byVisit.tr()), CustomTabBarModel(null, LocaleKeys.byTest.tr()), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), ), ], ), if (activeIndex == 0) Padding( padding: EdgeInsets.symmetric(vertical: 10.h), child: Row( children: [ // CustomButton( // text: LocaleKeys.byClinic.tr(context: context), // onPressed: () { // labViewModel.setIsSortByClinic(true); // }, // backgroundColor: labViewModel.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, // borderColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), // textColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, // fontSize: 12.f, // isBold: true, // borderRadius: 10, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), // SizedBox(width: 8.h), // CustomButton( // text: LocaleKeys.byHospital.tr(context: context), // onPressed: () { // labViewModel.setIsSortByClinic(false); // }, // backgroundColor: labViewModel.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, // borderColor: labViewModel.isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, // textColor: labViewModel.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, // fontSize: 12, // isBold: true, // borderRadius: 10, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), ], ), ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? Column( children: [ AppCustomChipWidget( labelText: selectedFilterText!, backgroundColor: AppColors.alertColor, textColor: AppColors.whiteColor, deleteIcon: AppAssets.close_bottom_sheet_icon, deleteIconColor: AppColors.whiteColor, deleteIconHasColor: true, isEnglishOnly: true, onDeleteTap: () { selectedFilterText = ""; labProvider.filterLabReports(""); }, ), SizedBox(height: 8.h), ], ) : SizedBox(), activeIndex == 0 ? // By Visit - show grouped view when available labViewModel.isLabOrdersLoading ? ListView.builder( shrinkWrap: true, physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: 5, itemBuilder: (context, index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ), ) : (labViewModel.patientLabOrders.isNotEmpty ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: labViewModel.filteredLabOrders.length, itemBuilder: (context, index) { final group = labViewModel.filteredLabOrders[index]; final isExpanded = expandedIndex == index; return AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, margin: EdgeInsets.symmetric(vertical: 8.h), decoration: RoundedRectangleBorder() .toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), child: InkWell( onTap: () { setState(() { expandedIndex = isExpanded ? null : index; }); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.all(16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ AppCustomChipWidget( richText: Row( children: [ "${group.testDetails!.length} ".toText10( color: AppColors.successColor, isEnglishOnly: true, weight: FontWeight.w600), LocaleKeys.tests .tr(context: context) .toText10(color: AppColors.successColor, weight: FontWeight.w600), ], ), labelText: "${group.testDetails!.length} ${LocaleKeys.tests.tr(context: context)}", backgroundColor: AppColors.successColor.withOpacity(0.1), textColor: AppColors.successColor, ), AppCustomChipWidget( labelText: "${_appState.isArabic() ? group.isInOutPatientDescriptionN : group.isInOutPatientDescription}", backgroundColor: AppColors.warningColorYellow.withOpacity(0.1), textColor: AppColors.warningColorYellow, ) ], ), Icon(isExpanded ? Icons.expand_less : Icons.expand_more), ], ), SizedBox(height: 8.h), Row( mainAxisSize: MainAxisSize.min, children: [ Image.network( group.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", width: 24.w, height: 24.h, fit: BoxFit.cover, ).circle(100), SizedBox(width: 8.h), Expanded( child: (group.doctorName ?? group.doctorNameEnglish ?? "") .toString() .toText14(isBold: true)), ], ), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ Directionality( textDirection: ui.TextDirection.ltr, child: AppCustomChipWidget( labelText: DateUtil.formatDateToDate( DateUtil.convertStringToDate(group.orderDate ?? ""), false), isEnglishOnly: true, )), AppCustomChipWidget( labelText: (group.projectName ?? ""), ), AppCustomChipWidget( labelText: (group.clinicDescription ?? ""), ), ], ), ], ), ), AnimatedSwitcher( duration: Duration(milliseconds: 500), switchInCurve: Curves.easeIn, switchOutCurve: Curves.easeOut, transitionBuilder: (Widget child, Animation animation) { return FadeTransition( opacity: animation, child: SizeTransition( sizeFactor: animation, axisAlignment: 0.0, child: child, ), ); }, child: isExpanded ? Container( key: ValueKey(index), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 0.h), child: Column( children: [ ListView.separated( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemBuilder: (cxt, index) { PatientLabOrdersResponseModel order = group; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ "• ${order.testDetails![index].description!}" .toText14(isBold: true, isEnglishOnly: true), SizedBox(height: 4.h), order.testDetails![index].testDescriptionEn!.toText12( isBold: true, color: AppColors.textColorLight, isEnglishOnly: true), ], ).paddingOnly(top: 16, bottom: 16); }, separatorBuilder: (cxt, index) => Divider( color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), itemCount: group.testDetails!.length > 3 ? 3 : group.testDetails!.length), SizedBox(height: 16.h), CustomButton( text: "${LocaleKeys.viewResults.tr()} (${group.testDetails!.length})", onPressed: () { labProvider.currentlySelectedPatientOrder = group; labProvider.getPatientLabResultByHospital(group); labProvider.getPatientSpecialResult(group); Navigator.of(context).push( CustomPageRoute( page: LabResultByClinic(labOrder: group), ), ); }, backgroundColor: AppColors.infoColor.withAlpha(20), borderColor: AppColors.infoColor.withAlpha(0), textColor: AppColors.infoColor, fontSize: (isFoldable || isTablet) ? 12.f : 14.f, isBold: true, borderRadius: 12.r, padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0), height: 40.h, iconSize: 14.h, icon: AppAssets.view_report_icon, iconColor: AppColors.infoColor, ), SizedBox(height: 16.h), ], ), ) : SizedBox.shrink(), ), ], ), ), ), ), )); }, ) : Utils.getNoDataWidget(context, noDataText: LocaleKeys.noLabResults.tr(context: context))) : // By Test or other tabs keep existing behavior (labViewModel.isLabOrdersLoading) ? Column( children: List.generate( 5, (index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, )), ) : AlphabeticScroll( alpahbetsAvailable: labViewModel.filteredIndexedCharacterForUniqueTest, details: labViewModel.filteredUniqueTestsList, labViewModel: labViewModel, rangeViewModel: rangeViewModel, appState: _appState, ) ], )); }, ), ); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index e281743a..6bbd5074 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -41,9 +41,6 @@ import 'package:hmg_patient_app_new/presentation/appointments/widgets/ask_doctor import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_profile_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; -import 'package:hmg_patient_app_new/presentation/habib_wallet/habib_wallet_page.dart'; -import 'package:hmg_patient_app_new/presentation/habib_wallet/recharge_wallet_page.dart'; -import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_approvals_page.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart'; @@ -647,11 +644,7 @@ class _MedicalFilePageState extends State { isLargeText: true, iconSize: 36.w, ).onPress(() { - Navigator.of(context).push( - CustomPageRoute( - page: InsuranceApprovalsPage(), - ), - ); + Navigator.of(context).push(CustomPageRoute(page: InsuranceApprovalsPage())); }), MedicalFileCard( label: LocaleKeys.myInvoicesList.tr(context: context), @@ -661,12 +654,22 @@ class _MedicalFilePageState extends State { isLargeText: true, iconSize: 36.w, ).onPress(() { - Navigator.of(context).push( - CustomPageRoute( - page: MyInvoicesList(), - ), - ); + Navigator.of(context).push(CustomPageRoute(page: MyInvoicesList())); }), + // MedicalFileCard( + // label: LocaleKeys.requestRefund.tr(context: context), + // textColor: AppColors.blackColor, + // backgroundColor: AppColors.whiteColor, + // svgIcon: AppAssets.ancillary_orders_list_icon, + // isLargeText: true, + // iconSize: 36.w, + // ).onPress(() { + // Navigator.of(context).push( + // CustomPageRoute( + // page: const RefundRequestCreatePage(initialTabIndex: 0), + // ), + // ); + // }), MedicalFileCard( label: LocaleKeys.ancillaryOrdersListNew.tr(context: context), textColor: AppColors.blackColor, diff --git a/lib/presentation/my_invoices/my_invoices_list.dart b/lib/presentation/my_invoices/my_invoices_list.dart index 26f0d1c9..67ee05a6 100644 --- a/lib/presentation/my_invoices/my_invoices_list.dart +++ b/lib/presentation/my_invoices/my_invoices_list.dart @@ -137,215 +137,101 @@ class _MyInvoicesListState extends State { } }, ).paddingSymmetrical(24.h, 0.h), - myInvoicesVM.selectedTabIndex == 0 ? Column( - children: [ - SizedBox(height: 16.h), - Row( - children: [ - CustomButton( - text: LocaleKeys.allInvoices.tr(context: context), - onPressed: () { - myInvoicesViewModel.filterInvoices(InvoiceFilterType.all); - }, - backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), - textColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.primaryRedColor : AppColors.blackColor, - fontSize: 12, - fontWeight: FontWeight.w600, - borderRadius: 10, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40.h, - ), - SizedBox(width: 8.h), - CustomButton( - text: LocaleKeys.hospitals.tr(context: context), - onPressed: () { - if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return; - _showHospitalFilterBottomSheet(context); - }, - backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), - textColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.primaryRedColor : AppColors.blackColor, - fontSize: 12, - fontWeight: FontWeight.w600, - borderRadius: 10, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40.h, - ), - SizedBox(width: 8.h), - CustomButton( - text: LocaleKeys.clinics.tr(context: context), - onPressed: () { - if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return; - _showClinicFilterBottomSheet(context); - }, - backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), - textColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.primaryRedColor : AppColors.blackColor, - fontSize: 12, - fontWeight: FontWeight.w600, - borderRadius: 10, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40.h, - ), - SizedBox(width: 8.h), - CustomButton( - text: LocaleKeys.doctors.tr(context: context), - onPressed: () { - if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return; - _showDoctorFilterBottomSheet(context); - }, - backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), - textColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.primaryRedColor : AppColors.blackColor, - fontSize: 12, - fontWeight: FontWeight.w600, - borderRadius: 10, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40.h, - ), - ], - ).paddingSymmetrical(24.h, 0.h), - SizedBox(height: 16.h), - ListView.builder( - itemCount: myInvoicesVM.isInvoicesListLoading - ? 4 - : myInvoicesVM.allInvoicesList.isEmpty - ? 1 - : myInvoicesVM.allInvoicesList.length, - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - padding: EdgeInsetsGeometry.zero, - itemBuilder: (context, index) { - return myInvoicesVM.isInvoicesListLoading - ? LabResultItemView(onTap: () {}, labOrder: null, index: index, isLoading: true) - : myInvoicesVM.allInvoicesList.isNotEmpty - ? AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - child: InvoiceListCard( - getInvoicesListResponseModel: myInvoicesVM.allInvoicesList[index], - onTap: () async { - if (Utils.isVidaPlusProject(myInvoicesVM.allInvoicesList[index].projectId ?? 0)) { - LoaderBottomSheet.showLoader(loadingText: LocaleKeys.sendingEmailPleaseWait.tr(context: context)); - await myInvoicesViewModel.sendInvoiceEmail( - appointmentNo: myInvoicesVM.allInvoicesList[index].appointmentNo!, - projectID: myInvoicesVM.allInvoicesList[index].projectId!, - onSuccess: (val) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight(context, - child: Utils.getSuccessWidget(loadingText: LocaleKeys.emailSentSuccessfullyMessage.tr(context: context)), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - isAutoDismiss: true); - }, - onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }); - } else { - LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context)); - myInvoicesViewModel.downloadInvoicePDF( - setupId: myInvoicesVM.allInvoicesList[index].setupId!, - invoiceNo: myInvoicesVM.allInvoicesList[index].invoiceNo!, - projectID: myInvoicesVM.allInvoicesList[index].projectId!, - onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }, - onSuccess: (value) async { - LoaderBottomSheet.hideLoader(); - if (myInvoicesViewModel.downloadInvoicePDFBase64!.isNotEmpty) { - String path = await Utils.createFileFromString(myInvoicesViewModel.downloadInvoicePDFBase64!, "pdf"); - try { - OpenFilex.open(path); - } catch (ex) { - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: "Cannot open file"), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - } - } - }, - ); - } - }, - // onTap: () async { - // myInvoicesVM.setInvoiceDetailLoading(); - // LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingInvoiceDetails.tr(context: context)); - // await myInvoicesVM.getInvoiceDetails( - // appointmentNo: myInvoicesVM.allInvoicesList[index].appointmentNo!, - // invoiceNo: myInvoicesVM.allInvoicesList[index].invoiceNo!, - // projectID: myInvoicesVM.allInvoicesList[index].projectId!, - // onSuccess: (val) { - // LoaderBottomSheet.hideLoader(); - // Navigator.of(context).push( - // CustomPageRoute( - // page: MyInvoicesDetailsPage( - // getInvoiceDetailsResponseModel: myInvoicesVM.invoiceDetailsResponseModel, - // getInvoicesListResponseModel: myInvoicesVM.allInvoicesList[index], - // ), - // ), - // ); - // }, - // onError: (err) { - // LoaderBottomSheet.hideLoader(); - // showCommonBottomSheetWithoutHeight( - // context, - // child: Utils.getErrorWidget(loadingText: err), - // callBackFunc: () {}, - // isFullScreen: false, - // isCloseButtonVisible: true, - // ); - // }); - // }, - ), - ), - ), - ), - ) - : Utils.getNoDataWidget(context); - }).paddingSymmetrical(24.w, 0.h), - ], - ) - : Column( + myInvoicesVM.selectedTabIndex == 0 + ? Column( children: [ + SizedBox(height: 16.h), + Row( + children: [ + CustomButton( + text: LocaleKeys.allInvoices.tr(context: context), + onPressed: () { + myInvoicesViewModel.filterInvoices(InvoiceFilterType.all); + }, + backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.all + ? AppColors.primaryRedColor + : AppColors.textColor.withValues(alpha: 0.2), + textColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w600, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: LocaleKeys.hospitals.tr(context: context), + onPressed: () { + if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return; + _showHospitalFilterBottomSheet(context); + }, + backgroundColor: + myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital + ? AppColors.primaryRedColor + : AppColors.textColor.withValues(alpha: 0.2), + textColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w600, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: LocaleKeys.clinics.tr(context: context), + onPressed: () { + if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return; + _showClinicFilterBottomSheet(context); + }, + backgroundColor: + myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic + ? AppColors.primaryRedColor + : AppColors.textColor.withValues(alpha: 0.2), + textColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w600, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + SizedBox(width: 8.h), + CustomButton( + text: LocaleKeys.doctors.tr(context: context), + onPressed: () { + if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return; + _showDoctorFilterBottomSheet(context); + }, + backgroundColor: + myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor + ? AppColors.primaryRedColor + : AppColors.textColor.withValues(alpha: 0.2), + textColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.primaryRedColor : AppColors.blackColor, + fontSize: 12, + fontWeight: FontWeight.w600, + borderRadius: 10, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, + ), + ], + ).paddingSymmetrical(24.h, 0.h), SizedBox(height: 16.h), ListView.builder( - itemCount: myInvoicesVM.isPharmacyInvoicesLoading + itemCount: myInvoicesVM.isInvoicesListLoading ? 4 - : myInvoicesVM.allPharmacyInvoicesList.isEmpty + : myInvoicesVM.allInvoicesList.isEmpty ? 1 - : myInvoicesVM.allPharmacyInvoicesList.length, + : myInvoicesVM.allInvoicesList.length, physics: NeverScrollableScrollPhysics(), shrinkWrap: true, padding: EdgeInsetsGeometry.zero, itemBuilder: (context, index) { - return myInvoicesVM.isPharmacyInvoicesLoading + return myInvoicesVM.isInvoicesListLoading ? LabResultItemView(onTap: () {}, labOrder: null, index: index, isLoading: true) - : myInvoicesVM.allPharmacyInvoicesList.isNotEmpty + : myInvoicesVM.allInvoicesList.isNotEmpty ? AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), @@ -355,29 +241,41 @@ class _MyInvoicesListState extends State { child: AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, - child: PharmacyInvoiceCard( - pharmacyInvoice: myInvoicesVM.allPharmacyInvoicesList[index], + child: InvoiceListCard( + getInvoicesListResponseModel: myInvoicesVM.allInvoicesList[index], onTap: () async { - // if (Utils.isVidaPlusProject(myInvoicesVM.allPharmacyInvoicesList[index].projectID ?? 0)) { - // // For Vida+ projects, send email (if API is available) - // showCommonBottomSheetWithoutHeight( - // context, - // child: Utils.getSuccessWidget( - // loadingText: LocaleKeys.comingSoon.tr(context: context), - // ), - // callBackFunc: () {}, - // isFullScreen: false, - // isCloseButtonVisible: true, - // isAutoDismiss: true, - // ); - // } else { - // Download pharmacy invoice PDF + if (Utils.isVidaPlusProject(myInvoicesVM.allInvoicesList[index].projectId ?? 0)) { + LoaderBottomSheet.showLoader( + loadingText: LocaleKeys.sendingEmailPleaseWait.tr(context: context)); + await myInvoicesViewModel.sendInvoiceEmail( + appointmentNo: myInvoicesVM.allInvoicesList[index].appointmentNo!, + projectID: myInvoicesVM.allInvoicesList[index].projectId!, + onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight(context, + child: Utils.getSuccessWidget( + loadingText: LocaleKeys.emailSentSuccessfullyMessage.tr(context: context)), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + isAutoDismiss: true); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } else { LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context)); - myInvoicesViewModel.downloadPharmacyInvoicePDF( - setupId: myInvoicesVM.allPharmacyInvoicesList[index].setupId ?? "", - invoiceNo: myInvoicesVM.allPharmacyInvoicesList[index].invoiceNo ?? 0, - projectID: myInvoicesVM.allPharmacyInvoicesList[index].pharmacyProjectID ?? 0, - locationID: myInvoicesVM.allPharmacyInvoicesList[index].locationID ?? 0, + myInvoicesViewModel.downloadInvoicePDF( + setupId: myInvoicesVM.allInvoicesList[index].setupId!, + invoiceNo: myInvoicesVM.allInvoicesList[index].invoiceNo!, + projectID: myInvoicesVM.allInvoicesList[index].projectId!, onError: (err) { LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight( @@ -390,14 +288,15 @@ class _MyInvoicesListState extends State { }, onSuccess: (value) async { LoaderBottomSheet.hideLoader(); - if (myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!.isNotEmpty) { - String path = await Utils.createFileFromString(myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!, "pdf"); + if (myInvoicesViewModel.downloadInvoicePDFBase64!.isNotEmpty) { + String path = await Utils.createFileFromString( + myInvoicesViewModel.downloadInvoicePDFBase64!, "pdf"); try { OpenFilex.open(path); } catch (ex) { showCommonBottomSheetWithoutHeight( context, - child: Utils.getErrorWidget(loadingText: "Cannot open file"), + child: Utils.getErrorWidget(loadingText: LocaleKeys.cannotOpenThisFile.tr()), callBackFunc: () {}, isFullScreen: false, isCloseButtonVisible: true, @@ -406,6 +305,94 @@ class _MyInvoicesListState extends State { } }, ); + } + }, + ), + ), + ), + ), + ) + : Utils.getNoDataWidget(context); + }).paddingSymmetrical(24.w, 0.h), + ], + ) + : Column( + children: [ + SizedBox(height: 16.h), + ListView.builder( + itemCount: myInvoicesVM.isPharmacyInvoicesLoading + ? 4 + : myInvoicesVM.allPharmacyInvoicesList.isEmpty + ? 1 + : myInvoicesVM.allPharmacyInvoicesList.length, + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: EdgeInsetsGeometry.zero, + itemBuilder: (context, index) { + return myInvoicesVM.isPharmacyInvoicesLoading + ? LabResultItemView(onTap: () {}, labOrder: null, index: index, isLoading: true) + : myInvoicesVM.allPharmacyInvoicesList.isNotEmpty + ? AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: PharmacyInvoiceCard( + pharmacyInvoice: myInvoicesVM.allPharmacyInvoicesList[index], + onTap: () async { + // if (Utils.isVidaPlusProject(myInvoicesVM.allPharmacyInvoicesList[index].projectID ?? 0)) { + // // For Vida+ projects, send email (if API is available) + // showCommonBottomSheetWithoutHeight( + // context, + // child: Utils.getSuccessWidget( + // loadingText: LocaleKeys.comingSoon.tr(context: context), + // ), + // callBackFunc: () {}, + // isFullScreen: false, + // isCloseButtonVisible: true, + // isAutoDismiss: true, + // ); + // } else { + // Download pharmacy invoice PDF + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context)); + myInvoicesViewModel.downloadPharmacyInvoicePDF( + setupId: myInvoicesVM.allPharmacyInvoicesList[index].setupId ?? "", + invoiceNo: myInvoicesVM.allPharmacyInvoicesList[index].invoiceNo ?? 0, + projectID: myInvoicesVM.allPharmacyInvoicesList[index].pharmacyProjectID ?? 0, + locationID: myInvoicesVM.allPharmacyInvoicesList[index].locationID ?? 0, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }, + onSuccess: (value) async { + LoaderBottomSheet.hideLoader(); + if (myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!.isNotEmpty) { + String path = await Utils.createFileFromString( + myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!, "pdf"); + try { + OpenFilex.open(path); + } catch (ex) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Cannot open file"), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + } + }, + ); // } }, ), diff --git a/lib/presentation/my_invoices/widgets/invoice_list_card.dart b/lib/presentation/my_invoices/widgets/invoice_list_card.dart index 465c1c0b..dc5414cd 100644 --- a/lib/presentation/my_invoices/widgets/invoice_list_card.dart +++ b/lib/presentation/my_invoices/widgets/invoice_list_card.dart @@ -1,3 +1,5 @@ +import 'dart:ui' as ui; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -9,12 +11,16 @@ 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/my_invoices/models/get_invoices_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_create_page.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; - -import 'dart:ui' as ui; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; class InvoiceListCard extends StatelessWidget { final GetInvoicesListResponseModel getInvoicesListResponseModel; @@ -22,15 +28,51 @@ class InvoiceListCard extends StatelessWidget { InvoiceListCard({super.key, required this.getInvoicesListResponseModel, required this.onTap}); + void _handleRefundInvoiceTap(BuildContext context) { + final invoiceNo = getInvoicesListResponseModel.invoiceNo; + final projectId = getInvoicesListResponseModel.projectId; + + if (invoiceNo == null || projectId == null) { + Navigator.of(context).push( + CustomPageRoute( + page: RefundRequestCreatePage(initialTabIndex: 0, invoice: getInvoicesListResponseModel), + ), + ); + return; + } + + final viewModel = context.read(); + + LoaderBottomSheet.showLoader(); + + viewModel.checkIsRefundableInvoice( + invoiceNo: invoiceNo, + projectId: projectId, + onSuccess: (result) { + LoaderBottomSheet.hideLoader(); + if (result.isRefundable == true) { + Navigator.of(context).push( + CustomPageRoute( + page: RefundRequestCreatePage(initialTabIndex: 0, invoice: getInvoicesListResponseModel), + ), + ); + } else { + final message = result.message?.isNotEmpty == true ? result.message! : LocaleKeys.thisInvoiceIsNotEligibleForRefund.tr(context: context); + getIt().showErrorBottomSheet(message: message); + } + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + getIt().showErrorBottomSheet(message: error); + }, + ); + } + @override Widget build(BuildContext context) { AppState appState = getIt(); return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: true, - ), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), child: Padding( padding: EdgeInsets.all(14.h), child: Column( @@ -49,7 +91,7 @@ class InvoiceListCard extends StatelessWidget { textColor: AppColors.textColor, ), AppCustomChipWidget( - labelText: appState.isArabic() ? "العيادات الخارجيه" :"Out Patient", + labelText: appState.isArabic() ? "العيادات الخارجيه" : "Out Patient", backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.1), textColor: AppColors.warningColorYellow, ), @@ -98,9 +140,13 @@ class InvoiceListCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ (getIt().isArabic() - ? (getInvoicesListResponseModel.doctorNameN ?? getInvoicesListResponseModel.doctorName ?? LocaleKeys.doctor.tr(context: context)) - : (getInvoicesListResponseModel.doctorName ?? getInvoicesListResponseModel.doctorNameN ?? LocaleKeys.doctor.tr(context: context)) - ).toText16(isBold: true), + ? (getInvoicesListResponseModel.doctorNameN ?? + getInvoicesListResponseModel.doctorName ?? + LocaleKeys.doctor.tr(context: context)) + : (getInvoicesListResponseModel.doctorName ?? + getInvoicesListResponseModel.doctorNameN ?? + LocaleKeys.doctor.tr(context: context))) + .toText16(isBold: true), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, @@ -128,7 +174,8 @@ class InvoiceListCard extends StatelessWidget { child: AppCustomChipWidget( labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), icon: AppAssets.doctor_calendar_icon, - labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(getInvoicesListResponseModel.appointmentDate), false), + labelText: + DateUtil.formatDateToDate(DateUtil.convertStringToDate(getInvoicesListResponseModel.appointmentDate), false), isEnglishOnly: true, ), ), @@ -139,25 +186,57 @@ class InvoiceListCard extends StatelessWidget { ), ], ), - SizedBox(height: 16.h), - CustomButton( - // text: LocaleKeys.viewInvoiceDetails.tr(context: context), - text: Utils.isVidaPlusProject(getInvoicesListResponseModel.projectId ?? 0) ? LocaleKeys.sendEmail.tr(context: context) : LocaleKeys.downloadInvoice.tr(context: context), - onPressed: () { - if (onTap != null) { - onTap!(); - } - }, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), - borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), - textColor: AppColors.primaryRedColor, - fontSize: 14.f, - fontWeight: FontWeight.w600, - borderRadius: 12.r, - padding: EdgeInsets.symmetric(horizontal: 10.w), - height: 40.h, - iconSize: 14.h, - ), + if (onTap != null) ...[ + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: CustomButton( + // text: LocaleKeys.viewInvoiceDetails.tr(context: context), + text: Utils.isVidaPlusProject(getInvoicesListResponseModel.projectId ?? 0) + ? LocaleKeys.sendEmail.tr(context: context) + : LocaleKeys.downloadInvoice.tr(context: context), + onPressed: () { + if (onTap != null) { + onTap!(); + } + }, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), + borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), + textColor: AppColors.primaryRedColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + iconSize: 14.h, + ), + ), + if (getInvoicesListResponseModel.isAllowRefund == true) ...[ + SizedBox(width: 8.w), + Expanded( + child: CustomButton( + text: LocaleKeys.requestRefund.tr(context: context), + onPressed: () { + _handleRefundInvoiceTap(context); + }, + icon: AppAssets.refundIcon, + iconColor: AppColors.caloriesCalculatorColor, + backgroundColor: AppColors.blueColor.withValues(alpha: 0.14), + borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), + textColor: AppColors.caloriesCalculatorColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + iconSize: 14.h, + ), + ), + ] + ], + ), + ], ], ), ), diff --git a/lib/presentation/refund_request/refund_history_screen.dart b/lib/presentation/refund_request/refund_history_screen.dart new file mode 100644 index 00000000..630c5dc0 --- /dev/null +++ b/lib/presentation/refund_request/refund_history_screen.dart @@ -0,0 +1,264 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_detail_screen.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +class RefundHistoryScreen extends StatefulWidget { + final int refundType; + + const RefundHistoryScreen({super.key, required this.refundType}); + + @override + State createState() => _RefundHistoryScreenState(); +} + +class _RefundHistoryScreenState extends State { + @override + void initState() { + super.initState(); + final vm = context.read(); + scheduleMicrotask(() async { + await vm.getRefundInvoices(refundType: widget.refundType); + }); + } + + // ── Helpers ────────────────────────────────────────────────────────────── + + String _formatDate(String? rawDate) { + if (rawDate == null || rawDate.isEmpty) return ''; + try { + final date = DateUtil.convertStringToDate(rawDate); + return DateFormat('MMM dd, yyyy').format(date); + } catch (_) { + return rawDate; + } + } + + // ── Shimmer ─────────────────────────────────────────────────────────────── + + Widget _buildLoadingShimmer() { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.only(top: 24.h), + itemCount: 4, + separatorBuilder: (_, __) => SizedBox(height: 16.h), + itemBuilder: (_, __) => _buildInvoiceCard(null, isLoading: true), + ); + } + + // ── Card ────────────────────────────────────────────────────────────────── + + Color getStatusColor(int status) { + Color statusColor = AppColors.textColor; + if (status == 3) { + statusColor = AppColors.statusRejectedColor; + } else if (status == 2) { + statusColor = AppColors.statusCompletedColor; + } else if (status == 1) { + statusColor = AppColors.statusPendingColor; + } + return statusColor; + } + + String getRefundDestinationIcon(String refundDestination) { + String icon = ""; + + if (refundDestination.toLowerCase().contains("wallet")) { + icon = AppAssets.habibLogoOnly; + } else if (refundDestination.toLowerCase().contains("bank")) { + icon = AppAssets.bankIconOnly; + } + + return icon; + } + + Widget _buildInvoiceCard(RefundInvoiceModel? invoice, {bool isLoading = false}) { + final referenceNo = isLoading ? 'INV-000000' : (invoice?.referenceNumber ?? '-'); + final String requestStatusName = isLoading ? 'Processed' : (invoice?.requestStatusName ?? ''); + final int requestStatusId = isLoading ? 1 : (invoice?.requestStatusId ?? 1); + final refundDestinationId = (invoice?.refundDestination ?? ''); + final refundDestination = isLoading ? 'Wallet' : (invoice?.refundDestination ?? ''); + final isInvoiceRefund = invoice?.refundTypeId == 1; + + // Resolve total refund amount from whichever payload is present + final totalAmount = isLoading + ? 0.0 + : isInvoiceRefund + ? (invoice?.invoiceRequestPayload?.totalRefundAmount) + : (invoice?.walletRequestPayload?.totalRefundAmount); + final totalAmountStr = totalAmount?.toStringAsFixed(2); + + // Resolve total refund amount from whichever payload is present + final projectName = isInvoiceRefund ? (invoice?.invoiceRequestPayload?.project ?? "") : ""; + final String totalProcedures = (invoice?.invoiceRequestPayload?.proceduresList?.length.toString() ?? ''); + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Invoice No + Last Updated row + + AppCustomChipWidget( + icon: "", + labelText: requestStatusName, + textColor: getStatusColor(requestStatusId), + backgroundColor: getStatusColor(requestStatusId).withValues(alpha: 0.2), + ).toShimmer2(isShow: isLoading), + SizedBox(height: 8.h), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "${LocaleKeys.referenceNo.tr(context: context)} $referenceNo" + .toText16( + color: AppColors.textColor, + isBold: true, + ) + .toShimmer2(isShow: isLoading), + ], + ), + + SizedBox(height: 8.h), + Wrap( + spacing: 6.w, + runSpacing: 6.h, + children: [ + if (!isLoading && refundDestination.isNotEmpty) + AppCustomChipWidget(icon: getRefundDestinationIcon(refundDestination), iconHasColor: false, labelText: refundDestination), + if (totalProcedures.isNotEmpty) AppCustomChipWidget(icon: "", labelText: "$totalProcedures ${LocaleKeys.procedures.tr()}"), + if (!isLoading && projectName.isNotEmpty) AppCustomChipWidget(icon: "", labelText: projectName), + ], + ), + + SizedBox(height: 16.h), + + // Total refund amount + if (totalAmountStr != null) + Utils.getPaymentAmountWithSymbol( + isExpanded: false, + (isLoading ? '0.00' : totalAmountStr).toText20(isBold: true, color: AppColors.textColor), + AppColors.textColor, + 18, + isSaudiCurrency: true, + ).toShimmer2(isShow: isLoading), + + SizedBox(height: 16.h), + CustomButton( + text: LocaleKeys.viewDetails.tr(context: context), + onPressed: () => Navigator.of(context).push(CustomPageRoute(page: RefundRequestDetailScreen(invoice: invoice!))), + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), + borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), + textColor: AppColors.primaryRedColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + iconColor: AppColors.whiteColor, + iconSize: 18.h, + ).toShimmer2(isShow: isLoading), + ], + ), + ), + ); + } + + // ── Empty ───────────────────────────────────────────────────────────────── + + Widget _buildEmptyState() { + return Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 40.h), + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + hasShadow: false, + ), + child: Utils.getNoDataWidget( + context, + noDataText: LocaleKeys.noRefundHistoryFound.tr(context: context), + isSmallWidget: true, + width: 62.w, + height: 62.h, + ), + ), + ), + ); + } + + // ── Build ───────────────────────────────────────────────────────────────── + + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: LocaleKeys.refundHistory.tr(context: context), + isLeading: true, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Consumer( + builder: (context, viewModel, _) { + if (viewModel.isRefundInvoicesLoading) { + return _buildLoadingShimmer(); + } + + if (viewModel.refundInvoicesList.isEmpty) { + return _buildEmptyState(); + } + return ListView.separated( + padding: EdgeInsets.only(top: 24.h), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: viewModel.refundInvoicesList.length, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + final invoice = viewModel.refundInvoicesList[index]; + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: _buildInvoiceCard(invoice), + ), + ), + ); + }, + ); + }, + ), + ], + ).paddingSymmetrical(24.w, 0), + ), + ); + } +} diff --git a/lib/presentation/refund_request/refund_request_create_page.dart b/lib/presentation/refund_request/refund_request_create_page.dart new file mode 100644 index 00000000..fd64bb67 --- /dev/null +++ b/lib/presentation/refund_request/refund_request_create_page.dart @@ -0,0 +1,720 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoices_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/req_models/submit_refund_request_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/my_invoices/widgets/invoice_list_card.dart'; +import 'package:hmg_patient_app_new/presentation/refund_request/refund_history_screen.dart'; +import 'package:hmg_patient_app_new/presentation/refund_request/widgets/refund_reason_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/radio_list_tile_widget.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:hmg_patient_app_new/widgets/stepper/stepper_widget.dart'; +import 'package:provider/provider.dart'; + +class RefundRequestCreatePage extends StatefulWidget { + final GetInvoicesListResponseModel invoice; + + /// 0 = Invoices (default), 1 = HMG Wallet + final int initialTabIndex; + + const RefundRequestCreatePage({ + super.key, + required this.invoice, + this.initialTabIndex = 0, + }); + + @override + State createState() => _RefundRequestCreatePageState(); +} + +class _RefundRequestCreatePageState extends State { + late RefundRequestViewModel _refundRequestViewModel; + late HabibWalletViewModel _habibWalletViewModel; + late AppState _appState; + + final TextEditingController _ibanController = TextEditingController(); + final TextEditingController _descriptionController = TextEditingController(); + final FocusNode _ibanFocusNode = FocusNode(); + final FocusNode _descriptionFocusNode = FocusNode(); + + @override + void initState() { + super.initState(); + _appState = getIt.get(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _refundRequestViewModel = context.read(); + _ibanController.addListener(() => _refundRequestViewModel.triggerRebuild()); + _descriptionController.addListener(() => _refundRequestViewModel.triggerRebuild()); + _loadInvoiceDetails(); + }); + } + + @override + void dispose() { + _ibanController.dispose(); + _descriptionController.dispose(); + _ibanFocusNode.dispose(); + _descriptionFocusNode.dispose(); + _refundRequestViewModel.resetState(); + super.dispose(); + } + + void _loadInvoiceDetails() { + final invoiceNo = widget.invoice.invoiceNo; + final projectId = widget.invoice.projectId; + if (invoiceNo == null || projectId == null) return; + + _refundRequestViewModel.getRefundableInvoicesDetails( + invoiceNo: invoiceNo, + projectId: projectId, + onSuccess: (details) { + final procedures = details.refundableInvoiceDetailsItems + .map((item) => RefundableProcedure( + procedureId: item.lineItemNo ?? 0, + procedureName: item.procedureName ?? '', + patientPaidAmount: item.patientPaidAmount ?? 0.0, + apiLineItemNo: item.lineItemNo, + apiProcedureId: item.procedureId, + )) + .toList(); + _refundRequestViewModel.setProcedures(procedures); + }, + ); + } + + // ── Stepper ─────────────────────────────────────────────────────────────── + + Widget _buildStepper(RefundRequestViewModel vm) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: LayoutBuilder( + builder: (context, constraints) { + // StepperWidget appends SizedBox(width: 8.h) after itself + final itemWidth = (constraints.maxWidth - 2 * 8.h) / 2; + return Row( + children: List.generate(2, (index) { + final isActive = index <= vm.currentStep; + final isThumb = index == vm.currentStep; + return StepperWidget( + itemWidth, + isActive ? AppColors.primaryRedColor : AppColors.greyColor, + isThumb, + 6.h, + ); + }), + ); + }, + ), + ); + } + + // ── Step 1 — exactly mirrors RechargeWalletPage hospital row ───────────── + + Widget _buildStep1Content(RefundRequestViewModel vm) { + return Consumer( + builder: (context, refundVM, _) { + final isArabic = _appState.isArabic(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.selectProcedures.tr(context: context).toText20(isBold: true), + SizedBox(height: 24.h), + if (vm.isProceduresLoading) + ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => SizedBox(height: 16.h), + itemCount: 4, + itemBuilder: (_, index) => Container( + height: 40.h, + margin: EdgeInsets.only(bottom: 24.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.h, + ), + ).toShimmer2(isShow: true, radius: 16.r), + ) + else if (vm.procedures.isEmpty) + Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 24.h), + child: LocaleKeys.noProceduresAvailableForRefund.tr().toText14(color: AppColors.greyTextColor), + ), + ) + else + AnimationLimiter( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: false), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Select All row + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _refundRequestViewModel.toggleSelectAll(), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + SizedBox( + width: 24.w, + height: 24.w, + child: Transform.scale( + scale: 24.w / 22, + child: Checkbox( + value: vm.isAllSelected, + onChanged: (_) => _refundRequestViewModel.toggleSelectAll(), + activeColor: AppColors.primaryRedColor, + side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.r)), + ), + ), + ), + SizedBox(width: 8.w), + LocaleKeys.selectAll.tr(context: context).toText16(isBold: true), + ], + ), + ), + ), + ), + Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w), + ...List.generate( + vm.procedures.length, + (index) => _buildProcedureCard(vm.procedures[index], index, vm.procedures.length), + ), + ], + ), + ), + ), + ], + ); + }, + ); + } + + // ── Step 2 — payment method selection ──────────────────────────────────── + + Widget _buildStep2Content(RefundRequestViewModel vm) { + final paymentOptions = [ + RefundTabType.hmgWallet, + RefundTabType.invoices, + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.selectRefundMethod.tr(context: context).toText20(isBold: true), + SizedBox(height: 24.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: paymentOptions.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final type = paymentOptions[index]; + return Column( + children: [ + RadioListTileWidget( + value: type, + groupValue: vm.selectedPaymentMethod, + title: type.toDisplayString(), + onChanged: (value) { + if (value != null) vm.setPaymentMethod(value); + }, + ), + if (index != paymentOptions.length - 1) + Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w), + ], + ); + }, + ), + ), + if (vm.selectedPaymentMethod == RefundTabType.invoices) ...[ + SizedBox(height: 16.h), + _buildBankInputDetails(vm), + ], + ], + ); + } + + // ── Bank Transfer Input ─────────────────────────────────────────────────── + + Widget _buildBankInputDetails(RefundRequestViewModel vm) { + final hasError = vm.ibanError != null || vm.descriptionError != null; + return Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + border: Border.all( + color: hasError ? AppColors.primaryRedBorderColor : Colors.transparent, + width: 1, + ), + ), + child: Column( + children: [ + TextInputWidget( + labelText: LocaleKeys.addIbanDetails.tr(), + hintText: "SA0000000000000000000000", + controller: _ibanController, + focusNode: _ibanFocusNode, + autoFocus: false, + keyboardType: TextInputType.text, + isEnable: true, + isReadOnly: false, + prefix: null, + isBorderAllowed: false, + isAllowLeadingIcon: true, + fontSize: 14.f, + isCountryDropDown: false, + leadingIcon: AppAssets.bankIcon, + fontFamily: "Poppins", + hasError: false, + errorMessage: vm.ibanError, + onChange: (value) { + vm.clearIbanError(); + }, + onSubmitted: (value) { + _descriptionFocusNode.requestFocus(); + }, + ), + Divider(color: AppColors.bottomNAVBorder, thickness: 1).withVerticalPadding(16.h), + TextInputWidget( + labelText: LocaleKeys.description.tr(), + hintText: LocaleKeys.enterDescription.tr(), + controller: _descriptionController, + focusNode: _descriptionFocusNode, + autoFocus: false, + keyboardType: TextInputType.text, + isEnable: true, + isReadOnly: false, + prefix: null, + isBorderAllowed: false, + isAllowLeadingIcon: true, + fontSize: 14.f, + isCountryDropDown: false, + leadingIcon: AppAssets.descriptionIcon, + fontFamily: "Poppins", + hasError: false, + errorMessage: vm.descriptionError, + onChange: (value) { + vm.clearDescriptionError(); + }, + ), + ], + ), + ); + } + + // ── Step 2 — procedure checkboxes ───────────────────────────────────────── + + Widget _buildProcedureCard(RefundableProcedure procedure, int index, int length) { + final isArabic = _appState.isArabic(); + final name = isArabic && procedure.procedureNameN != null ? procedure.procedureNameN! : procedure.procedureName; + + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 400), + child: SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _refundRequestViewModel.toggleProcedureSelection(procedure), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 24.w, + height: 24.w, + child: Transform.scale( + scale: 24.w / 22, + child: Checkbox( + value: procedure.isSelected, + onChanged: (_) => _refundRequestViewModel.toggleProcedureSelection(procedure), + activeColor: AppColors.primaryRedColor, + side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4.r), + ), + ), + ), + ), + SizedBox(width: 8.w), + Expanded(child: name.toText16(isBold: true)), + SizedBox(width: 8.w), + Utils.getPaymentAmountWithSymbol( + procedure.patientPaidAmount.toStringAsFixed(2).toText20( + isBold: true, + isEnglishOnly: true, + color: AppColors.textColor, + ), + AppColors.textColor, + 10.f, + isSaudiCurrency: true, + ), + ], + ), + if (procedure.isSelected) ...[ + InkWell( + onTap: () => showRefundReasonBottomSheet( + context, + selectedReason: procedure.refundReason, + onDone: (reason) => _refundRequestViewModel.setRefundReasonForProcedure(procedure, reason), + ), + child: Padding( + padding: EdgeInsets.only(top: 16.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.refundReason.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), + (procedure.refundReason ?? LocaleKeys.selectRefundReason.tr(context: context)) + .toText14(color: AppColors.textColor, isBold: true), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), + ], + ), + ), + ), + ], + if (index != length - 1) ...[ + Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingOnly(top: 16.h), + ] + ], + ), + ), + ), + ), + ), + ), + ), + ); + } + + // ── Bottom bar ──────────────────────────────────────────────────────────── + + Widget _buildBottomBar(RefundRequestViewModel refundRequestViewModel) { + final isStep2 = refundRequestViewModel.currentStep == 1; + final isWallet = refundRequestViewModel.selectedTab == RefundTabType.hmgWallet; + + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!isWallet && refundRequestViewModel.selectedProcedures.isNotEmpty) ...[ + Padding( + padding: EdgeInsets.fromLTRB(24.w, 16.h, 24.w, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.totalAmount.tr().toText18(color: AppColors.textColor, isBold: true), + Utils.getPaymentAmountWithSymbol( + refundRequestViewModel.totalRefundableAmount.toStringAsFixed(2).toText24( + isBold: true, + isEnglishOnly: true, + color: AppColors.textColor, + ), + AppColors.textColorLight, + 14, + isSaudiCurrency: true, + ), + ], + ), + ), + ], + CustomButton( + icon: isStep2 ? AppAssets.refundIcon : null, + text: isStep2 ? LocaleKeys.submit.tr(context: context) : LocaleKeys.next.tr(context: context), + onPressed: () { + if (!isStep2) { + if (refundRequestViewModel.selectedProcedures.isNotEmpty) refundRequestViewModel.nextStep(); + } else { + final isBankTransfer = refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices; + if (isBankTransfer) { + final isValid = refundRequestViewModel.validateBankTransferForm( + iban: _ibanController.text, + description: _descriptionController.text, + ); + if (!isValid) return; + } + _handleInvoiceRefundRequestSubmit(refundRequestViewModel); + } + }, + isDisabled: isStep2 + ? (refundRequestViewModel.selectedPaymentMethod == null || + (refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices && + (_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty))) + : (refundRequestViewModel.selectedProcedures.isEmpty || !refundRequestViewModel.allSelectedProceduresHaveReasons), + ).paddingSymmetrical(24.h, 24.h), + ], + ), + ); + } + + // ── HMG Wallet placeholder ───────────────────────────────────────────────── + + Widget buildInvoiceCard() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: InvoiceListCard(onTap: null, getInvoicesListResponseModel: widget.invoice), + ); + } + + // ── Submit ──────────────────────────────────────────────────────────────── + + void _handleInvoiceRefundRequestSubmit(RefundRequestViewModel vm) { + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.notice.tr(context: context), + child: Utils.getWarningWidget( + loadingText: LocaleKeys.confirmSubmitRequest.tr(context: context), + isShowActionButtons: true, + onCancelTap: () => Navigator.pop(context), + onConfirmTap: () async { + Navigator.pop(context); + LoaderBottomSheet.showLoader(); + + final isBankTransfer = vm.selectedPaymentMethod == RefundTabType.invoices; + final invoice = widget.invoice; + final userPayload = _appState.getAuthenticatedUser(); + final patientId = userPayload!.patientId; + final patientName = "${userPayload.firstName} ${userPayload.lastName}"; + final patientMobileNumber = "${userPayload.mobileNumber}"; + final patientNationalId = userPayload.patientIdentificationNo.toString(); + final request = SubmitRefundRequestModel( + projectId: invoice.projectId ?? 0, + refundType: '1', + // 1 = invoice refund + refundProject: invoice.projectName ?? '', + refundProjectId: invoice.projectId ?? 0, + refundPatientId: patientId.toString(), + refundPatientName: patientName, + refundNationalId: patientNationalId, + patientMobileNumber: patientMobileNumber, + refundInvoiceNo: (invoice.invoiceNo ?? 0).toString(), + refundAppointmentNo: (invoice.appointmentNo ?? 0).toString(), + refundDoctorId: invoice.doctorId ?? 0, + refundDoctorName: invoice.doctorName ?? '', + refundClinicId: invoice.clinicId ?? 0, + refundClinicName: invoice.clinicName ?? '', + refundDestination: isBankTransfer ? '2' : '1', + refundIban: isBankTransfer ? _ibanController.text.trim() : '', + refundRemarks: isBankTransfer ? _descriptionController.text.trim() : '', + refundTotalAmount: vm.totalRefundableAmount, + refundProceduresList: vm.selectedProcedures + .map((p) => SubmitRefundProcedureItem( + lineItemNo: p.apiLineItemNo ?? 0, + procedureId: p.apiProcedureId ?? '', + procedureName: p.procedureName, + patientPaidAmount: p.patientPaidAmount, + refundReason: p.refundReason ?? '', + )) + .toList(), + ); + + await vm.submitInvoiceRefundRequest( + request: request, + onSuccess: (response) { + LoaderBottomSheet.hideLoader(); + _showSuccessBottomSheet(response.refNo ?? ''); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + ); + }, + ); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + void _showSuccessBottomSheet(String refNo) { + showCommonBottomSheetWithoutHeight( + context, + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + Utils.getSuccessWidget(loadingText: LocaleKeys.requestSubmittedSuccessfully.tr(context: context)), + if (refNo.isNotEmpty) ...[ + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + LocaleKeys.hereIsYourRequestNumber.tr().toText16( + color: AppColors.textColorLight, + ), + SizedBox(width: 6.w), + refNo.toText18(isBold: true, color: AppColors.textColor, isEnglishOnly: true), + ], + ).paddingSymmetrical(24.w, 0), + ], + SizedBox(height: 24.h), + Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: LocaleKeys.ok.tr(), + onPressed: () { + Navigator.pop(context); // pop success sheet + Navigator.pop(context); // pop refund page + Navigator.of(context).push( + CustomPageRoute( + page: RefundHistoryScreen( + refundType: 1, // 2 for wallet, 1 for invoice + ), + ), + ); + }, + textColor: AppColors.whiteColor, + ), + ), + ], + ), + ], + ), + ), + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false, + isAutoDismiss: false, + ); + } + +// ── Build ───────────────────────────────────────────────────────────────── + + @override + Widget build(BuildContext context) { + _habibWalletViewModel = context.read(); + return Consumer( + builder: (context, vm, _) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: LocaleKeys.requestRefund.tr(context: context), + leadingCallback: () { + final refundRequestViewModel = context.read(); + if (refundRequestViewModel.currentStep > 0) { + refundRequestViewModel.previousStep(); + } else { + context.pop(); + } + }, + requests: () => Navigator.of(context).push(CustomPageRoute( + page: RefundHistoryScreen( + refundType: 1, // 1 for invoice, 2 for wallet + ))), + child: SingleChildScrollView( + padding: EdgeInsets.only(bottom: 32.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Stepper (invoices tab only) + if (vm.selectedTab == RefundTabType.invoices) ...[ + buildInvoiceCard(), + _buildStepper(vm), + SizedBox(height: 16.h), + ], + // Content + AnimatedSwitcher( + duration: const Duration(milliseconds: 350), + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: const Offset(0.05, 0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + ), + child: vm.currentStep == 0 + ? KeyedSubtree( + key: const ValueKey('step1'), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: _buildStep1Content(vm), + ), + ) + : KeyedSubtree( + key: const ValueKey('step2'), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: _buildStep2Content(vm), + ), + ), + ), + ], + ), + ), + ), + ), + _buildBottomBar(vm), + ], + ), + ); + }, + ); + } +} diff --git a/lib/presentation/refund_request/refund_request_detail_screen.dart b/lib/presentation/refund_request/refund_request_detail_screen.dart new file mode 100644 index 00000000..a28aa4dd --- /dev/null +++ b/lib/presentation/refund_request/refund_request_detail_screen.dart @@ -0,0 +1,283 @@ +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/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_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'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; + +class RefundRequestDetailScreen extends StatelessWidget { + final RefundInvoiceModel invoice; + + const RefundRequestDetailScreen({super.key, required this.invoice}); + + String _formatDate(String? rawDate) { + if (rawDate == null || rawDate.isEmpty) return ''; + try { + final date = DateUtil.convertStringToDate(rawDate); + return DateFormat('MMM dd, yyyy').format(date); + } catch (_) { + return rawDate; + } + } + + String rawDateForAdvance(String? rawDate) { + if (rawDate == null || rawDate.isEmpty) return ''; + try { + final date = DateTime.parse(rawDate); + return DateFormat('MMM dd, yyyy').format(date); + } catch (_) { + return rawDate; + } + } + + Color getStatusColor(int status) { + Color statusColor = AppColors.textColor; + if (status == 3) { + statusColor = AppColors.statusRejectedColor; + } else if (status == 2) { + statusColor = AppColors.statusCompletedColor; + } else if (status == 1) { + statusColor = AppColors.statusPendingColor; + } + return statusColor; + } + + String getRefundDestinationIcon(String refundDestination) { + String icon = ""; + + if (refundDestination.toLowerCase().contains("wallet")) { + icon = AppAssets.habibLogoOnly; + } else if (refundDestination.toLowerCase().contains("bank")) { + icon = AppAssets.bankIconOnly; + } + + return icon; + } + + Widget _buildInvoiceCard(BuildContext context, RefundInvoiceModel? invoice, {bool isLoading = false}) { + final isInvoiceType = (invoice?.refundTypeId ?? 0) == 1; + final isWalletType = (invoice?.refundTypeId ?? 0) == 2; + final referenceNo = (invoice?.referenceNumber ?? ''); + final lastUpdated = _formatDate(invoice?.lastUpdated); + final invoicePayload = invoice?.invoiceRequestPayload; + final walletPayload = invoice?.walletRequestPayload; + final totalProcedures = (invoicePayload?.proceduresList?.length.toString() ?? ''); + final statusName = (invoice?.requestStatusName ?? ''); + final statusId = (invoice?.requestStatusId ?? 1); + final refundDestination = (invoice?.refundDestination ?? ''); + final refundType = invoice?.refundType ?? ''; + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: false), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget( + icon: "", + labelText: statusName, + textColor: getStatusColor(statusId), + backgroundColor: getStatusColor(statusId).withValues(alpha: 0.2), + ), + " $lastUpdated" + .toText12( + color: AppColors.textColorLight, + isBold: false, + ) + .toShimmer2(isShow: isLoading), + ], + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "${LocaleKeys.referenceNo.tr(context: context)} $referenceNo" + .toText16( + color: AppColors.textColor, + isBold: true, + ) + .toShimmer2(isShow: isLoading), + // lastUpdated.toText12(color: AppColors.textColorLight).toShimmer2(isShow: isLoading), + ], + ), + SizedBox(height: 8.h), + Wrap( + spacing: 6.w, + runSpacing: 6.h, + children: [ + if (!isLoading && refundDestination.isNotEmpty) + AppCustomChipWidget(icon: getRefundDestinationIcon(refundDestination), iconHasColor: false, labelText: refundDestination), + if (refundType.isNotEmpty) AppCustomChipWidget(icon: "", labelText: "$refundType ${LocaleKeys.refund.tr()}"), + if (isInvoiceType && invoicePayload != null) ...[ + if (totalProcedures.isNotEmpty) AppCustomChipWidget(icon: "", labelText: "$totalProcedures ${LocaleKeys.procedures.tr()}"), + if (invoicePayload.project != null && invoicePayload.project!.isNotEmpty) + AppCustomChipWidget(icon: '', labelText: invoicePayload.project ?? ''), + if (invoicePayload.clinicName != null && invoicePayload.clinicName!.isNotEmpty) + AppCustomChipWidget(icon: '', labelText: invoicePayload.clinicName ?? ''), + if (invoicePayload.doctorName != null && invoicePayload.doctorName!.isNotEmpty) + AppCustomChipWidget(icon: '', labelText: invoicePayload.doctorName ?? ''), + // AppCustomChipWidget(icon: '', labelText: invoicePayload?.refundTypeName ?? ''), + ] else if (isWalletType && walletPayload != null) ...[ + AppCustomChipWidget(icon: '', labelText: walletPayload.project ?? ''), + // AppCustomChipWidget(icon: '', labelText: invoicePayload?.refundTypeName ?? ''), + ], + ], + ), + SizedBox(height: 16.h), + if (isInvoiceType) ...[ + Utils.getPaymentAmountWithSymbol( + isExpanded: false, + ('${invoicePayload?.totalRefundAmount ?? 0}').toText20(isBold: true, color: AppColors.textColor), + AppColors.textColor, + 14, + isSaudiCurrency: true, + ) + ] else ...[ + Utils.getPaymentAmountWithSymbol( + isExpanded: false, + ('${walletPayload?.totalRefundAmount ?? 0}').toText20(isBold: true, color: AppColors.textColor), + AppColors.textColor, + 14, + isSaudiCurrency: true, + ) + ] + ], + ), + ), + ); + } + + Widget buildListDetails(BuildContext context, RefundInvoiceModel? invoice, {bool isLoading = false}) { + final isInvoiceType = (invoice?.refundTypeId ?? 0) == 1; + final isWalletType = (invoice?.refundTypeId ?? 0) == 2; + final invoicePayload = invoice?.invoiceRequestPayload; + final walletPayload = invoice?.walletRequestPayload; + + bool shouldShowPadding = (isWalletType && walletPayload != null) || (isInvoiceType && invoicePayload != null); + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(shouldShowPadding ? 16.w : 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Summary info + if (isInvoiceType && invoicePayload != null) ...[ + // Procedures header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.procedure.tr(context: context).toText12(color: AppColors.textColor, isBold: true), + LocaleKeys.amountOnly.tr(context: context).toText12(color: AppColors.textColor, isBold: true), + ], + ), + SizedBox(height: 16.h), + Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1), + SizedBox(height: 16.h), + if (invoicePayload.proceduresList != null && invoicePayload.proceduresList!.isNotEmpty) + ...invoicePayload.proceduresList!.map((p) => _buildProcedureRow(context, p)), + ], + if (isWalletType && walletPayload != null) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: LocaleKeys.advanceNumber.tr(context: context).toText12(color: AppColors.textColor, isBold: true)), + SizedBox( + width: 80.w, + child: + LocaleKeys.amountOnly.tr(context: context).toText12(color: AppColors.textColor, isBold: true, textAlignment: TextAlign.end), + ), + ], + ), + SizedBox(height: 16.h), + Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1), + SizedBox(height: 16.h), + if (walletPayload.advancesList != null && walletPayload.advancesList!.isNotEmpty) + ...walletPayload.advancesList!.map((a) => _buildAdvanceRow(context, a)), + ], + ], + ), + ), + ); + } + + Widget _buildProcedureRow(BuildContext context, InvoiceRefundProcedure procedure) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 6.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 3, + child: (procedure.procedureName ?? '-').toText12(color: AppColors.textColor), + ), + SizedBox(width: 8.w), + (procedure.patientPaidAmount?.toString() ?? '-').toText12(color: AppColors.textColor, isBold: true), + ], + ), + ); + } + + Widget _buildAdvanceRow(BuildContext context, WalletAdvanceItem advance) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 6.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (advance.advanceNo ?? '').toText12(color: AppColors.textColor), + if (advance.advanceDate != null && advance.advanceDate!.isNotEmpty) + rawDateForAdvance(advance.advanceDate!).toText10(color: AppColors.greyTextColor), + ], + ), + ), + SizedBox( + width: 80.w, + child: (advance.balanceAmount?.toStringAsFixed(2) ?? '').toText12(color: AppColors.textColor, isBold: true, textAlignment: TextAlign.end), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: LocaleKeys.refundDetails.tr(context: context), + isLeading: true, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInvoiceCard(context, invoice), + SizedBox(height: 16.h), + buildListDetails(context, invoice), + ], + ).paddingSymmetrical(24.w, 0), + ), + ); + } +} diff --git a/lib/presentation/refund_request/widgets/refund_reason_bottom_sheet.dart b/lib/presentation/refund_request/widgets/refund_reason_bottom_sheet.dart new file mode 100644 index 00000000..5fa8e5ad --- /dev/null +++ b/lib/presentation/refund_request/widgets/refund_reason_bottom_sheet.dart @@ -0,0 +1,111 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/radio_list_tile_widget.dart'; + +void showRefundReasonBottomSheet( + BuildContext context, { + required String? selectedReason, + required ValueChanged onDone, +}) { + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.selectRefundReason.tr(context: context), + isDismissible: true, + child: _RefundReasonSheetContent( + selectedReason: selectedReason, + onDone: onDone, + ), + callBackFunc: () {}, + ); +} + +class _RefundReasonSheetContent extends StatefulWidget { + final String? selectedReason; + final ValueChanged onDone; + + const _RefundReasonSheetContent({ + required this.selectedReason, + required this.onDone, + }); + + @override + State<_RefundReasonSheetContent> createState() => _RefundReasonSheetContentState(); +} + +class _RefundReasonSheetContentState extends State<_RefundReasonSheetContent> { + String? _selected; + + static List _reasons(BuildContext context) => [ + LocaleKeys.refundReason1.tr(context: context), + LocaleKeys.refundReason2.tr(context: context), + LocaleKeys.refundReason3.tr(context: context), + LocaleKeys.refundReason4.tr(context: context), + LocaleKeys.refundReason5.tr(context: context), + LocaleKeys.refundReason6.tr(context: context), + LocaleKeys.refundReason7.tr(context: context), + ]; + + @override + void initState() { + super.initState(); + _selected = widget.selectedReason; + } + + @override + Widget build(BuildContext context) { + final reasons = _reasons(context); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: reasons.length, + separatorBuilder: (_, __) => Divider( + color: AppColors.bottomNAVBorder, + height: 1, + thickness: 1, + ).paddingSymmetrical(16.w, 0.w), + itemBuilder: (context, index) { + final reason = reasons[index]; + return RadioListTileWidget( + value: reason, + groupValue: _selected, + title: reason, + onChanged: (value) { + if (value != null) setState(() => _selected = value); + }, + ); + }, + ), + ), + SizedBox(height: 16.h), + CustomButton( + text: LocaleKeys.done.tr(context: context), + isDisabled: _selected == null, + onPressed: () { + if (_selected != null) { + widget.onDone(_selected!); + Navigator.of(context).pop(); + } + }, + ).paddingSymmetrical(0, 0), + SizedBox(height: 8.h), + ], + ); + } +} diff --git a/lib/presentation/servicesPriceList/services_price_list_page.dart b/lib/presentation/service_price/services_price_list_page.dart similarity index 100% rename from lib/presentation/servicesPriceList/services_price_list_page.dart rename to lib/presentation/service_price/services_price_list_page.dart diff --git a/lib/presentation/symptoms_checker/triage_screen.dart b/lib/presentation/symptoms_checker/triage_screen.dart index 79c47d09..fc99d4e6 100644 --- a/lib/presentation/symptoms_checker/triage_screen.dart +++ b/lib/presentation/symptoms_checker/triage_screen.dart @@ -77,8 +77,10 @@ class _TriagePageState extends State { highestProbability = (sortedConditions.first.probability ?? 0.0) * 100; } - // Case 2: Should stop flag is true OR Case 3: Probability >= 70% OR Case 4: 7 or more questions answered - if (highestProbability >= 70.0 || viewModel.triageQuestionCount >= 30) { + // Case 2: Should stop flag is true OR + // Case 3: Probability >= 70% OR + // Case 4: 30 or more questions answered + if (viewModel.shouldStopTriage || highestProbability >= 70.0 || viewModel.triageQuestionCount >= 30) { // Navigate to results/possible conditions screen context.navigateWithName(AppRoutes.possibleConditionsPage); return; diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index f4ddff9b..f292755b 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoices_list_response_model.dart'; import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; import 'package:hmg_patient_app_new/presentation/authentication/register.dart'; import 'package:hmg_patient_app_new/presentation/authentication/register_step2.dart'; @@ -10,6 +11,7 @@ import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/comprehen import 'package:hmg_patient_app_new/presentation/covid19test/covid19_landing_page.dart'; import 'package:hmg_patient_app_new/presentation/e_referral/new_e_referral.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/emergency_services_page.dart'; +import 'package:hmg_patient_app_new/presentation/habib_wallet/withdraw_request_create_page.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculators_page.dart'; import 'package:hmg_patient_app_new/presentation/health_trackers/add_health_tracker_entry_page.dart'; import 'package:hmg_patient_app_new/presentation/health_trackers/health_tracker_detail_page.dart'; @@ -19,7 +21,11 @@ import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_procedures_page.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; import 'package:hmg_patient_app_new/presentation/my_family/my_family.dart'; -import 'package:hmg_patient_app_new/presentation/servicesPriceList/services_price_list_page.dart'; +import 'package:hmg_patient_app_new/presentation/parking/paking_page.dart'; +import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_create_page.dart'; +import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_detail_screen.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart'; +import 'package:hmg_patient_app_new/presentation/service_price/services_price_list_page.dart'; import 'package:hmg_patient_app_new/presentation/smartwatches/smartwatch_home_page.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/organ_selector_screen.dart'; import 'package:hmg_patient_app_new/presentation/symptoms_checker/possible_conditions_screen.dart'; @@ -41,7 +47,6 @@ import '../core/dependencies.dart'; import '../features/monthly_reports/monthly_reports_repo.dart'; import '../features/monthly_reports/monthly_reports_view_model.dart'; import '../features/qr_parking/qr_parking_view_model.dart'; -import '../presentation/parking/paking_page.dart'; import '../services/error_handler_service.dart'; class AppRoutes { @@ -101,6 +106,12 @@ class AppRoutes { // Weather static const String familyFilesPage = '/familyFilesPage'; + // Refund Request + static const String refundRequestCreatePage = '/refundRequestCreatePage'; + static const String createWithdrawRequest = '/createWithdrawRequest'; + static const String refundHistoryScreen = '/refundHistoryScreen'; + static const String refundRequestDetailScreen = '/refundRequestDetailScreen'; + static Map get routes => { initialRoute: (context) => SplashPage(), loginScreen: (context) => LoginScreen(), @@ -160,6 +171,17 @@ class AppRoutes { qrParking: (context) => ChangeNotifierProvider( create: (_) => getIt(), child: const ParkingPage(), - ) + ), + refundRequestCreatePage: (context) { + final args = ModalRoute.of(context)?.settings.arguments as GetInvoicesListResponseModel; + return RefundRequestCreatePage(invoice: args); + }, + createWithdrawRequest: (context) { + return WithdrawRequestCreatePage(); + }, + refundRequestDetailScreen: (context) { + final args = ModalRoute.of(context)?.settings.arguments as RefundInvoiceModel; + return RefundRequestDetailScreen(invoice: args); + }, }; } diff --git a/lib/splashPage.dart b/lib/splashPage.dart index cf1ca7f4..22b9f979 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_callkit_incoming/entities/call_event.dart'; import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; -import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart'; import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; @@ -28,7 +27,6 @@ import 'package:lottie/lottie.dart'; import 'core/cache_consts.dart'; import 'core/utils/push_notification_handler.dart'; - class SplashPage extends StatefulWidget { const SplashPage({super.key}); @@ -48,7 +46,6 @@ class _SplashScreenState extends State { PushNotificationHandler().init(context); // Asyncronously }, ); - // TODO: This next line should be tested. This stops the app at this step, sometimes await authVm.getServicePrivilege(); Timer(Duration(seconds: 2, milliseconds: 500), () async { bool isAppOpenedFromCall = getIt.get().getBool(key: CacheConst.isAppOpenedFromCall) ?? false; @@ -80,7 +77,6 @@ class _SplashScreenState extends State { } navigateToTeleConsult() async { - String roomID = await Utils.getStringFromPrefs(CacheConst.zoomRoomID); String callTypeID = await Utils.getStringFromPrefs(CacheConst.callTypeID); @@ -192,9 +188,7 @@ class _SplashScreenState extends State { String sessionIdFromCache = await Utils.getStringFromPrefs(CacheConst.zoomRoomID); // Use event data if available, otherwise use cache - String sessionId = (sessionIdFromEvent != null && sessionIdFromEvent.isNotEmpty) - ? sessionIdFromEvent - : sessionIdFromCache; + String sessionId = (sessionIdFromEvent != null && sessionIdFromEvent.isNotEmpty) ? sessionIdFromEvent : sessionIdFromCache; print('🔍 Session ID from event: "$sessionIdFromEvent"'); print('🔍 Session ID from cache: "$sessionIdFromCache"'); @@ -259,11 +253,7 @@ class _SplashScreenState extends State { final apiClient = getIt.get(); - Map requestBody = { - "Open_SessionID": sessionId, - "SessionStatus": sessionStatus, - "SessionEndedBy": sessionEndedBy - }; + Map requestBody = {"Open_SessionID": sessionId, "SessionStatus": sessionStatus, "SessionEndedBy": sessionEndedBy}; await apiClient.post( CHANGE_PATIENT_ER_SESSION, diff --git a/lib/widgets/buttons/custom_button.dart b/lib/widgets/buttons/custom_button.dart index d868755a..3f4902e0 100644 --- a/lib/widgets/buttons/custom_button.dart +++ b/lib/widgets/buttons/custom_button.dart @@ -81,13 +81,18 @@ class CustomButton extends StatelessWidget { children: [ if (icon != null) Padding( - padding: text.isNotEmpty ? EdgeInsets.only(right: 4.w, left: 4.w) : EdgeInsets.zero, - child: Utils.buildSvgWithAssets(icon: icon!, iconColor: iconColor, isDisabled: isButtonDisabled, width: iconS, height: iconS, applyThemeColor: applyThemeColor), + 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, + width: iconS, + height: iconS, + applyThemeColor: applyThemeColor), ), if (text.isNotEmpty && icon != null) Flexible( - child: - Text( + child: Text( text, overflow: textOverflow ?? TextOverflow.ellipsis, maxLines: 1, diff --git a/lib/widgets/chip/app_custom_chip_widget.dart b/lib/widgets/chip/app_custom_chip_widget.dart index 58ce21a0..734ca2cc 100644 --- a/lib/widgets/chip/app_custom_chip_widget.dart +++ b/lib/widgets/chip/app_custom_chip_widget.dart @@ -8,29 +8,28 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:smooth_corner/smooth_corner.dart'; class AppCustomChipWidget extends StatelessWidget { - const AppCustomChipWidget({ - super.key, - this.labelText, - this.textColor, - this.backgroundColor, - this.iconSize, - this.icon = "", - this.iconColor, - this.richText, - this.iconHasColor = true, - this.shape, - this.deleteIcon, - this.deleteIconSize, - this.deleteIconColor, - this.deleteIconHasColor = false, - this.padding = EdgeInsets.zero, - this.isIconPNG = false, - this.onChipTap, - this.labelPadding, - this.onDeleteTap, - this.applyThemeColor = true, - this.isEnglishOnly = false - }); + const AppCustomChipWidget( + {super.key, + this.labelText, + this.textColor, + this.backgroundColor, + this.iconSize, + this.icon = "", + this.iconColor, + this.richText, + this.iconHasColor = true, + this.shape, + this.deleteIcon, + this.deleteIconSize, + this.deleteIconColor, + this.deleteIconHasColor = false, + this.padding = EdgeInsets.zero, + this.isIconPNG = false, + this.onChipTap, + this.labelPadding, + this.onDeleteTap, + this.applyThemeColor = true, + this.isEnglishOnly = false}); final String? labelText; final Widget? richText; @@ -58,7 +57,7 @@ class AppCustomChipWidget extends StatelessWidget { final iconS = iconSize ?? 12.w; final resolvedTextColor = textColor ?? AppColors.textColor; final resolvedBackgroundColor = backgroundColor ?? AppColors.greyColor; - final resolvedIconColor = iconColor ?? AppColors.textColor; + final resolvedIconColor = (iconColor ?? AppColors.textColor); final resolvedDeleteIconColor = deleteIconColor ?? AppColors.textColor; return GestureDetector( onTap: onChipTap, @@ -89,7 +88,8 @@ class AppCustomChipWidget extends StatelessWidget { applyThemeColor: applyThemeColor, ) : SizedBox.shrink(), - label: richText ?? (labelText?? "").toText10(isBold: true, letterSpacing: 0, color: resolvedTextColor, isEnglishOnly: isEnglishOnly), + label: + richText ?? (labelText ?? "").toText10(isBold: true, letterSpacing: 0, color: resolvedTextColor, isEnglishOnly: isEnglishOnly), padding: padding, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, labelPadding: labelPadding ?? EdgeInsetsDirectional.only(end: deleteIcon?.isNotEmpty == true ? 2.w : 8.w), @@ -119,7 +119,9 @@ class AppCustomChipWidget extends StatelessWidget { ) : Chip( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - label: richText ?? (labelText?? "").toText10(isBold: true, letterSpacing: 0, color: resolvedTextColor, isCenter: true, isEnglishOnly: isEnglishOnly), + label: richText ?? + (labelText ?? "") + .toText10(isBold: true, letterSpacing: 0, color: resolvedTextColor, isCenter: true, isEnglishOnly: isEnglishOnly), padding: EdgeInsets.zero, backgroundColor: resolvedBackgroundColor, shape: shape ?? @@ -133,7 +135,11 @@ class AppCustomChipWidget extends StatelessWidget { ? InkWell( onTap: onDeleteTap, child: Utils.buildSvgWithAssets( - icon: deleteIcon!, width: iconS, height: iconS, iconColor: deleteIconHasColor ? resolvedDeleteIconColor : null, applyThemeColor: applyThemeColor)) + icon: deleteIcon!, + width: iconS, + height: iconS, + iconColor: deleteIconHasColor ? resolvedDeleteIconColor : null, + applyThemeColor: applyThemeColor)) : null, onDeleted: deleteIcon?.isNotEmpty == true ? () {} : null, ), diff --git a/lib/widgets/image_picker.dart b/lib/widgets/image_picker.dart index 1b6fd3ab..e849cc96 100644 --- a/lib/widgets/image_picker.dart +++ b/lib/widgets/image_picker.dart @@ -2,12 +2,12 @@ import 'dart:convert'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.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/attachment_options.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:image_picker/image_picker.dart'; import 'package:image_picker_android/image_picker_android.dart'; @@ -21,12 +21,13 @@ class ImageOptions { BuildContext context, bool showFilesOption, Function(String, File) image, { + String? title, Future Function()? checkCameraPermission, Future Function()? checkGalleryPermission, }) { showCommonBottomSheetWithoutHeight( context, - title: LocaleKeys.profilePicture.tr(context: context), + title: title ?? LocaleKeys.profilePicture.tr(context: context), child: AttachmentOptions( showFilesOption: showFilesOption, onCameraTap: () async { @@ -78,11 +79,11 @@ class ImageOptions { } }, onFilesTap: () async { - FilePickerResult? result = await FilePicker.pickFiles( + FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: [ 'jpg', - 'jpeg ', + 'jpeg', 'pdf', 'txt', 'docx', @@ -223,7 +224,8 @@ void galleryImageAndroid(Function(String, File) image) async { } try { - final XFile? pickedFile = await _pickerPlatform.getImageFromSource(source: ImageSource.gallery, options: const ImagePickerOptions(imageQuality: 20)); + final XFile? pickedFile = + await _pickerPlatform.getImageFromSource(source: ImageSource.gallery, options: const ImagePickerOptions(imageQuality: 20)); if (pickedFile == null) return; @@ -269,7 +271,9 @@ class _BottomSheet extends StatelessWidget { Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(vertical: 12.0), - decoration: BoxDecoration(color: Theme.of(context).scaffoldBackgroundColor, borderRadius: const BorderRadius.only(topLeft: Radius.circular(16.0), topRight: Radius.circular(16.0))), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: const BorderRadius.only(topLeft: Radius.circular(16.0), topRight: Radius.circular(16.0))), child: SafeArea( top: false, child: Column( diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index e77cf96d..16a955c8 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -1,3 +1,5 @@ +import 'dart:ui' as ui; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -16,7 +18,6 @@ import 'package:hmg_patient_app_new/widgets/date_range_selector/date_range_calen 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/time_picker_widget.dart'; -import 'dart:ui' as ui; class TextInputWidget extends StatelessWidget { final String labelText; @@ -54,51 +55,53 @@ class TextInputWidget extends StatelessWidget { final Color? hintColor; final bool? isHideSwitcher; final bool? isArrowTrailing; - + final bool leadingIconNeedsContainer; final String? fontFamily; // final List countryList; // final Function(Country)? onCountryChange; - TextInputWidget( - {super.key, - required this.labelText, - required this.hintText, - this.controller, - this.onChange, - this.onCalendarTypeChanged, - this.prefix, - this.isEnable = true, - this.isBorderAllowed = true, - this.isAllowRadius = true, - this.isReadOnly = false, - this.keyboardType = TextInputType.number, - this.focusNode, - this.autoFocus = false, - this.padding, - this.isAllowLeadingIcon = false, - this.leadingIcon, - this.isCountryDropDown = false, - this.hasError = false, - this.errorMessage, - this.onCountryChange, - this.selectionType, - this.fontSize, - this.isWalletAmountInput = false, - this.suffix, - this.labelColor, - this.onSubmitted, - this.hintColor, - // multiline defaults - this.isMultiline = false, - this.minLines = 3, - this.maxLines = 6, - this.isHideSwitcher, - this.isArrowTrailing, - this.fontFamily, - // this.countryList = const [], - // this.onCountryChange, - this.btnTitle}); + TextInputWidget({ + super.key, + required this.labelText, + required this.hintText, + this.controller, + this.onChange, + this.onCalendarTypeChanged, + this.prefix, + this.isEnable = true, + this.isBorderAllowed = true, + this.isAllowRadius = true, + this.isReadOnly = false, + this.keyboardType = TextInputType.number, + this.focusNode, + this.autoFocus = false, + this.padding, + this.isAllowLeadingIcon = false, + this.leadingIcon, + this.isCountryDropDown = false, + this.hasError = false, + this.errorMessage, + this.onCountryChange, + this.selectionType, + this.fontSize, + this.isWalletAmountInput = false, + this.suffix, + this.labelColor, + this.onSubmitted, + this.hintColor, + // multiline defaults + this.isMultiline = false, + this.minLines = 3, + this.maxLines = 6, + this.isHideSwitcher, + this.isArrowTrailing, + this.fontFamily, + this.leadingIconNeedsContainer = true, + // this.countryList = const [], + // this.onCountryChange, + this.btnTitle, + }); final FocusNode _focusNode = FocusNode(); @@ -220,16 +223,19 @@ class TextInputWidget extends StatelessWidget { } Widget _buildLeadingIcon(BuildContext context) { - return Container( - height: 40.h, - width: 40.h, - // margin: EdgeInsets.symmetric(horizontal: 10.w), - padding: EdgeInsets.all(8.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - borderRadius: 12.r, - color: AppColors.greyColor, - ), - child: Utils.buildSvgWithAssets(icon: leadingIcon!, applyThemeColor: false)); + if (leadingIconNeedsContainer) { + return Container( + height: 40.h, + width: 40.h, + // margin: EdgeInsets.symmetric(horizontal: 10.w), + padding: EdgeInsets.all(8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + borderRadius: 12.r, + color: AppColors.greyColor, + ), + child: Utils.buildSvgWithAssets(icon: leadingIcon!, applyThemeColor: false)); + } + return Utils.buildSvgWithAssets(icon: leadingIcon!, applyThemeColor: false, height: 40.h, width: 40.h); } Widget _buildTrailingIcon(BuildContext context, {bool isArrowTrailing = false}) { @@ -403,7 +409,9 @@ class TextInputWidget extends StatelessWidget { hintLocales: const [Locale('en', 'US')], enabled: isEnable, scrollPadding: EdgeInsets.zero, - keyboardType: isMultiline ? TextInputType.multiline : (isWalletAmountInput! ? const TextInputType.numberWithOptions(decimal: true) : keyboardType), + keyboardType: isMultiline + ? TextInputType.multiline + : (isWalletAmountInput! ? const TextInputType.numberWithOptions(decimal: true) : keyboardType), controller: controller, readOnly: isReadOnly, textAlignVertical: TextAlignVertical.top, diff --git a/pubspec.lock b/pubspec.lock index dd27bab8..ce36792b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1042,13 +1042,13 @@ packages: source: hosted version: "1.2.1" image_picker_android: - dependency: transitive + dependency: "direct main" description: name: image_picker_android - sha256: "5e9bf126c37c117cf8094215373c6d561117a3cfb50ebc5add1a61dc6e224677" + sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f url: "https://pub.dev" source: hosted - version: "0.8.13+10" + version: "0.8.13+17" image_picker_for_web: dependency: transitive description: @@ -1082,7 +1082,7 @@ packages: source: hosted version: "0.2.2+1" image_picker_platform_interface: - dependency: transitive + dependency: "direct main" description: name: image_picker_platform_interface sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"