From 7cf593443d18c5ee9490105ddb9b6a2c3e799daa Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Thu, 7 May 2026 18:20:48 +0300 Subject: [PATCH 1/6] Refund Request Started --- assets/langs/ar-SA.json | 11 +- assets/langs/en-US.json | 17 +- ios/Podfile.lock | 4 +- ios/Runner.xcodeproj/project.pbxproj | 32 +- lib/core/api_consts.dart | 5 +- lib/core/dependencies.dart | 68 ++- .../submit_refund_request_model.dart | 1 + .../refund_request_list_response_model.dart | 1 + .../refundable_invoices_response_model.dart | 90 ++++ .../refund_request/refund_request_repo.dart | 81 +++ .../refund_request_view_model.dart | 194 ++++++++ lib/generated/locale_keys.g.dart | 4 +- lib/main.dart | 21 +- .../habib_wallet/recharge_wallet_page.dart | 466 +++++++++--------- lib/presentation/lab/lab_orders_page.dart | 2 +- .../medical_file/medical_file_page.dart | 23 +- .../refund_request_create_page.dart | 442 +++++++++++++++++ .../select_hospital_bottom_sheet_refund.dart | 223 +++++++++ lib/routes/app_routes.dart | 12 +- pubspec.lock | 8 +- 20 files changed, 1392 insertions(+), 313 deletions(-) create mode 100644 lib/features/refund_request/models/req_models/submit_refund_request_model.dart create mode 100644 lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart create mode 100644 lib/features/refund_request/models/resp_models/refundable_invoices_response_model.dart create mode 100644 lib/features/refund_request/refund_request_repo.dart create mode 100644 lib/features/refund_request/refund_request_view_model.dart create mode 100644 lib/presentation/refund_request/refund_request_create_page.dart create mode 100644 lib/presentation/refund_request/widgets/select_hospital_bottom_sheet_refund.dart diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index dab50baa..6ba4a959 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1780,11 +1780,11 @@ "illurgyInfomation": "الحساسية", "vaccineInfomation": "التطعيمات", "approvals1": "موافقات", - "convertBloodcholesterolInfo":"حول قيمة كوليسترول الدم بين ملجم/ديسيلتر و مليمول/لتر", + "convertBloodcholesterolInfo": "حول قيمة كوليسترول الدم بين ملجم/ديسيلتر و مليمول/لتر", "carbsAlert": "حساب نسب المغذيات (الكربوهيدرات ،البروتين والدهون) بالسعرات الحرارية والقرامات وفقا لنسب محددة مسبقا", "bodyFatAlert": "يحسب بناءً على الحجم", "ibwAlert": "حساب الوزن المثالي بناءً على الطول، الوزن، وبنية الجسم", - "glucoseAlert":"تغيير وحدات القياس بين مليجرام لكل ديسيلتر و ملي مول لكل لتر", + "glucoseAlert": "تغيير وحدات القياس بين مليجرام لكل ديسيلتر و ملي مول لكل لتر", "selectBloodGroup": "اختر الفصيلة", "routePrescription": "طريقة الاستخدام", "noNearestER": "لا يوجد طوارئ قريب منك", @@ -1820,9 +1820,9 @@ "dragToMoveAround": "اسحب للتحرك", "startExploring": "ابدأ الاستكشاف", "symptomsCheckSuggestions": "الاحتمالات", - "appointmentArrived": "تم الحضور", + "appointmentArrived": "تم الحضور", "calendarPermissionAlertForAppointment": "يرجى منح إذن الوصول إلى التقويم من إعدادات التطبيق لضبط تذكير الموعد", -"rechargePageKey": "شحن المحفظة", + "rechargePageKey": "شحن المحفظة", "selectFamilyFile": "اختيار ", "ancillaryOrdersListNew": "طلبات غير مكتملة", "medicalReportNew": "التقارير الطبية", @@ -1845,5 +1845,6 @@ "maxOneFileAllowed": "يمكن رفع ملف واحد فقط", "fileSizeExceedsLimit": "يجب ألا يتجاوز حجم الملف 1 ميغابايت", "completedPrescriptionOrder": "تمت الخدمة", - "submitEReferral": "تقديم الطلب" + "submitEReferral": "تقديم الطلب", + "requestRefund": "طلب استرداد" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index b4dd35f7..312a8121 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...", @@ -1747,7 +1747,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 +1765,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 +1810,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", @@ -1835,7 +1835,8 @@ "maxOneFileAllowed": "Only 1 file can be uploaded", "fileSizeExceedsLimit": "File size must not exceed 1 MB", "completedPrescriptionOrder": "Completed", - "submitEReferral": "Submit" + "submitEReferral": "Submit", + "requestRefund": "Request Refund" } 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_consts.dart b/lib/core/api_consts.dart index 74fa0ee7..15541878 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -4,7 +4,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -269,6 +269,9 @@ 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 getAiAnalysisURL = "https://api.labpredict.com"; static String getAiRadiologyAnalysis = "$getAiAnalysisURL/radiology"; static String getAiOverViewLabOrdersNew = "$getAiAnalysisURL/analyze_lab_orders"; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 1de302b7..1e65535a 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'; @@ -55,6 +55,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'; @@ -66,7 +68,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'; @@ -83,13 +84,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; @@ -176,18 +175,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())); // ViewModels // Global/shared VMs → LazySingleton @@ -198,13 +193,16 @@ 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()), @@ -223,7 +221,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( @@ -237,7 +240,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( @@ -310,22 +319,33 @@ 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(() => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt())); + getIt.registerLazySingleton( + () => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt())); + getIt.registerLazySingleton( + () => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt())); + getIt.registerLazySingleton(() => RefundRequestViewModel( + appState: getIt(), + refundRequestRepo: getIt(), + errorHandlerService: getIt(), + )); } } 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..8b137891 --- /dev/null +++ b/lib/features/refund_request/models/req_models/submit_refund_request_model.dart @@ -0,0 +1 @@ + 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..8b137891 --- /dev/null +++ b/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart @@ -0,0 +1 @@ + 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..1df3e76b --- /dev/null +++ b/lib/features/refund_request/models/resp_models/refundable_invoices_response_model.dart @@ -0,0 +1,90 @@ +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, + }; + } +} + 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..e9f64d69 --- /dev/null +++ b/lib/features/refund_request/refund_request_repo.dart @@ -0,0 +1,81 @@ +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/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}); +} + +class RefundRequestRepoImp implements RefundRequestRepo { + final ApiClient apiClient; + final LoggerService loggerService; + + RefundRequestRepoImp({required this.apiClient, required this.loggerService}); + + @override + Future>>> + getRefundableInvoices({required int projectID}) async { + // ApiClient auto-injects: PatientID, TokenID, LanguageID, PatientOutSA, + // PatientTypeID, IPAdress, generalid, VersionID, Channel + 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())); + } + } +} + 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..030b2a70 --- /dev/null +++ b/lib/features/refund_request/refund_request_view_model.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_state.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/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/services/error_handler_service.dart'; + +enum RefundTabType { invoices, hmgWallet } + +/// Lightweight model representing a refundable procedure line item. +class RefundableProcedure { + final int procedureId; + final String procedureName; + final String? procedureNameN; + final double patientShare; + bool isSelected; + + RefundableProcedure({ + required this.procedureId, + required this.procedureName, + this.procedureNameN, + required this.patientShare, + this.isSelected = false, + }); +} + +/// 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(); + } + } + + // ── 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(); + } + + List get selectedProcedures => procedures.where((p) => p.isSelected).toList(); + + // ── Calculation ─────────────────────────────────────────────────────────── + double get totalRefundableAmount { + double total = 0.0; + for (final p in selectedProcedures) { + total += p.patientShare; + } + return total; + } + + // ── Validation ──────────────────────────────────────────────────────────── + bool get isStep1Valid => selectedHospital != null && selectedInvoice != null; + + bool get isStep2Valid => selectedProcedures.isNotEmpty; +} diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index ae8c6817..84cff5ad 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'; @@ -1838,5 +1838,5 @@ abstract class LocaleKeys { static const fileSizeExceedsLimit = 'fileSizeExceedsLimit'; static const completedPrescriptionOrder = 'completedPrescriptionOrder'; static const submitEReferral = 'submitEReferral'; - + static const requestRefund = 'requestRefund'; } diff --git a/lib/main.dart b/lib/main.dart index ffe2043e..938904a3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,17 +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/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'; @@ -39,22 +38,19 @@ import 'package:hmg_patient_app_new/features/prescriptions/prescriptions_view_mo 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'; @@ -72,7 +68,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) { @@ -257,11 +252,15 @@ void main() async { ), ChangeNotifierProvider( create: (_) => getIt.get(), - ) , + ), ChangeNotifierProvider( create: (_) => getIt.get(), - ), ChangeNotifierProvider( + ), + ChangeNotifierProvider( create: (_) => getIt.get(), + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), ) ], child: MyApp()), ), // Wrap your app 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/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 b60cd8bc..560768b5 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -40,9 +40,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'; @@ -62,6 +59,7 @@ import 'package:hmg_patient_app_new/presentation/my_invoices/my_invoices_list.da import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart'; import 'package:hmg_patient_app_new/presentation/radiology/radiology_orders_page.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/todo_page.dart'; +import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_create_page.dart'; import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_page.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; @@ -584,7 +582,6 @@ class _MedicalFilePageState extends State { ) : Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.bgRedLightColor, borderRadius: 12.r, hasShadow: false, ), @@ -653,11 +650,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), @@ -666,10 +659,20 @@ class _MedicalFilePageState extends State { svgIcon: AppAssets.invoices_list_icon, isLargeText: true, iconSize: 36.w, + ).onPress(() { + 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: MyInvoicesList(), + page: const RefundRequestCreatePage(initialTabIndex: 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..692a10a6 --- /dev/null +++ b/lib/presentation/refund_request/refund_request_create_page.dart @@ -0,0 +1,442 @@ +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/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/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/widgets/select_hospital_bottom_sheet_refund.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/custom_tab_bar.dart'; +import 'package:hmg_patient_app_new/widgets/stepper/stepper_widget.dart'; +import 'package:provider/provider.dart'; + +class RefundRequestCreatePage extends StatefulWidget { + /// 0 = Invoices (default), 1 = HMG Wallet + final int initialTabIndex; + + const RefundRequestCreatePage({super.key, this.initialTabIndex = 0}); + + @override + State createState() => _RefundRequestCreatePageState(); +} + +class _RefundRequestCreatePageState extends State { + late RefundRequestViewModel _vm; + late HabibWalletViewModel _habibWalletVM; + late AppState _appState; + + // TODO: Replace with actual procedure list fetched from API per invoice + List _mockProceduresForInvoice(int invoiceNo) => [ + RefundableProcedure(procedureId: 101, procedureName: 'Consultation', procedureNameN: 'استشارة', patientShare: 120.0), + RefundableProcedure(procedureId: 102, procedureName: 'Blood Test – CBC', procedureNameN: 'تحليل دم شامل', patientShare: 85.50), + RefundableProcedure(procedureId: 103, procedureName: 'X-Ray Chest', procedureNameN: 'أشعة سينية صدر', patientShare: 200.0), + RefundableProcedure(procedureId: 104, procedureName: 'ECG', procedureNameN: 'تخطيط قلب', patientShare: 150.0), + ]; + + @override + void initState() { + super.initState(); + _appState = getIt.get(); + WidgetsBinding.instance.addPostFrameCallback((_) { + // Load hospital list the same way RechargeWalletPage does + _habibWalletVM.getProjectsList(); + }); + } + + // ── 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, + 4.h, + ); + }), + ); + }, + ), + ); + } + + // ── Step 1 — exactly mirrors RechargeWalletPage hospital row ───────────── + + Widget _buildStep1Content(RefundRequestViewModel vm) { + final isArabic = _appState.isArabic(); + return Consumer( + builder: (context, refundVM, _) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Padding( + padding: EdgeInsets.all(16.h), + child: 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), + (refundVM.selectedHospital != null + ? ((isArabic + ? (refundVM.selectedHospital!.nameN?.toString() ?? refundVM.selectedHospital!.name) + : refundVM.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: SelectHospitalBottomSheetRefund(), + callBackFunc: () {}, + ); + }), + ), + ); + }, + ); + } + + // ── Step 2 — procedure checkboxes ───────────────────────────────────────── + + Widget _buildProcedureCard(RefundableProcedure procedure, int index) { + 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, + margin: EdgeInsets.only(bottom: 12.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: procedure.isSelected ? AppColors.primaryRedColor.withAlpha(12) : AppColors.whiteColor, + borderRadius: 16.h, + hasShadow: true, + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _vm.toggleProcedureSelection(procedure), + borderRadius: BorderRadius.circular(16.h), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Checkbox( + value: procedure.isSelected, + onChanged: (_) => _vm.toggleProcedureSelection(procedure), + activeColor: AppColors.primaryRedColor, + side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1.5), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4.r), + ), + ), + SizedBox(width: 8.w), + Expanded(child: name.toText14(isBold: true)), + SizedBox(width: 8.w), + Utils.getPaymentAmountWithSymbol( + procedure.patientShare.toStringAsFixed(2).toText14( + isBold: true, + isEnglishOnly: true, + color: AppColors.primaryRedColor, + ), + AppColors.textColorLight, + 13, + isSaudiCurrency: true, + ), + ], + ), + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildStep2Content(RefundRequestViewModel vm) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Text( + 'Select Procedures', + style: TextStyle( + fontSize: 20.f, + fontWeight: FontWeight.w600, + color: AppColors.blackColor, + letterSpacing: -0.5, + ), + ), + ), + SizedBox(height: 24.h), + if (vm.isProceduresLoading) + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.symmetric(horizontal: 24.w), + itemCount: 4, + itemBuilder: (_, index) => Container( + height: 56.h, + margin: EdgeInsets.only(bottom: 12.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: 'No procedures available for refund'.toText14(color: AppColors.greyTextColor), + ), + ) + else + AnimationLimiter( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.bgScaffoldColor, + borderRadius: 24.h, + hasShadow: false, + ), + padding: EdgeInsets.all(12.w), + child: Column( + children: List.generate( + vm.procedures.length, + (index) => _buildProcedureCard(vm.procedures[index], index), + ), + ), + ), + ), + ), + ], + ); + } + + // ── Bottom bar ──────────────────────────────────────────────────────────── + + Widget _buildBottomBar(RefundRequestViewModel vm) { + final isStep2 = vm.currentStep == 1; + final isWallet = vm.selectedTab == RefundTabType.hmgWallet; + + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (isStep2 && !isWallet && vm.selectedProcedures.isNotEmpty) ...[ + Padding( + padding: EdgeInsets.fromLTRB(24.w, 16.h, 24.w, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + 'Total Refundable Amount'.toText12( + color: AppColors.textColorLight, + isBold: true, + ), + Utils.getPaymentAmountWithSymbol( + vm.totalRefundableAmount.toStringAsFixed(2).toText18( + isBold: true, + isEnglishOnly: true, + color: AppColors.primaryRedColor, + ), + AppColors.textColorLight, + 14, + isSaudiCurrency: true, + ), + ], + ), + ), + ], + CustomButton( + text: isStep2 ? LocaleKeys.submit.tr(context: context) : LocaleKeys.next.tr(context: context), + onPressed: () { + if (!isStep2) { + if (isWallet || vm.isStep1Valid) vm.nextStep(); + } else { + // TODO: Submit API call + } + }, + isDisabled: (!isWallet && !isStep2 && !vm.isStep1Valid) || (isStep2 && !isWallet && !vm.isStep2Valid), + ).paddingSymmetrical(24.h, 24.h), + ], + ), + ); + } + + // ── HMG Wallet placeholder ───────────────────────────────────────────────── + + Widget _buildHmgWalletContent() { + return KeyedSubtree( + key: const ValueKey('hmgWallet'), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + padding: EdgeInsets.all(24.w), + child: Column( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.wallet, + iconColor: AppColors.primaryRedColor, + width: 48.h, + height: 48.h, + ), + SizedBox(height: 16.h), + 'HMG Wallet Refund'.toText16(isBold: true), + SizedBox(height: 8.h), + 'Refund requests to HMG Wallet will be processed within 1-3 business days.'.toText14(color: AppColors.greyTextColor, isCenter: true), + ], + ), + ), + ), + ); + } + + // ── Build ───────────────────────────────────────────────────────────────── + + @override + Widget build(BuildContext context) { + _habibWalletVM = context.read(); + return Consumer( + builder: (context, vm, _) { + _vm = vm; + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: LocaleKeys.requestRefund.tr(context: context), + history: () async { + // TODO: Navigate to refund history page + }, + child: SingleChildScrollView( + padding: EdgeInsets.only(bottom: 32.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Tab bar + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: CustomTabBar( + initialIndex: widget.initialTabIndex, + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.primaryRedColor.withAlpha(25), + tabs: [ + CustomTabBarModel(null, 'Invoices'), + CustomTabBarModel(null, 'HMG Wallet'), + ], + onTabChange: (index) { + vm.setTab(index == 0 ? RefundTabType.invoices : RefundTabType.hmgWallet); + vm.goToStep(0); + }, + ), + ), + SizedBox(height: 24.h), + // Stepper (invoices tab only) + if (vm.selectedTab == RefundTabType.invoices) ...[ + _buildStepper(vm), + SizedBox(height: 24.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.selectedTab == RefundTabType.hmgWallet + ? _buildHmgWalletContent() + : 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: _buildStep2Content(vm), + ), + ), + ], + ), + ), + ), + ), + _buildBottomBar(vm), + ], + ), + ); + }, + ); + } +} diff --git a/lib/presentation/refund_request/widgets/select_hospital_bottom_sheet_refund.dart b/lib/presentation/refund_request/widgets/select_hospital_bottom_sheet_refund.dart new file mode 100644 index 00000000..e38a831e --- /dev/null +++ b/lib/presentation/refund_request/widgets/select_hospital_bottom_sheet_refund.dart @@ -0,0 +1,223 @@ +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/location_util.dart'; +import 'package:hmg_patient_app_new/core/utils/doctor_response_mapper.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/features/my_appointments/models/resp_models/hospital_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/habib_wallet/widgets/hospital_list_item.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:provider/provider.dart'; + +class SelectHospitalBottomSheetRefund extends StatefulWidget { + const SelectHospitalBottomSheetRefund({super.key}); + + @override + State createState() => + _SelectHospitalBottomSheetRefundState(); +} + +class _SelectHospitalBottomSheetRefundState + extends State { + late RefundRequestViewModel refundVM; + late HabibWalletViewModel habibWalletVM; + bool sortByLocation = false; + bool isLoading = false; + List displayList = []; + + @override + void initState() { + super.initState(); + final appState = getIt.get(); + sortByLocation = (appState.userLat != 0.0) && (appState.userLong != 0.0); + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateDisplayList(); + }); + } + + void _updateDisplayList() { + final appState = getIt.get(); + // Reuse the same hospital list already loaded in HabibWalletViewModel + final hospitals = List.from(habibWalletVM.advancePaymentHospitals); + + if (sortByLocation && appState.userLat != 0.0 && appState.userLong != 0.0) { + hospitals.sort((a, b) { + final distA = (a.latitude != null && a.longitude != null) + ? DoctorMapper.calculateDistance(appState.userLat, appState.userLong, + double.parse(a.latitude!), double.parse(a.longitude!)) + : double.infinity; + final distB = (b.latitude != null && b.longitude != null) + ? DoctorMapper.calculateDistance(appState.userLat, appState.userLong, + double.parse(b.latitude!), double.parse(b.longitude!)) + : double.infinity; + return distA.compareTo(distB); + }); + } + + setState(() { + displayList = hospitals; + isLoading = false; + }); + } + + void _refreshHospitalListAfterApi() { + void listener() { + if (habibWalletVM.advancePaymentHospitals.isNotEmpty) { + habibWalletVM.removeListener(listener); + _updateDisplayList(); + } + } + habibWalletVM.addListener(listener); + habibWalletVM.getProjectsList(); + } + + void _handleSortByLocationToggle(bool value) { + if (value) { + final locationUtils = getIt.get(); + locationUtils.getLocation( + isShowConfirmDialog: true, + onSuccess: (latLng) { + setState(() { + sortByLocation = true; + isLoading = true; + }); + _refreshHospitalListAfterApi(); + }, + onFailure: () { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.giveLocationPermissionForNearestList + .tr(context: context), + isShowActionButtons: true, + onCancelTap: () => Navigator.of(context).pop(), + onConfirmTap: () async { + Navigator.of(context).pop(); + openAppSettings(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + setState(() => sortByLocation = false); + }, + onLocationDeniedForever: () { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.giveLocationPermissionForNearestList + .tr(context: context), + isShowActionButtons: true, + onCancelTap: () => Navigator.of(context).pop(), + onConfirmTap: () async { + Navigator.of(context).pop(); + openAppSettings(); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + setState(() => sortByLocation = false); + }, + ); + } else { + final appState = getIt.get(); + appState.resetLocation(); + setState(() { + sortByLocation = false; + isLoading = true; + }); + _refreshHospitalListAfterApi(); + } + } + + @override + Widget build(BuildContext context) { + refundVM = context.read(); + habibWalletVM = context.read(); + + if (displayList.isEmpty && habibWalletVM.advancePaymentHospitals.isNotEmpty) { + displayList = List.from(habibWalletVM.advancePaymentHospitals); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.selectHospitalForAdvancePayment + .tr(context: context) + .toText16(color: AppColors.greyTextColor, isBold: true), + SizedBox(height: 16.h), + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.location, + iconColor: AppColors.greyTextColor, + width: 18.h, + height: 18.h), + SizedBox(width: 8.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.sortByLocation.tr(context: context).toText14(isBold: true), + LocaleKeys.sortByNearestLocation + .tr(context: context) + .toText11(color: AppColors.textColorLight, isBold: true), + ], + ), + ], + ), + Switch( + value: sortByLocation, + onChanged: _handleSortByLocationToggle, + activeThumbColor: AppColors.successColor, + activeTrackColor: AppColors.successColor.withValues(alpha: 0.15), + ), + ], + ), + ), + SizedBox(height: 8.h), + isLoading + ? SizedBox( + height: MediaQuery.sizeOf(context).height * .4, + child: Center(child: Utils.getLoadingWidget()), + ) + : SizedBox( + height: MediaQuery.sizeOf(context).height * .4, + child: ListView.separated( + itemCount: displayList.length, + separatorBuilder: (_, __) => SizedBox(height: 16.h), + itemBuilder: (_, index) { + return HospitalListItemAdvancePayment( + hospitalModel: displayList[index], + isLocationEnabled: sortByLocation, + ).onPress(() { + refundVM.setSelectedHospital(displayList[index]); + Navigator.of(context).pop(); + }); + }, + ), + ), + ], + ); + } +} + diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index f4ddff9b..7b34e447 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -41,7 +41,8 @@ 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 '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 '../services/error_handler_service.dart'; class AppRoutes { @@ -101,6 +102,9 @@ class AppRoutes { // Weather static const String familyFilesPage = '/familyFilesPage'; + // Refund Request + static const String refundRequestCreatePage = '/refundRequestCreatePage'; + static Map get routes => { initialRoute: (context) => SplashPage(), loginScreen: (context) => LoginScreen(), @@ -160,6 +164,10 @@ class AppRoutes { qrParking: (context) => ChangeNotifierProvider( create: (_) => getIt(), child: const ParkingPage(), - ) + ), + refundRequestCreatePage: (context) { + final args = ModalRoute.of(context)?.settings.arguments as int?; + return RefundRequestCreatePage(initialTabIndex: args ?? 0); + }, }; } 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" -- 2.30.2 From 8aa152f1a0ab7ea16336e397ac3e9ad0128e60d7 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Mon, 11 May 2026 11:41:36 +0300 Subject: [PATCH 2/6] added apis --- .../refund_request/refund_request_repo.dart | 23 +- .../widgets/select_hospital_bottom_sheet.dart | 44 ++-- .../hmg_services/services_page.dart | 104 ++++---- .../refund_request_create_page.dart | 6 +- .../select_hospital_bottom_sheet_refund.dart | 223 ------------------ .../services_price_list_page.dart | 0 lib/routes/app_routes.dart | 6 +- 7 files changed, 93 insertions(+), 313 deletions(-) delete mode 100644 lib/presentation/refund_request/widgets/select_hospital_bottom_sheet_refund.dart rename lib/presentation/{servicesPriceList => service_price}/services_price_list_page.dart (100%) diff --git a/lib/features/refund_request/refund_request_repo.dart b/lib/features/refund_request/refund_request_repo.dart index e9f64d69..f8e603b6 100644 --- a/lib/features/refund_request/refund_request_repo.dart +++ b/lib/features/refund_request/refund_request_repo.dart @@ -7,8 +7,7 @@ import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/r import 'package:hmg_patient_app_new/services/logger_service.dart'; abstract class RefundRequestRepo { - Future>>> - getRefundableInvoices({required int projectID}); + Future>>> getRefundableInvoices({required int projectID}); } class RefundRequestRepoImp implements RefundRequestRepo { @@ -18,8 +17,7 @@ class RefundRequestRepoImp implements RefundRequestRepo { RefundRequestRepoImp({required this.apiClient, required this.loggerService}); @override - Future>>> - getRefundableInvoices({required int projectID}) async { + Future>>> getRefundableInvoices({required int projectID}) async { // ApiClient auto-injects: PatientID, TokenID, LanguageID, PatientOutSA, // PatientTypeID, IPAdress, generalid, VersionID, Channel Map body = { @@ -38,20 +36,14 @@ class RefundRequestRepoImp implements RefundRequestRepo { }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { - final refundableInvoicesObject = - response['RefundableInvoicesObject']; + final refundableInvoicesObject = response['RefundableInvoicesObject']; if (refundableInvoicesObject != null) { - final invoiceItemsJson = - refundableInvoicesObject['RefundableInvoiceItems']; + final invoiceItemsJson = refundableInvoicesObject['RefundableInvoiceItems']; - final List invoicesList = - invoiceItemsJson != null - ? (invoiceItemsJson as List) - .map((item) => RefundableInvoiceItem.fromJson( - item as Map)) - .toList() - : []; + final List invoicesList = invoiceItemsJson != null + ? (invoiceItemsJson as List).map((item) => RefundableInvoiceItem.fromJson(item as Map)).toList() + : []; apiResponse = GenericApiModel>( messageStatus: messageStatus, @@ -78,4 +70,3 @@ class RefundRequestRepoImp implements RefundRequestRepo { } } } - 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/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index b2028dd8..a4a9ee10 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -30,7 +30,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 +42,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(); @@ -560,55 +560,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/refund_request/refund_request_create_page.dart b/lib/presentation/refund_request/refund_request_create_page.dart index 692a10a6..76ae7520 100644 --- a/lib/presentation/refund_request/refund_request_create_page.dart +++ b/lib/presentation/refund_request/refund_request_create_page.dart @@ -11,7 +11,7 @@ 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/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/widgets/select_hospital_bottom_sheet_refund.dart'; +import 'package:hmg_patient_app_new/presentation/habib_wallet/widgets/select_hospital_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'; @@ -124,7 +124,9 @@ class _RefundRequestCreatePageState extends State { context, title: LocaleKeys.selectHospital.tr(context: context), isDismissible: false, - child: SelectHospitalBottomSheetRefund(), + child: SelectHospitalBottomSheet( + onHospitalSelected: (hospital) => _vm.setSelectedHospital(hospital), + ), callBackFunc: () {}, ); }), diff --git a/lib/presentation/refund_request/widgets/select_hospital_bottom_sheet_refund.dart b/lib/presentation/refund_request/widgets/select_hospital_bottom_sheet_refund.dart deleted file mode 100644 index e38a831e..00000000 --- a/lib/presentation/refund_request/widgets/select_hospital_bottom_sheet_refund.dart +++ /dev/null @@ -1,223 +0,0 @@ -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/location_util.dart'; -import 'package:hmg_patient_app_new/core/utils/doctor_response_mapper.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/features/my_appointments/models/resp_models/hospital_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/habib_wallet/widgets/hospital_list_item.dart'; -import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:provider/provider.dart'; - -class SelectHospitalBottomSheetRefund extends StatefulWidget { - const SelectHospitalBottomSheetRefund({super.key}); - - @override - State createState() => - _SelectHospitalBottomSheetRefundState(); -} - -class _SelectHospitalBottomSheetRefundState - extends State { - late RefundRequestViewModel refundVM; - late HabibWalletViewModel habibWalletVM; - bool sortByLocation = false; - bool isLoading = false; - List displayList = []; - - @override - void initState() { - super.initState(); - final appState = getIt.get(); - sortByLocation = (appState.userLat != 0.0) && (appState.userLong != 0.0); - WidgetsBinding.instance.addPostFrameCallback((_) { - _updateDisplayList(); - }); - } - - void _updateDisplayList() { - final appState = getIt.get(); - // Reuse the same hospital list already loaded in HabibWalletViewModel - final hospitals = List.from(habibWalletVM.advancePaymentHospitals); - - if (sortByLocation && appState.userLat != 0.0 && appState.userLong != 0.0) { - hospitals.sort((a, b) { - final distA = (a.latitude != null && a.longitude != null) - ? DoctorMapper.calculateDistance(appState.userLat, appState.userLong, - double.parse(a.latitude!), double.parse(a.longitude!)) - : double.infinity; - final distB = (b.latitude != null && b.longitude != null) - ? DoctorMapper.calculateDistance(appState.userLat, appState.userLong, - double.parse(b.latitude!), double.parse(b.longitude!)) - : double.infinity; - return distA.compareTo(distB); - }); - } - - setState(() { - displayList = hospitals; - isLoading = false; - }); - } - - void _refreshHospitalListAfterApi() { - void listener() { - if (habibWalletVM.advancePaymentHospitals.isNotEmpty) { - habibWalletVM.removeListener(listener); - _updateDisplayList(); - } - } - habibWalletVM.addListener(listener); - habibWalletVM.getProjectsList(); - } - - void _handleSortByLocationToggle(bool value) { - if (value) { - final locationUtils = getIt.get(); - locationUtils.getLocation( - isShowConfirmDialog: true, - onSuccess: (latLng) { - setState(() { - sortByLocation = true; - isLoading = true; - }); - _refreshHospitalListAfterApi(); - }, - onFailure: () { - showCommonBottomSheetWithoutHeight( - title: LocaleKeys.notice.tr(context: context), - context, - child: Utils.getWarningWidget( - loadingText: LocaleKeys.giveLocationPermissionForNearestList - .tr(context: context), - isShowActionButtons: true, - onCancelTap: () => Navigator.of(context).pop(), - onConfirmTap: () async { - Navigator.of(context).pop(); - openAppSettings(); - }, - ), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - setState(() => sortByLocation = false); - }, - onLocationDeniedForever: () { - showCommonBottomSheetWithoutHeight( - title: LocaleKeys.notice.tr(context: context), - context, - child: Utils.getWarningWidget( - loadingText: LocaleKeys.giveLocationPermissionForNearestList - .tr(context: context), - isShowActionButtons: true, - onCancelTap: () => Navigator.of(context).pop(), - onConfirmTap: () async { - Navigator.of(context).pop(); - openAppSettings(); - }, - ), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - setState(() => sortByLocation = false); - }, - ); - } else { - final appState = getIt.get(); - appState.resetLocation(); - setState(() { - sortByLocation = false; - isLoading = true; - }); - _refreshHospitalListAfterApi(); - } - } - - @override - Widget build(BuildContext context) { - refundVM = context.read(); - habibWalletVM = context.read(); - - if (displayList.isEmpty && habibWalletVM.advancePaymentHospitals.isNotEmpty) { - displayList = List.from(habibWalletVM.advancePaymentHospitals); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.selectHospitalForAdvancePayment - .tr(context: context) - .toText16(color: AppColors.greyTextColor, isBold: true), - SizedBox(height: 16.h), - Padding( - padding: EdgeInsets.symmetric(horizontal: 4.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.location, - iconColor: AppColors.greyTextColor, - width: 18.h, - height: 18.h), - SizedBox(width: 8.w), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.sortByLocation.tr(context: context).toText14(isBold: true), - LocaleKeys.sortByNearestLocation - .tr(context: context) - .toText11(color: AppColors.textColorLight, isBold: true), - ], - ), - ], - ), - Switch( - value: sortByLocation, - onChanged: _handleSortByLocationToggle, - activeThumbColor: AppColors.successColor, - activeTrackColor: AppColors.successColor.withValues(alpha: 0.15), - ), - ], - ), - ), - SizedBox(height: 8.h), - isLoading - ? SizedBox( - height: MediaQuery.sizeOf(context).height * .4, - child: Center(child: Utils.getLoadingWidget()), - ) - : SizedBox( - height: MediaQuery.sizeOf(context).height * .4, - child: ListView.separated( - itemCount: displayList.length, - separatorBuilder: (_, __) => SizedBox(height: 16.h), - itemBuilder: (_, index) { - return HospitalListItemAdvancePayment( - hospitalModel: displayList[index], - isLocationEnabled: sortByLocation, - ).onPress(() { - refundVM.setSelectedHospital(displayList[index]); - Navigator.of(context).pop(); - }); - }, - ), - ), - ], - ); - } -} - 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/routes/app_routes.dart b/lib/routes/app_routes.dart index 7b34e447..9170e039 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -19,7 +19,9 @@ 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/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,8 +43,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 '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 '../services/error_handler_service.dart'; class AppRoutes { -- 2.30.2 From 198280fbdea120c758ee022e4e6031f31f3fe938 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Wed, 13 May 2026 15:59:33 +0300 Subject: [PATCH 3/6] Refund Request and Withdraw Module In Progress! --- assets/images/svg/bank_icon.svg | 7 + assets/images/svg/description_icon.svg | 7 + assets/images/svg/image_icon.svg | 1 + assets/images/svg/refund_icon.svg | 4 + assets/images/svg/select_image_icon.svg | 5 + assets/langs/ar-SA.json | 36 +- assets/langs/en-US.json | 38 +- lib/core/api/api_client.dart | 14 +- lib/core/api_consts.dart | 5 + lib/core/app_assets.dart | 26 +- lib/core/dependencies.dart | 2 +- lib/extensions/string_extensions.dart | 215 +++++- .../habib_wallet/habib_wallet_view_model.dart | 95 ++- .../get_invoices_list_response_model.dart | 84 +- .../submit_refund_request_model.dart | 98 +++ .../refund_request_list_response_model.dart | 91 +++ .../refundable_invoices_response_model.dart | 119 ++- .../refund_request/refund_request_repo.dart | 238 +++++- .../refund_request_view_model.dart | 346 ++++++++- lib/generated/locale_keys.g.dart | 27 + .../comprehensive_checkup_page.dart | 2 +- .../habib_wallet/habib_wallet_page.dart | 62 +- .../withdraw_request_create_page.dart | 325 ++++++++ .../hmg_services/services_page.dart | 3 +- lib/presentation/home/landing_page.dart | 19 +- .../medical_file/medical_file_page.dart | 29 +- .../widgets/invoice_list_card.dart | 101 ++- .../refund_request/refund_history_screen.dart | 258 +++++++ .../refund_request_create_page.dart | 721 ++++++++++++------ .../widgets/refund_reason_bottom_sheet.dart | 111 +++ lib/routes/app_routes.dart | 11 +- lib/widgets/buttons/custom_button.dart | 13 +- lib/widgets/chip/app_custom_chip_widget.dart | 58 +- lib/widgets/image_picker.dart | 14 +- lib/widgets/input_widget.dart | 112 +-- 35 files changed, 2794 insertions(+), 503 deletions(-) create mode 100644 assets/images/svg/bank_icon.svg create mode 100644 assets/images/svg/description_icon.svg create mode 100644 assets/images/svg/refund_icon.svg create mode 100644 assets/images/svg/select_image_icon.svg create mode 100644 lib/presentation/habib_wallet/withdraw_request_create_page.dart create mode 100644 lib/presentation/refund_request/refund_history_screen.dart create mode 100644 lib/presentation/refund_request/widgets/refund_reason_bottom_sheet.dart 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/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/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 6ba4a959..6a0dedf5 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1158,6 +1158,7 @@ "transportOptions": "خيارات النقل", "selectHospitalForAdvancePayment": "يرجى تحديد المستشفى أولاً.", "recharge": "شحن المحفظة", + "withdraw": "سحب", "activityLevel": "مستوى النشاط", "selectActivityLevel": "حدد مستوى النشاط", "almostInactive": "غير نشط تقريبًا (بدون تمارين)", @@ -1846,5 +1847,38 @@ "fileSizeExceedsLimit": "يجب ألا يتجاوز حجم الملف 1 ميغابايت", "completedPrescriptionOrder": "تمت الخدمة", "submitEReferral": "تقديم الطلب", - "requestRefund": "طلب استرداد" + "requestRefund": "طلب استرداد", + "uploadImage": "رفع صورة", + "selectImage": "اختر صورة", + "selectOptions": "اختر الخيارات", + "iban": "رقم الآيبان", + "addIbanDetails": "أضف تفاصيل الآيبان", + "description": "الوصف", + "enterDescription": "أدخل الوصف", + "submitRequest": "تقديم الطلب", + "ibanIsRequired": "حقل رقم الآيبان مطلوب", + "descriptionIsRequired": "الوصف مطلوب", + "invalidIbanFormat": "صيغة الآيبان غير صحيحة. يجب أن تبدأ بـ SA يليها 20 رقمًا (مثال: SA0380000000608010167519)", + "refundHistory": "سجل الاسترداد", + "noRefundHistoryFound": "لا يوجد سجل استرداد حتى الآن.", + "invoiceNo": "رقم الفاتورة", + "totalAmount": "المبلغ الإجمالي", + "patientShare": "نصيب المريض", + "selectProcedure": "حدد العمليات", + "noProceduresAvailableForRefund": "لا توجد عمليات متاحة للاسترداد", + "selectAll": "تحديد الكل", + "selectRefundMethod": "اختر طريقة الاسترداد", + "hmgWallet": "محفظة HMG", + "bankTransfer": "تحويل بنكي", + "selectRefundReason": "اختر سبب الاسترداد", + "refundReason": "سبب الاسترداد", + "done": "تم", + "refundReason1": "تم معالجة مريض الشركة كمدفوع ذاتي وإعادة فوترته", + "refundReason2": "المريض لا يريد إجراء سريرياً", + "refundReason3": "المريض لم ينتظر الاستشارة / يريد طبيباً آخر", + "refundReason4": "المريض أوقف الاشتراك في الباقة", + "refundReason5": "الخدمة لم تُنجز / غير متاحة بسبب عطل الجهاز / رفض المريض", + "refundReason6": "تم إلغاء الإجراء السريري أو تغييره من قِبل الطبيب المحيل", + "refundReason7": "تم إلغاء إجراء وحدة الخدمة / المختبر / الأشعة أو تغييره من قِبل الطبيب المحيل", + "selectProcedures": "اختر العمليات" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 312a8121..5d5fe9a8 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -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", @@ -1836,7 +1839,40 @@ "fileSizeExceedsLimit": "File size must not exceed 1 MB", "completedPrescriptionOrder": "Completed", "submitEReferral": "Submit", - "requestRefund": "Request Refund" + "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. Must start with SA followed by 20 digits (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" } diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 64074ff8..8b1e288f 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( @@ -208,8 +208,9 @@ class ApiClientImp implements ApiClient { body['TokenID'] = "@dm!n"; } - // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 1018977; + // TODO: REMOVE THIS MUST + body['TokenID'] = "@dm!n"; + body['PatientID'] = 4768571; // 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 15541878..0d615789 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -271,6 +271,11 @@ class ApiConsts { //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 getAiAnalysisURL = "https://api.labpredict.com"; static String getAiRadiologyAnalysis = "$getAiAnalysisURL/radiology"; diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 2612bde9..813ec01c 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -257,16 +257,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'; @@ -386,6 +385,11 @@ 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 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'; @@ -427,8 +431,6 @@ class AppAssets { static const String bmiFullBodyFemale = '$pngBasePath/female_bmi.png'; static const String defaultMEP = '$pngBasePath/default_MEP.png'; - - static const String ambulance_section = '$pngBasePath/ambulance_section.png'; static const String rrt_section = '$pngBasePath/rrt_section.png'; static const String er_checkin = '$pngBasePath/er_checkin.png'; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 1e65535a..c2fa02c1 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -209,7 +209,7 @@ class AppDependencies { ); getIt.registerLazySingleton( - () => HabibWalletViewModel(habibWalletRepo: getIt(), errorHandlerService: getIt()), + () => HabibWalletViewModel(habibWalletRepo: getIt(), errorHandlerService: getIt(), refundRequestRepo: getIt()), ); getIt.registerLazySingleton( 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 01dc2357..0353868e 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -3,10 +3,21 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_repo.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/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; @@ -14,6 +25,10 @@ class HabibWalletViewModel extends ChangeNotifier { bool isBottomSheetContentLoading = false; + // ── Refundable Advances ─────────────────────────────────────────────────── + bool isAdvancesLoading = false; + double totalAdvancesBalance = 0.0; + bool isSearchedFileNumberDataShown = false; int currentIndex = 0; @@ -22,9 +37,6 @@ class HabibWalletViewModel extends ChangeNotifier { HospitalsModel? selectedHospital; - HabibWalletRepo habibWalletRepo; - ErrorHandlerService errorHandlerService; - String fileNumber = ''; String depositorName = ''; String mobileNumber = ''; @@ -40,6 +52,7 @@ class HabibWalletViewModel extends ChangeNotifier { // Getters for field errors String? get amountError => _amountError; + String? get hospitalError => _hospitalError; String yahalaAccountNumber = ''; @@ -63,6 +76,50 @@ 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 { + // KSA IBAN format: SA followed by 2 check digits + 18 alphanumeric chars = 22 chars total + final ksaIbanRegex = RegExp(r'^SA\d{2}[0-9A-Z]{18}$', caseSensitive: false); + if (!ksaIbanRegex.hasMatch(iban.trim().replaceAll(' ', ''))) { + _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 @@ -98,7 +155,27 @@ class HabibWalletViewModel extends ChangeNotifier { return true; } - HabibWalletViewModel({required this.habibWalletRepo, required this.errorHandlerService}); + Future fetchAdvancesForHospital() async { + isAdvancesLoading = true; + totalAdvancesBalance = 0.0; + notifyListeners(); + + final result = await refundRequestRepo.getRefundableAdvances(); + result.fold( + (failure) { + isAdvancesLoading = false; + notifyListeners(); + errorHandlerService.handleError(failure: failure); + }, + (apiResponse) { + isAdvancesLoading = false; + if (apiResponse.data != null) { + totalAdvancesBalance = apiResponse.data!.refundableAdvancesItems.fold(0.0, (sum, item) => sum + (item.balanceAmount ?? 0.0)); + } + notifyListeners(); + }, + ); + } initHabibWalletProvider() { isWalletAmountLoading = true; @@ -218,7 +295,12 @@ 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), @@ -235,7 +317,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( 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/refund_request/models/req_models/submit_refund_request_model.dart b/lib/features/refund_request/models/req_models/submit_refund_request_model.dart index 8b137891..f1c5ed57 100644 --- 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 @@ -1 +1,99 @@ +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}'; + } +} + +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; + + 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, + }); + + Map toJson() => { + "ProjectID": projectId, + "Refund_RefundType": refundType, + "Refund_Project": refundProject, + "Refund_ProjectId": refundProjectId, + "Refund_PatientId": refundPatientId, + "PatientMobileNumber": patientMobileNumber, + "Refund_PatientName": refundPatientName, + "Refund_NationalId": refundNationalId, + "Refund_InvoiceNo": refundInvoiceNo, + "Refund_AppointmentNo": refundAppointmentNo, + "Refund_DoctorId": refundDoctorId, + "Refund_DoctorName": refundDoctorName, + "Refund_ClinicId": refundClinicId, + "Refund_ClinicName": refundClinicName, + "Refund_RefundDestination": refundDestination, + "Refund_IBAN": refundIban, + "Refund_Remarks": refundRemarks, + "Refund_TotalRefundAmount": refundTotalAmount, + "Refund_ProceduresList": refundProceduresList.map((p) => p.toJson()).toList(), + }; + + @override + String toString() { + return 'SubmitRefundRequestModel{projectId: $projectId, refundType: $refundType, refundProject: $refundProject, refundProjectId: $refundProjectId, refundPatientId: $refundPatientId, patientMobileNumber: $patientMobileNumber, refundPatientName: $refundPatientName, refundNationalId: $refundNationalId, refundInvoiceNo: $refundInvoiceNo, refundAppointmentNo: $refundAppointmentNo, refundDoctorId: $refundDoctorId, refundDoctorName: $refundDoctorName, refundClinicId: $refundClinicId, refundClinicName: $refundClinicName, refundDestination: $refundDestination, refundIban: $refundIban, refundRemarks: $refundRemarks, refundTotalAmount: $refundTotalAmount, refundProceduresList: $refundProceduresList}'; + } +} 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 index 8b137891..d8b6ffc9 100644 --- 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 @@ -1 +1,92 @@ +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'] as String?, + refundType: json['refundType'] as String?, + patientId: json['patientId']?.toString(), + refNo: 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 RefundInvoiceModel { + final String? invoiceNumber; + final String? invoiceDate; + final double? totalAmount; + final double? patientShare; + final String? clinicName; + final String? clinicNameN; + final int? projectId; + final String? projectName; + final String? projectNameN; + + RefundInvoiceModel({ + this.invoiceNumber, + this.invoiceDate, + this.totalAmount, + this.patientShare, + this.clinicName, + this.clinicNameN, + this.projectId, + this.projectName, + this.projectNameN, + }); + + factory RefundInvoiceModel.fromJson(Map json) { + return RefundInvoiceModel( + invoiceNumber: json['InvoiceNumber'] as String?, + invoiceDate: json['InvoiceDate'] as String?, + totalAmount: (json['TotalAmount'] as num?)?.toDouble(), + patientShare: (json['PatientShare'] as num?)?.toDouble(), + clinicName: json['ClinicName'] as String?, + clinicNameN: json['ClinicNameN'] as String?, + projectId: json['ProjectID'] as int?, + projectName: json['ProjectName'] as String?, + projectNameN: json['ProjectNameN'] as String?, + ); + } +} 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 index 1df3e76b..2409f3ec 100644 --- 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 @@ -18,9 +18,7 @@ class 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() + ? (json['RefundableInvoiceItems'] as List).map((item) => RefundableInvoiceItem.fromJson(item as Map)).toList() : null, statusCode: json['StatusCode'] as int?, ); @@ -52,8 +50,7 @@ class RefundableInvoiceItem { this.projectID, }); - factory RefundableInvoiceItem.fromRawJson(String str) => - RefundableInvoiceItem.fromJson(json.decode(str)); + factory RefundableInvoiceItem.fromRawJson(String str) => RefundableInvoiceItem.fromJson(json.decode(str)); String toRawJson() => json.encode(toJson()); @@ -88,3 +85,115 @@ class RefundableInvoiceItem { } } +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 index f8e603b6..03797e54 100644 --- a/lib/features/refund_request/refund_request_repo.dart +++ b/lib/features/refund_request/refund_request_repo.dart @@ -3,11 +3,26 @@ 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/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 String patientId, String invoiceNumberQuery}); + + Future>> submitRefund({required SubmitRefundRequestModel request}); } class RefundRequestRepoImp implements RefundRequestRepo { @@ -18,11 +33,7 @@ class RefundRequestRepoImp implements RefundRequestRepo { @override Future>>> getRefundableInvoices({required int projectID}) async { - // ApiClient auto-injects: PatientID, TokenID, LanguageID, PatientOutSA, - // PatientTypeID, IPAdress, generalid, VersionID, Channel - Map body = { - "ProjectID": projectID, - }; + Map body = {"ProjectID": projectID}; try { GenericApiModel>? apiResponse; @@ -69,4 +80,221 @@ class RefundRequestRepoImp implements RefundRequestRepo { 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 String patientId, + String invoiceNumberQuery = "", + }) async { + final Map body = { + "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['RefundInvoicesList']; + 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())); + } + } } diff --git a/lib/features/refund_request/refund_request_view_model.dart b/lib/features/refund_request/refund_request_view_model.dart index 030b2a70..dcd954b6 100644 --- a/lib/features/refund_request/refund_request_view_model.dart +++ b/lib/features/refund_request/refund_request_view_model.dart @@ -1,12 +1,27 @@ +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/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'; 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; @@ -14,6 +29,11 @@ class RefundableProcedure { final String? procedureNameN; final double patientShare; 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, @@ -21,6 +41,9 @@ class RefundableProcedure { this.procedureNameN, required this.patientShare, this.isSelected = false, + this.refundReason, + this.apiLineItemNo, + this.apiProcedureId, }); } @@ -80,6 +103,22 @@ class RefundRequestViewModel extends ChangeNotifier { } } + 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; @@ -176,8 +215,26 @@ class RefundRequestViewModel extends ChangeNotifier { 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; @@ -187,8 +244,295 @@ class RefundRequestViewModel extends ChangeNotifier { 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 { + final ksaIbanRegex = RegExp(r'^SA\d{2}[0-9A-Z]{20}$', caseSensitive: false); + if (!ksaIbanRegex.hasMatch(iban.trim().replaceAll(' ', ''))) { + _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; + 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({ + String? patientId, + String invoiceNumberQuery = "", + Function(List)? onSuccess, + Function(String)? onError, + }) async { + isRefundInvoicesLoading = true; + refundInvoicesList.clear(); + notifyListeners(); + + // final resolvedPatientId = patientId ?? appState.getAuthenticatedUser()?.patientId?.toString() ?? ""; + final resolvedPatientId = 4768571; + final result = await refundRequestRepo.getRefundInvoices( + patientId: resolvedPatientId.toString(), + 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 submitRefund({ + 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"); + } + } + }, + ); + } } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 84cff5ad..cf221cff 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -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'; @@ -1839,4 +1840,30 @@ abstract class LocaleKeys { 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'; } 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 c21e7a62..56cbec35 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'; @@ -41,7 +42,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( @@ -56,11 +57,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( @@ -73,8 +73,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, @@ -85,13 +86,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); }), ], @@ -127,6 +123,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), @@ -146,9 +159,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/withdraw_request_create_page.dart b/lib/presentation/habib_wallet/withdraw_request_create_page.dart new file mode 100644 index 00000000..11f422e4 --- /dev/null +++ b/lib/presentation/habib_wallet/withdraw_request_create_page.dart @@ -0,0 +1,325 @@ +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/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() { + _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) { + // TODO: Submit withdraw request + } + }, + ).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, _) { + return Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Column( + children: [ + TextInputWidget( + labelText: LocaleKeys.addIbanDetails.tr(), + hintText: "SA0000000000000000000000", + controller: _bankDetailsController, + focusNode: _ibanFocusNode, + leadingIconNeedsContainer: false, + 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: viewModel.ibanError != null, + 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: viewModel.withdrawDescriptionError != null, + 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) {}, + ); + }), + ); + } + +// ── 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())), + 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(), + ], + ), + ); + }, + ); + } +} diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index a4a9ee10..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'; @@ -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( diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 5ea5b96b..ae270353 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -147,11 +147,14 @@ class _LandingPageState extends State { notificationsViewModel.initNotificationsViewModel(); insuranceViewModel.initInsuranceProvider(); - if(!appState.isRatedVisible) { - appointmentRatingViewModel.getLastRatingAppointment(onSuccess: (response) { - if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) { - appointmentRatingViewModel.getAppointmentDetails(appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!, appointmentRatingViewModel.appointmentRatedList.last.projectID!, - onSuccess: ((response) { + if (!appState.isRatedVisible) { + appointmentRatingViewModel.getLastRatingAppointment( + onSuccess: (response) { + if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) { + appointmentRatingViewModel.getAppointmentDetails( + appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!, + appointmentRatingViewModel.appointmentRatedList.last.projectID!, + onSuccess: ((response) { appointmentRatingViewModel.setClinicOrDoctor(false); appointmentRatingViewModel.setTitle(LocaleKeys.rateDoctor.tr(context: context)); appointmentRatingViewModel.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr(context: context)); @@ -452,7 +455,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) { @@ -840,7 +845,7 @@ class _LandingPageState extends State { children: [ LocaleKeys.myBalanceSubtitle.tr(context: context).toText16(isBold: true).paddingSymmetrical(24.h, 0.h), SizedBox( - height: 170.h, + height: 175.h, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: 2, diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 560768b5..8f2ce569 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -59,7 +59,6 @@ import 'package:hmg_patient_app_new/presentation/my_invoices/my_invoices_list.da import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart'; import 'package:hmg_patient_app_new/presentation/radiology/radiology_orders_page.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/todo_page.dart'; -import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_create_page.dart'; import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_page.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; @@ -662,20 +661,20 @@ class _MedicalFilePageState extends State { ).onPress(() { 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.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/widgets/invoice_list_card.dart b/lib/presentation/my_invoices/widgets/invoice_list_card.dart index 465c1c0b..2cafdc57 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'; @@ -10,11 +12,11 @@ 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/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/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/routes/custom_page_route.dart'; class InvoiceListCard extends StatelessWidget { final GetInvoicesListResponseModel getInvoicesListResponseModel; @@ -26,11 +28,7 @@ class InvoiceListCard extends StatelessWidget { 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 +47,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 +96,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 +130,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 +142,61 @@ 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: () { + Navigator.of(context).push( + CustomPageRoute( + page: RefundRequestCreatePage(initialTabIndex: 0, invoice: getInvoicesListResponseModel), + ), + ); + }, + 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..04f369d0 --- /dev/null +++ b/lib/presentation/refund_request/refund_history_screen.dart @@ -0,0 +1,258 @@ +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_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/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/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'; +import 'package:provider/provider.dart'; + +class RefundHistoryScreen extends StatefulWidget { + const RefundHistoryScreen({super.key}); + + @override + State createState() => _RefundHistoryScreenState(); +} + +class _RefundHistoryScreenState extends State { + late AppState _appState; + + @override + void initState() { + super.initState(); + _appState = getIt(); + final vm = context.read(); + scheduleMicrotask(() async { + await vm.getRefundInvoices(); + }); + } + + // ── Helpers ─────────────────────────────────���───────────────────────────── + + String _formatDate(String? rawDate) { + if (rawDate == null || rawDate.isEmpty) return ''; + try { + // Handle WCF /Date(...)/ format + final match = RegExp(r'/Date\((-?\d+)').firstMatch(rawDate); + if (match != null) { + final ms = int.parse(match.group(1)!); + return DateFormat('MMM dd, yyyy').format(DateTime.fromMillisecondsSinceEpoch(ms)); + } + return DateFormat('MMM dd, yyyy').format(DateTime.parse(rawDate)); + } 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 ────────────────────────────────────────────────────────────────── + + Widget _buildInvoiceCard(RefundInvoiceModel? invoice, {bool isLoading = false}) { + final isArabic = _appState.isArabic(); + + final clinicName = isLoading ? 'Clinic Name' : (isArabic ? (invoice?.clinicNameN ?? invoice?.clinicName) : invoice?.clinicName) ?? '-'; + + final projectName = isLoading ? 'Hospital Name' : (isArabic ? (invoice?.projectNameN ?? invoice?.projectName) : invoice?.projectName) ?? '-'; + + final invoiceNo = isLoading ? 'INV-000000' : (invoice?.invoiceNumber ?? '-'); + final invoiceDate = isLoading ? 'Jan 01, 2024' : _formatDate(invoice?.invoiceDate); + final totalAmount = isLoading ? '0.00' : (invoice?.totalAmount?.toStringAsFixed(2) ?? '-'); + final patientShare = isLoading ? '0.00' : (invoice?.patientShare?.toStringAsFixed(2) ?? '-'); + + 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 + Date row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (!isLoading) + LocaleKeys.invoiceNo.tr(context: context).toText12( + color: AppColors.textColorLight, + isBold: true, + ), + if (!isLoading) SizedBox(width: 4.w), + invoiceNo.toText14(isBold: true, isEnglishOnly: true).toShimmer2(isShow: isLoading), + ], + ), + invoiceDate.toText12(color: AppColors.textColorLight, isBold: true).toShimmer2(isShow: isLoading), + ], + ), + + SizedBox(height: 12.h), + + // Chips — clinic & hospital + Wrap( + spacing: 6.w, + runSpacing: 6.h, + children: [ + AppCustomChipWidget( + icon: "", + labelText: clinicName, + ).toShimmer2(isShow: isLoading), + AppCustomChipWidget( + icon: "", + labelText: projectName, + ).toShimmer2(isShow: isLoading), + ], + ), + + SizedBox(height: 12.h), + + // Amounts row + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.totalAmount + .tr(context: context) + .toText12(color: AppColors.textColorLight, isBold: true) + .toShimmer2(isShow: isLoading), + SizedBox(height: 4.h), + Utils.getPaymentAmountWithSymbol( + totalAmount.toText16(isBold: true, isEnglishOnly: true), + AppColors.blackColor, + 11, + isSaudiCurrency: true, + isExpanded: false, + ).toShimmer2(isShow: isLoading), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.patientShare + .tr(context: context) + .toText12(color: AppColors.textColorLight, isBold: true) + .toShimmer2(isShow: isLoading), + SizedBox(height: 4.h), + Utils.getPaymentAmountWithSymbol( + patientShare.toText16(isBold: true, isEnglishOnly: true), + AppColors.primaryRedColor, + 11, + isSaudiCurrency: true, + isExpanded: false, + ).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 index 76ae7520..284d4581 100644 --- a/lib/presentation/refund_request/refund_request_create_page.dart +++ b/lib/presentation/refund_request/refund_request_create_page.dart @@ -9,50 +9,95 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_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/habib_wallet/widgets/select_hospital_bottom_sheet.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/custom_tab_bar.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, this.initialTabIndex = 0}); + const RefundRequestCreatePage({ + super.key, + required this.invoice, + this.initialTabIndex = 0, + }); @override State createState() => _RefundRequestCreatePageState(); } class _RefundRequestCreatePageState extends State { - late RefundRequestViewModel _vm; - late HabibWalletViewModel _habibWalletVM; + late RefundRequestViewModel _refundRequestViewModel; + late HabibWalletViewModel _habibWalletViewModel; late AppState _appState; - // TODO: Replace with actual procedure list fetched from API per invoice - List _mockProceduresForInvoice(int invoiceNo) => [ - RefundableProcedure(procedureId: 101, procedureName: 'Consultation', procedureNameN: 'استشارة', patientShare: 120.0), - RefundableProcedure(procedureId: 102, procedureName: 'Blood Test – CBC', procedureNameN: 'تحليل دم شامل', patientShare: 85.50), - RefundableProcedure(procedureId: 103, procedureName: 'X-Ray Chest', procedureNameN: 'أشعة سينية صدر', patientShare: 200.0), - RefundableProcedure(procedureId: 104, procedureName: 'ECG', procedureNameN: 'تخطيط قلب', patientShare: 150.0), - ]; + 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((_) { - // Load hospital list the same way RechargeWalletPage does - _habibWalletVM.getProjectsList(); + _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 ?? '', + patientShare: item.patientPaidAmount ?? 0.0, + apiLineItemNo: item.lineItemNo, + apiProcedureId: item.procedureId, + )) + .toList(); + _refundRequestViewModel.setProcedures(procedures); + }, + ); + } + // ── Stepper ─────────────────────────────────────────────────────────────── Widget _buildStepper(RefundRequestViewModel vm) { @@ -70,7 +115,7 @@ class _RefundRequestCreatePageState extends State { itemWidth, isActive ? AppColors.primaryRedColor : AppColors.greyColor, isThumb, - 4.h, + 6.h, ); }), ); @@ -82,63 +127,211 @@ class _RefundRequestCreatePageState extends State { // ── Step 1 — exactly mirrors RechargeWalletPage hospital row ───────────── Widget _buildStep1Content(RefundRequestViewModel vm) { - final isArabic = _appState.isArabic(); return Consumer( builder: (context, refundVM, _) { - return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), - child: Padding( - padding: EdgeInsets.all(16.h), - child: 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), - (refundVM.selectedHospital != null - ? ((isArabic - ? (refundVM.selectedHospital!.nameN?.toString() ?? refundVM.selectedHospital!.name) - : refundVM.selectedHospital!.name) ?? - LocaleKeys.selectHospital.tr(context: context)) - : LocaleKeys.selectHospital.tr(context: context)) - .toText14(color: AppColors.textColor, isBold: true), - ]), - ], + 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), ), - Utils.buildSvgWithAssets( - icon: AppAssets.arrow_down, - width: 25.h, - height: 25.h, + ) + 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), + ), + ], + ), ), - ], - ).onPress(() { - showCommonBottomSheetWithoutHeight( - context, - title: LocaleKeys.selectHospital.tr(context: context), - isDismissible: false, - child: SelectHospitalBottomSheet( - onHospitalSelected: (hospital) => _vm.setSelectedHospital(hospital), + ), + ], + ); + }, + ); + } + + // ── 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); + }, ), - callBackFunc: () {}, + 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) { + return Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + ), + child: Column( + children: [ + TextInputWidget( + labelText: LocaleKeys.addIbanDetails.tr(), + hintText: "SA0000000000000000000000", + controller: _ibanController, + focusNode: _ibanFocusNode, + leadingIconNeedsContainer: false, + 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: vm.ibanError != null, + 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, + leadingIconNeedsContainer: false, + isEnable: true, + isReadOnly: false, + prefix: null, + isBorderAllowed: false, + isAllowLeadingIcon: true, + fontSize: 14.f, + isCountryDropDown: false, + leadingIcon: AppAssets.descriptionIcon, + fontFamily: "Poppins", + hasError: vm.descriptionError != null, + errorMessage: vm.descriptionError, + onChange: (value) { + vm.clearDescriptionError(); + }, + ), + ], + ), ); } // ── Step 2 — procedure checkboxes ───────────────────────────────────────── - Widget _buildProcedureCard(RefundableProcedure procedure, int index) { + Widget _buildProcedureCard(RefundableProcedure procedure, int index, int length) { final isArabic = _appState.isArabic(); final name = isArabic && procedure.procedureNameN != null ? procedure.procedureNameN! : procedure.procedureName; @@ -151,46 +344,87 @@ class _RefundRequestCreatePageState extends State { child: AnimatedContainer( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, - margin: EdgeInsets.only(bottom: 12.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: procedure.isSelected ? AppColors.primaryRedColor.withAlpha(12) : AppColors.whiteColor, - borderRadius: 16.h, - hasShadow: true, - ), child: Material( color: Colors.transparent, child: InkWell( - onTap: () => _vm.toggleProcedureSelection(procedure), - borderRadius: BorderRadius.circular(16.h), + onTap: () => _refundRequestViewModel.toggleProcedureSelection(procedure), child: Padding( - padding: EdgeInsets.all(14.h), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Checkbox( - value: procedure.isSelected, - onChanged: (_) => _vm.toggleProcedureSelection(procedure), - activeColor: AppColors.primaryRedColor, - side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1.5), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: VisualDensity.compact, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4.r), - ), - ), - SizedBox(width: 8.w), - Expanded(child: name.toText14(isBold: true)), - SizedBox(width: 8.w), - Utils.getPaymentAmountWithSymbol( - procedure.patientShare.toStringAsFixed(2).toText14( - isBold: true, - isEnglishOnly: true, - color: AppColors.primaryRedColor, + 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), + ), + ), ), - AppColors.textColorLight, - 13, - isSaudiCurrency: true, + ), + SizedBox(width: 8.w), + Expanded(child: name.toText16(isBold: true)), + SizedBox(width: 8.w), + Utils.getPaymentAmountWithSymbol( + procedure.patientShare.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: 12.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + 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: procedure.refundReason != null ? AppColors.textColor : AppColors.greyTextColor, 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), + ] ], ), ), @@ -202,69 +436,6 @@ class _RefundRequestCreatePageState extends State { ); } - Widget _buildStep2Content(RefundRequestViewModel vm) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w), - child: Text( - 'Select Procedures', - style: TextStyle( - fontSize: 20.f, - fontWeight: FontWeight.w600, - color: AppColors.blackColor, - letterSpacing: -0.5, - ), - ), - ), - SizedBox(height: 24.h), - if (vm.isProceduresLoading) - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: EdgeInsets.symmetric(horizontal: 24.w), - itemCount: 4, - itemBuilder: (_, index) => Container( - height: 56.h, - margin: EdgeInsets.only(bottom: 12.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: 'No procedures available for refund'.toText14(color: AppColors.greyTextColor), - ), - ) - else - AnimationLimiter( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w), - child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.bgScaffoldColor, - borderRadius: 24.h, - hasShadow: false, - ), - padding: EdgeInsets.all(12.w), - child: Column( - children: List.generate( - vm.procedures.length, - (index) => _buildProcedureCard(vm.procedures[index], index), - ), - ), - ), - ), - ), - ], - ); - } - // ── Bottom bar ──────────────────────────────────────────────────────────── Widget _buildBottomBar(RefundRequestViewModel vm) { @@ -280,21 +451,18 @@ class _RefundRequestCreatePageState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (isStep2 && !isWallet && vm.selectedProcedures.isNotEmpty) ...[ + if (!isWallet && vm.selectedProcedures.isNotEmpty) ...[ Padding( padding: EdgeInsets.fromLTRB(24.w, 16.h, 24.w, 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - 'Total Refundable Amount'.toText12( - color: AppColors.textColorLight, - isBold: true, - ), + LocaleKeys.totalAmount.toText18(color: AppColors.textColor, isBold: true), Utils.getPaymentAmountWithSymbol( - vm.totalRefundableAmount.toStringAsFixed(2).toText18( + vm.totalRefundableAmount.toStringAsFixed(2).toText24( isBold: true, isEnglishOnly: true, - color: AppColors.primaryRedColor, + color: AppColors.textColor, ), AppColors.textColorLight, 14, @@ -305,15 +473,28 @@ class _RefundRequestCreatePageState extends State { ), ], CustomButton( + icon: isStep2 ? AppAssets.refundIcon : null, text: isStep2 ? LocaleKeys.submit.tr(context: context) : LocaleKeys.next.tr(context: context), onPressed: () { if (!isStep2) { - if (isWallet || vm.isStep1Valid) vm.nextStep(); + if (vm.selectedProcedures.isNotEmpty) vm.nextStep(); } else { - // TODO: Submit API call + final isBankTransfer = vm.selectedPaymentMethod == RefundTabType.invoices; + if (isBankTransfer) { + final isValid = vm.validateBankTransferForm( + iban: _ibanController.text, + description: _descriptionController.text, + ); + if (!isValid) return; + } + _handleInvoiceRefundRequestSubmit(vm); } }, - isDisabled: (!isWallet && !isStep2 && !vm.isStep1Valid) || (isStep2 && !isWallet && !vm.isStep2Valid), + isDisabled: isStep2 + ? (vm.selectedPaymentMethod == null || + (vm.selectedPaymentMethod == RefundTabType.invoices && + (_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty))) + : (vm.selectedProcedures.isEmpty || !vm.allSelectedProceduresHaveReasons), ).paddingSymmetrical(24.h, 24.h), ], ), @@ -322,46 +503,145 @@ class _RefundRequestCreatePageState extends State { // ── HMG Wallet placeholder ───────────────────────────────────────────────── - Widget _buildHmgWalletContent() { - return KeyedSubtree( - key: const ValueKey('hmgWallet'), + 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, + + // TODO: It should be removed + refundPatientId: 4768571.toString(), + refundPatientName: patientName, + refundNationalId: patientNationalId, + patientMobileNumber: patientMobileNumber, + refundInvoiceNo: 260000075.toString(), + // refundInvoiceNo: (invoice.invoiceNo ?? 0).toString(), TODO: It should be removed + 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.patientShare, + refundReason: p.refundReason ?? '', + )) + .toList(), + ); + + await vm.submitRefund( + 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.symmetric(horizontal: 24.w), - child: Container( - width: double.infinity, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: true, - ), - padding: EdgeInsets.all(24.w), - child: Column( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.wallet, - iconColor: AppColors.primaryRedColor, - width: 48.h, - height: 48.h, + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + Utils.getSuccessWidget(loadingText: LocaleKeys.requestSubmittedSuccessfully.tr(context: context)), + if (refNo.isNotEmpty) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LocaleKeys.hereIsYourRequestNumber.tr(context: context).toText14( + color: AppColors.textColorLight, + weight: FontWeight.w600, + ), + SizedBox(width: 4.w), + refNo.toText16(isBold: true), + ], ), - SizedBox(height: 16.h), - 'HMG Wallet Refund'.toText16(isBold: true), - SizedBox(height: 8.h), - 'Refund requests to HMG Wallet will be processed within 1-3 business days.'.toText14(color: AppColors.greyTextColor, isCenter: true), ], - ), + 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 + }, + textColor: AppColors.whiteColor, + ), + ), + ], + ), + ], ), ), + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false, + isAutoDismiss: false, ); } - // ── Build ───────────────────────────────────────────────────────────────── +// ── Build ───────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { - _habibWalletVM = context.read(); + _habibWalletViewModel = context.read(); return Consumer( builder: (context, vm, _) { - _vm = vm; return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: Column( @@ -369,38 +649,18 @@ class _RefundRequestCreatePageState extends State { Expanded( child: CollapsingListView( title: LocaleKeys.requestRefund.tr(context: context), - history: () async { - // TODO: Navigate to refund history page - }, + requests: () => Navigator.of(context).push(CustomPageRoute(page: RefundHistoryScreen())), child: SingleChildScrollView( padding: EdgeInsets.only(bottom: 32.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Tab bar - Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w), - child: CustomTabBar( - initialIndex: widget.initialTabIndex, - activeTextColor: AppColors.primaryRedColor, - activeBackgroundColor: AppColors.primaryRedColor.withAlpha(25), - tabs: [ - CustomTabBarModel(null, 'Invoices'), - CustomTabBarModel(null, 'HMG Wallet'), - ], - onTabChange: (index) { - vm.setTab(index == 0 ? RefundTabType.invoices : RefundTabType.hmgWallet); - vm.goToStep(0); - }, - ), - ), - SizedBox(height: 24.h), // Stepper (invoices tab only) if (vm.selectedTab == RefundTabType.invoices) ...[ + buildInvoiceCard(), _buildStepper(vm), - SizedBox(height: 24.h), + SizedBox(height: 16.h), ], - // Content AnimatedSwitcher( duration: const Duration(milliseconds: 350), @@ -414,20 +674,21 @@ class _RefundRequestCreatePageState extends State { child: child, ), ), - child: vm.selectedTab == RefundTabType.hmgWallet - ? _buildHmgWalletContent() - : 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: _buildStep2Content(vm), - ), + 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), + ), + ), ), ], ), 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/routes/app_routes.dart b/lib/routes/app_routes.dart index 9170e039..a82879f6 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'; @@ -104,6 +106,8 @@ class AppRoutes { // Refund Request static const String refundRequestCreatePage = '/refundRequestCreatePage'; + static const String createWithdrawRequest = '/createWithdrawRequest'; + static const String refundHistoryScreen = '/refundHistoryScreen'; static Map get routes => { initialRoute: (context) => SplashPage(), @@ -166,8 +170,11 @@ class AppRoutes { child: const ParkingPage(), ), refundRequestCreatePage: (context) { - final args = ModalRoute.of(context)?.settings.arguments as int?; - return RefundRequestCreatePage(initialTabIndex: args ?? 0); + final args = ModalRoute.of(context)?.settings.arguments as GetInvoicesListResponseModel; + return RefundRequestCreatePage(invoice: args); + }, + createWithdrawRequest: (context) { + return WithdrawRequestCreatePage(); }, }; } 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..d5d96229 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; @@ -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 3fa7d061..68d65bde 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 { @@ -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..7502404c 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 == true) { + 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, -- 2.30.2 From 67bb3895e206551610901ffa287ccef542d30f6c Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Sun, 17 May 2026 09:16:33 +0300 Subject: [PATCH 4/6] Refund Module almost done --- lib/core/api/api_client.dart | 4 +- lib/core/utils/validation_utils.dart | 29 ++--- .../habib_wallet/habib_wallet_view_model.dart | 100 +++++++++++++++++- .../submit_refund_request_model.dart | 87 +++++++++++---- .../refund_request_view_model.dart | 4 +- .../withdraw_request_create_page.dart | 86 ++++++++++++++- 6 files changed, 266 insertions(+), 44 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 8b1e288f..7b104ab3 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -209,8 +209,8 @@ class ApiClientImp implements ApiClient { } // TODO: REMOVE THIS MUST - body['TokenID'] = "@dm!n"; - body['PatientID'] = 4768571; + // body['TokenID'] = "@dm!n"; + // body['PatientID'] = 4768571; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; // body['SessionID'] = "45786230487560q"; diff --git a/lib/core/utils/validation_utils.dart b/lib/core/utils/validation_utils.dart index b5651648..b5ba4bae 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); @@ -182,7 +181,7 @@ class ValidationUtils { } } - if (phoneNumber == null || phoneNumber.isEmpty) { + if (phoneNumber == null || phoneNumber.isEmpty) { _dialogService.showExceptionBottomSheet(message: LocaleKeys.enterValidPhoneNumber.tr(), onOkPressed: onOkPress); return false; } @@ -190,9 +189,13 @@ class ValidationUtils { return isCorrectID; } - static bool isNullOrEmpty(String? value) { - return value == null || value - .trim() - .isEmpty; + static bool isNullOrEmpty(String? value) { + return value == null || value.trim().isEmpty; } -} \ No newline at end of file + + /// Validates a KSA IBAN: must start with SA + 2 check digits + 20 alphanumeric chars (24 chars total). + static bool validateKsaIban(String iban) { + final ksaIbanRegex = RegExp(r'^SA\d{2}[0-9A-Z]{20}$', caseSensitive: false); + return ksaIbanRegex.hasMatch(iban.trim().replaceAll(' ', '')); + } +} diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart index 0353868e..086b0ac2 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -1,8 +1,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.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/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/refundable_invoices_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/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'; @@ -28,6 +32,8 @@ class HabibWalletViewModel extends ChangeNotifier { // ── Refundable Advances ─────────────────────────────────────────────────── bool isAdvancesLoading = false; double totalAdvancesBalance = 0.0; + List refundableAdvancesList = []; + bool isSubmittingWithdrawal = false; bool isSearchedFileNumberDataShown = false; @@ -103,9 +109,7 @@ class HabibWalletViewModel extends ChangeNotifier { _ibanError = LocaleKeys.ibanIsRequired.tr(); isValid = false; } else { - // KSA IBAN format: SA followed by 2 check digits + 18 alphanumeric chars = 22 chars total - final ksaIbanRegex = RegExp(r'^SA\d{2}[0-9A-Z]{18}$', caseSensitive: false); - if (!ksaIbanRegex.hasMatch(iban.trim().replaceAll(' ', ''))) { + if (!ValidationUtils.validateKsaIban(iban.trim())) { _ibanError = LocaleKeys.invalidIbanFormat.tr(); isValid = false; } @@ -158,6 +162,7 @@ class HabibWalletViewModel extends ChangeNotifier { Future fetchAdvancesForHospital() async { isAdvancesLoading = true; totalAdvancesBalance = 0.0; + refundableAdvancesList = []; notifyListeners(); final result = await refundRequestRepo.getRefundableAdvances(); @@ -170,13 +175,86 @@ class HabibWalletViewModel extends ChangeNotifier { (apiResponse) { isAdvancesLoading = false; if (apiResponse.data != null) { - totalAdvancesBalance = apiResponse.data!.refundableAdvancesItems.fold(0.0, (sum, item) => sum + (item.balanceAmount ?? 0.0)); + refundableAdvancesList = apiResponse.data!.refundableAdvancesItems; + totalAdvancesBalance = refundableAdvancesList.fold(0.0, (sum, item) => sum + (item.balanceAmount ?? 0.0)); } notifyListeners(); }, ); } + Future submitWalletWithdrawal({ + 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 ?? '', + )) + .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', // 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; isBottomSheetContentLoading = false; @@ -203,6 +281,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(); 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 index f1c5ed57..3a8b104c 100644 --- 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 @@ -27,6 +27,33 @@ class SubmitRefundProcedureItem { } } +/// 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; @@ -47,6 +74,8 @@ class SubmitRefundRequestModel { 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, @@ -68,32 +97,46 @@ class SubmitRefundRequestModel { required this.refundRemarks, required this.refundTotalAmount, required this.refundProceduresList, + this.refundAdvancesList = const [], }); - Map toJson() => { - "ProjectID": projectId, - "Refund_RefundType": refundType, - "Refund_Project": refundProject, - "Refund_ProjectId": refundProjectId, - "Refund_PatientId": refundPatientId, - "PatientMobileNumber": patientMobileNumber, - "Refund_PatientName": refundPatientName, - "Refund_NationalId": refundNationalId, - "Refund_InvoiceNo": refundInvoiceNo, - "Refund_AppointmentNo": refundAppointmentNo, - "Refund_DoctorId": refundDoctorId, - "Refund_DoctorName": refundDoctorName, - "Refund_ClinicId": refundClinicId, - "Refund_ClinicName": refundClinicName, - "Refund_RefundDestination": refundDestination, - "Refund_IBAN": refundIban, - "Refund_Remarks": refundRemarks, - "Refund_TotalRefundAmount": refundTotalAmount, - "Refund_ProceduresList": refundProceduresList.map((p) => p.toJson()).toList(), - }; + /// 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, refundProject: $refundProject, refundProjectId: $refundProjectId, refundPatientId: $refundPatientId, patientMobileNumber: $patientMobileNumber, refundPatientName: $refundPatientName, refundNationalId: $refundNationalId, refundInvoiceNo: $refundInvoiceNo, refundAppointmentNo: $refundAppointmentNo, refundDoctorId: $refundDoctorId, refundDoctorName: $refundDoctorName, refundClinicId: $refundClinicId, refundClinicName: $refundClinicName, refundDestination: $refundDestination, refundIban: $refundIban, refundRemarks: $refundRemarks, refundTotalAmount: $refundTotalAmount, refundProceduresList: $refundProceduresList}'; + return 'SubmitRefundRequestModel{projectId: $projectId, refundType: $refundType, refundAdvancesList: $refundAdvancesList, refundProceduresList: $refundProceduresList}'; } } diff --git a/lib/features/refund_request/refund_request_view_model.dart b/lib/features/refund_request/refund_request_view_model.dart index dcd954b6..51212741 100644 --- a/lib/features/refund_request/refund_request_view_model.dart +++ b/lib/features/refund_request/refund_request_view_model.dart @@ -1,6 +1,7 @@ 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/refund_request_list_response_model.dart'; @@ -275,8 +276,7 @@ class RefundRequestViewModel extends ChangeNotifier { _ibanError = LocaleKeys.ibanIsRequired.tr(); isValid = false; } else { - final ksaIbanRegex = RegExp(r'^SA\d{2}[0-9A-Z]{20}$', caseSensitive: false); - if (!ksaIbanRegex.hasMatch(iban.trim().replaceAll(' ', ''))) { + if (!ValidationUtils.validateKsaIban(iban.trim())) { _ibanError = LocaleKeys.invalidIbanFormat.tr(); isValid = false; } diff --git a/lib/presentation/habib_wallet/withdraw_request_create_page.dart b/lib/presentation/habib_wallet/withdraw_request_create_page.dart index 11f422e4..b069b441 100644 --- a/lib/presentation/habib_wallet/withdraw_request_create_page.dart +++ b/lib/presentation/habib_wallet/withdraw_request_create_page.dart @@ -19,6 +19,7 @@ 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'; @@ -50,6 +51,7 @@ class _WithdrawRequestCreatePageState extends State { @override void dispose() { + _habibWalletViewModel.resetWithdrawForm(silent: true); _bankDetailsController.dispose(); _descriptionController.dispose(); _ibanFocusNode.dispose(); @@ -93,7 +95,7 @@ class _WithdrawRequestCreatePageState extends State { description: _descriptionController.text, ); if (isFormValid) { - // TODO: Submit withdraw request + _handleWalletWithdrawalSubmit(vm); } }, ).paddingSymmetrical(24.h, 24.h), @@ -284,6 +286,88 @@ class _WithdrawRequestCreatePageState extends State { ); } +// ── 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.submitWalletWithdrawal( + 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)), + SizedBox(height: 24.h), + Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: LocaleKeys.ok.tr(context: context), + onPressed: () { + Navigator.pop(context); + Navigator.pop(context); + }, + textColor: AppColors.whiteColor, + ), + ), + ], + ), + ], + ), + ), + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false, + callBackFunc: () {}, + ); + } + // ── Build ───────────────────────────────────────────────────────────────── @override -- 2.30.2 From 897bd308796d52f8104afd8c33fab4f7076aa2da Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Tue, 19 May 2026 17:49:26 +0300 Subject: [PATCH 5/6] Refund Module Completed --- assets/images/svg/bank_icon_only.svg | 6 + assets/images/svg/card_user.svg | 1 + assets/images/svg/habib logo_only.svg | 5 + assets/langs/ar-SA.json | 10 +- assets/langs/en-US.json | 14 +- lib/core/api/api_client.dart | 4 +- lib/core/api_consts.dart | 1 + lib/core/app_assets.dart | 2 + lib/core/dependencies.dart | 10 +- lib/core/utils/validation_utils.dart | 40 +- .../habib_wallet/habib_wallet_view_model.dart | 34 +- lib/features/paytabs/paytabs_view_model.dart | 39 +- .../is_refundable_invoice_response_model.dart | 23 + .../refund_request_list_response_model.dart | 276 ++++++++-- .../refund_request/refund_request_repo.dart | 64 ++- .../refund_request_view_model.dart | 49 +- lib/generated/locale_keys.g.dart | 9 +- .../authentication/register_step2.dart | 472 +++++++++--------- .../withdraw_request_create_page.dart | 64 ++- .../my_invoices/my_invoices_list.dart | 433 ++++++++-------- .../widgets/invoice_list_card.dart | 50 +- .../refund_request/refund_history_screen.dart | 192 +++---- .../refund_request_create_page.dart | 103 ++-- .../refund_request_detail_screen.dart | 283 +++++++++++ .../symptoms_checker/triage_screen.dart | 6 +- lib/routes/app_routes.dart | 7 + lib/splashPage.dart | 14 +- lib/widgets/chip/app_custom_chip_widget.dart | 2 +- lib/widgets/input_widget.dart | 2 +- 29 files changed, 1512 insertions(+), 703 deletions(-) create mode 100644 assets/images/svg/bank_icon_only.svg create mode 100644 assets/images/svg/habib logo_only.svg create mode 100644 lib/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart create mode 100644 lib/presentation/refund_request/refund_request_detail_screen.dart 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/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/langs/ar-SA.json b/assets/langs/ar-SA.json index 0904362e..12219c0c 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1858,7 +1858,7 @@ "submitRequest": "تقديم الطلب", "ibanIsRequired": "حقل رقم الآيبان مطلوب", "descriptionIsRequired": "الوصف مطلوب", - "invalidIbanFormat": "صيغة الآيبان غير صحيحة. يجب أن تبدأ بـ SA يليها 20 رقمًا (مثال: SA0380000000608010167519)", + "invalidIbanFormat": "رقم الآيبان غير صالح. يرجى إدخال رقم آيبان صالح (مثال: SA0380000000608010167519)", "refundHistory": "سجل الاسترداد", "noRefundHistoryFound": "لا يوجد سجل استرداد حتى الآن.", "invoiceNo": "رقم الفاتورة", @@ -1886,5 +1886,11 @@ "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 07e03830..8e1872b9 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1850,7 +1850,7 @@ "submitRequest": "Submit Request", "ibanIsRequired": "IBAN is required", "descriptionIsRequired": "Description is required", - "invalidIbanFormat": "Invalid IBAN. Must start with SA followed by 20 digits (e.g. SA0380000000608010167519)", + "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.", @@ -1873,12 +1873,20 @@ "refundReason6": "Clinical procedure cancelled or changed by requesting doctor", "refundReason7": "Service unit/lab/radiology procedure cancelled or changed by requesting doctor", "selectProcedures": "Select Procedures", - "submitEReferral": "Submit", "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/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 7b104ab3..d7717b54 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -208,9 +208,9 @@ class ApiClientImp implements ApiClient { body['TokenID'] = "@dm!n"; } - // TODO: REMOVE THIS MUST // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 4768571; + // body['PatientID'] = 4770714; + // body['PatientID'] = 89754444; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; // body['SessionID'] = "45786230487560q"; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 1cd20a55..6a27f641 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -276,6 +276,7 @@ class ApiConsts { 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"; diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index f4c18ff7..c4a79aed 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -390,6 +390,8 @@ class AppAssets { 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'; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 81c1fedb..279b61c8 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -339,17 +339,15 @@ class AppDependencies { getIt.registerLazySingleton(() => AskDoctorViewModel(askDoctorRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton( - () => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton( - () => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt())); getIt.registerLazySingleton(() => RefundRequestViewModel( appState: getIt(), refundRequestRepo: getIt(), errorHandlerService: getIt(), )); - getIt.registerLazySingleton(() => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton(() => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt())); + getIt.registerLazySingleton( + () => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt())); + getIt.registerLazySingleton( + () => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt())); getIt.registerLazySingleton(() => PayTabsViewModel(appState: getIt())); } diff --git a/lib/core/utils/validation_utils.dart b/lib/core/utils/validation_utils.dart index b5ba4bae..ca644e0c 100644 --- a/lib/core/utils/validation_utils.dart +++ b/lib/core/utils/validation_utils.dart @@ -181,7 +181,7 @@ class ValidationUtils { } } - if (phoneNumber == null || phoneNumber.isEmpty) { + if (phoneNumber == null || phoneNumber.isEmpty) { _dialogService.showExceptionBottomSheet(message: LocaleKeys.enterValidPhoneNumber.tr(), onOkPressed: onOkPress); return false; } @@ -189,13 +189,41 @@ class ValidationUtils { return isCorrectID; } - static bool isNullOrEmpty(String? value) { + static bool isNullOrEmpty(String? value) { return value == null || value.trim().isEmpty; } - /// Validates a KSA IBAN: must start with SA + 2 check digits + 20 alphanumeric chars (24 chars total). - static bool validateKsaIban(String iban) { - final ksaIbanRegex = RegExp(r'^SA\d{2}[0-9A-Z]{20}$', caseSensitive: false); - return ksaIbanRegex.hasMatch(iban.trim().replaceAll(' ', '')); + 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/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart index c9ff8649..e1e14ecf 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -1,16 +1,16 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:hmg_patient_app_new/core/utils/validation_utils.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/refundable_invoices_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'; @@ -96,7 +96,7 @@ 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 { @@ -161,7 +161,7 @@ class HabibWalletViewModel extends ChangeNotifier { _ibanError = LocaleKeys.ibanIsRequired.tr(); isValid = false; } else { - if (!ValidationUtils.validateKsaIban(iban.trim())) { + if (!ValidationUtils.validateIban(iban.trim())) { _ibanError = LocaleKeys.invalidIbanFormat.tr(); isValid = false; } @@ -235,7 +235,18 @@ class HabibWalletViewModel extends ChangeNotifier { ); } - Future submitWalletWithdrawal({ + /// 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, @@ -255,13 +266,15 @@ class HabibWalletViewModel extends ChangeNotifier { advanceNo: (a.advanceNo ?? 0).toString(), paidAmount: a.paidAmount ?? 0.0, balanceAmount: a.balanceAmount ?? 0.0, - advanceDate: a.advanceDate ?? '', + advanceDate: a.advanceDate ?? "", + // advanceDate: _formatAdvanceDate(a.advanceDate), )) .toList(); final request = SubmitRefundRequestModel( projectId: projectId, - refundType: '2', // wallet withdrawal + refundType: '2', + // wallet withdrawal refundProject: projectName, refundProjectId: projectId, refundPatientId: patientId, @@ -274,7 +287,7 @@ class HabibWalletViewModel extends ChangeNotifier { refundDoctorName: '', refundClinicId: 0, refundClinicName: '', - refundDestination: '2', // bank transfer + refundDestination: '2', // only bank transfer refundIban: iban, refundRemarks: remarks, refundTotalAmount: totalAdvancesBalance, @@ -456,7 +469,7 @@ class HabibWalletViewModel extends ChangeNotifier { result.fold( // (failure) async => await errorHandlerService.handleError(failure: failure), (failure) async { - if(onError != null) { + if (onError != null) { onError(failure.message); } }, @@ -522,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/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/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 index d8b6ffc9..b17dd5aa 100644 --- 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 @@ -13,10 +13,10 @@ class SubmitRefundResponseModel { factory SubmitRefundResponseModel.fromJson(Map json) { return SubmitRefundResponseModel( - message: json['message'] as String?, - refundType: json['refundType'] as String?, - patientId: json['patientId']?.toString(), - refNo: json['refNo']?.toString(), + 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(), ); } } @@ -53,40 +53,260 @@ class RefundStatusModel { } } -class RefundInvoiceModel { - final String? invoiceNumber; - final String? invoiceDate; - final double? totalAmount; - final double? patientShare; +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 String? clinicNameN; + 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? projectName; - final String? projectNameN; + 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; - RefundInvoiceModel({ - this.invoiceNumber, - this.invoiceDate, - this.totalAmount, - this.patientShare, + InvoiceRequestPayload({ + this.advancesList, + this.appointmentNo, + this.clinicId, this.clinicName, - this.clinicNameN, + 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.projectName, - this.projectNameN, + 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?, - invoiceDate: json['InvoiceDate'] as String?, - totalAmount: (json['TotalAmount'] as num?)?.toDouble(), - patientShare: (json['PatientShare'] as num?)?.toDouble(), - clinicName: json['ClinicName'] as String?, - clinicNameN: json['ClinicNameN'] as String?, - projectId: json['ProjectID'] as int?, - projectName: json['ProjectName'] as String?, - projectNameN: json['ProjectNameN'] 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/refund_request_repo.dart b/lib/features/refund_request/refund_request_repo.dart index 03797e54..eaf7d9e0 100644 --- a/lib/features/refund_request/refund_request_repo.dart +++ b/lib/features/refund_request/refund_request_repo.dart @@ -4,6 +4,7 @@ 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'; @@ -20,9 +21,18 @@ abstract class RefundRequestRepo { Future>> getRefundStatus({required String referenceNumber}); - Future>>> getRefundInvoices({required String patientId, String invoiceNumberQuery}); + 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 { @@ -215,10 +225,12 @@ class RefundRequestRepoImp implements RefundRequestRepo { @override Future>>> getRefundInvoices({ + required int refundType, required String patientId, String invoiceNumberQuery = "", }) async { final Map body = { + "Refund_RefundType": refundType, "Refund_PatientId": patientId, "Refund_InvoiceNumberQuery": invoiceNumberQuery, }; @@ -235,7 +247,7 @@ class RefundRequestRepoImp implements RefundRequestRepo { }, onSuccess: (response, statusCode, {messageStatus, errorMessage}) { try { - final listJson = response['RefundInvoicesList']; + final listJson = response['Refund_InvoicesList']; final List invoices = listJson != null ? (listJson as List).map((item) => RefundInvoiceModel.fromJson(item as Map)).toList() : []; @@ -297,4 +309,52 @@ class RefundRequestRepoImp implements RefundRequestRepo { 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 index 51212741..6c9b58dd 100644 --- a/lib/features/refund_request/refund_request_view_model.dart +++ b/lib/features/refund_request/refund_request_view_model.dart @@ -4,6 +4,7 @@ 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'; @@ -28,7 +29,7 @@ class RefundableProcedure { final int procedureId; final String procedureName; final String? procedureNameN; - final double patientShare; + final double patientPaidAmount; bool isSelected; String? refundReason; @@ -40,7 +41,7 @@ class RefundableProcedure { required this.procedureId, required this.procedureName, this.procedureNameN, - required this.patientShare, + required this.patientPaidAmount, this.isSelected = false, this.refundReason, this.apiLineItemNo, @@ -240,7 +241,7 @@ class RefundRequestViewModel extends ChangeNotifier { double get totalRefundableAmount { double total = 0.0; for (final p in selectedProcedures) { - total += p.patientShare; + total += p.patientPaidAmount; } return total; } @@ -276,7 +277,7 @@ class RefundRequestViewModel extends ChangeNotifier { _ibanError = LocaleKeys.ibanIsRequired.tr(); isValid = false; } else { - if (!ValidationUtils.validateKsaIban(iban.trim())) { + if (!ValidationUtils.validateIban(iban.trim())) { _ibanError = LocaleKeys.invalidIbanFormat.tr(); isValid = false; } @@ -457,7 +458,7 @@ class RefundRequestViewModel extends ChangeNotifier { bool isRefundInvoicesLoading = false; Future getRefundInvoices({ - String? patientId, + required int refundType, String invoiceNumberQuery = "", Function(List)? onSuccess, Function(String)? onError, @@ -466,10 +467,10 @@ class RefundRequestViewModel extends ChangeNotifier { refundInvoicesList.clear(); notifyListeners(); - // final resolvedPatientId = patientId ?? appState.getAuthenticatedUser()?.patientId?.toString() ?? ""; - final resolvedPatientId = 4768571; + final resolvedPatientId = appState.getAuthenticatedUser()?.patientId?.toString() ?? ""; final result = await refundRequestRepo.getRefundInvoices( - patientId: resolvedPatientId.toString(), + refundType: refundType, + patientId: resolvedPatientId, invoiceNumberQuery: invoiceNumberQuery, ); @@ -502,7 +503,7 @@ class RefundRequestViewModel extends ChangeNotifier { // ── Submit Refund ───────────────────────────────────────────────────────── bool isSubmitting = false; - Future submitRefund({ + Future submitInvoiceRefundRequest({ required SubmitRefundRequestModel request, Function(SubmitRefundResponseModel)? onSuccess, Function(String)? onError, @@ -535,4 +536,34 @@ class RefundRequestViewModel extends ChangeNotifier { }, ); } + + // ── 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 ec7bfb1d..7700984d 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1871,5 +1871,12 @@ abstract class LocaleKeys { 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/presentation/authentication/register_step2.dart b/lib/presentation/authentication/register_step2.dart index c17d9ba7..639ebef1 100644 --- a/lib/presentation/authentication/register_step2.dart +++ b/lib/presentation/authentication/register_step2.dart @@ -188,254 +188,270 @@ class _RegisterNew extends State { ), padding: EdgeInsets.only(left: 16.h, right: 16.h), child: Column( - children: [ - TextInputWidget( - labelText: authVM!.isUserFromUAE() ? LocaleKeys.fullName.tr(context: context) : LocaleKeys.name.tr(context: context), - hintText: authVM!.isUserFromUAE() ? LocaleKeys.enterNameHere.tr(context: context) : (name), - controller: authVM!.isUserFromUAE() ? authVM!.nameController : null, - isEnable: true, - prefix: null, - isAllowRadius: false, - isBorderAllowed: false, - keyboardType: TextInputType.text, - // textInputAction: TextInputAction.done, - onSubmitted: (value) { - FocusScope.of(context).unfocus(); - }, - onChange: (value) { - // Clear error when user starts typing - authVM!.clearNameError(); - }, - isAllowLeadingIcon: true, - isReadOnly: authVM!.isUserFromUAE() ? false : true, - leadingIcon: AppAssets.user_circle, - labelColor: AppColors.textColor, - ).paddingSymmetrical(0.h, 8.h), - // Show error message if exists - if (authVM!.isUserFromUAE() && authVM!.nameError != null) - Padding( - padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w), - child: Text( - authVM!.nameError!, - style: TextStyle( - color: AppColors.primaryRedColor, - fontSize: 12.f, + children: [ + TextInputWidget( + labelText: authVM!.isUserFromUAE() ? LocaleKeys.fullName.tr(context: context) : LocaleKeys.name.tr(context: context), + hintText: authVM!.isUserFromUAE() ? LocaleKeys.enterNameHere.tr(context: context) : (name), + controller: authVM!.isUserFromUAE() ? authVM!.nameController : null, + isEnable: true, + prefix: null, + isAllowRadius: false, + isBorderAllowed: false, + keyboardType: TextInputType.text, + // textInputAction: TextInputAction.done, + onSubmitted: (value) { + FocusScope.of(context).unfocus(); + }, + onChange: (value) { + // Clear error when user starts typing + authVM!.clearNameError(); + }, + isAllowLeadingIcon: true, + isReadOnly: authVM!.isUserFromUAE() ? false : true, + leadingIcon: AppAssets.user_circle, + labelColor: AppColors.textColor, + ).paddingSymmetrical(0.h, 8.h), + // Show error message if exists + if (authVM!.isUserFromUAE() && authVM!.nameError != null) + Padding( + padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w), + child: Text( + authVM!.nameError!, + style: TextStyle( + color: AppColors.primaryRedColor, + fontSize: 12.f, + ), + ), ), - ), - ), - Divider(height: 1.h, color: AppColors.greyColor), - TextInputWidget( - labelText: LocaleKeys.nationalIdNumber.tr(context: context), - hintText: authVM!.isUserFromUAE() ? appState.getUserRegistrationPayload.patientIdentificationId.toString() : (appState.getNHICUserData.idNumber ?? ""), - controller: null, - isEnable: true, - prefix: null, - isAllowRadius: false, - isBorderAllowed: false, - isAllowLeadingIcon: true, - isReadOnly: true, - labelColor: AppColors.textColor, - leadingIcon: AppAssets.student_card) - .paddingSymmetrical(0.h, 8.h), - Divider(height: 1, color: AppColors.greyColor), - authVM!.isUserFromUAE() - ? Selector( - selector: (_, authViewModel) => authViewModel.genderType, - shouldRebuild: (previous, next) => previous != next, - builder: (context, genderType, child) { - final authVM = context.read(); - return DropdownWidget( - labelText: LocaleKeys.gender.tr(context: context), - hintText: LocaleKeys.malE.tr(context: context), - isEnable: true, - dropdownItems: GenderTypeEnum.values.map((e) => appState.isArabic() ? e.typeAr : e.type).toList(), - selectedValue: genderType != null ? (appState.isArabic() ? genderType.typeAr : genderType.type) : "", - onChange: authVM.onGenderChange, - isBorderAllowed: false, - hasSelectionCustomIcon: true, - isAllowRadius: false, - labelColor: AppColors.textColor, - padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 0, right: 0), - selectionCustomIcon: AppAssets.arrow_down, - leadingIcon: AppAssets.user_full, - ).withVerticalPadding(8); - }) - : TextInputWidget( - labelText: LocaleKeys.gender.tr(context: context), - hintText: (appState.getNHICUserData.gender ?? ""), + Divider(height: 1.h, color: AppColors.greyColor), + TextInputWidget( + labelText: LocaleKeys.nationalIdNumber.tr(context: context), + hintText: authVM!.isUserFromUAE() + ? appState.getUserRegistrationPayload.patientIdentificationId.toString() + : (appState.getNHICUserData.idNumber ?? ""), controller: null, isEnable: true, prefix: null, isAllowRadius: false, isBorderAllowed: false, isAllowLeadingIcon: true, - isReadOnly: authVM!.isUserFromUAE() ? false : true, - leadingIcon: AppAssets.user_full, - labelColor: AppColors.textColor, - onChange: (value) {}) - .paddingSymmetrical(0.h, 8.h), - // Show gender error message if exists (for UAE users) - if (authVM!.isUserFromUAE() && authVM!.genderError != null) - Padding( - padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w), - child: Text( - authVM!.genderError!, - style: TextStyle( - color: AppColors.primaryRedColor, - fontSize: 12.f, - ), - ), - ), - Divider(height: 1, color: AppColors.greyColor), - authVM!.isUserFromUAE() - ? Selector( - selector: (_, authViewModel) => authViewModel.maritalStatus, - shouldRebuild: (previous, next) => previous != next, - builder: (context, maritalStatus, child) { - final authVM = context.read(); // For onChange - return DropdownWidget( - labelText: LocaleKeys.maritalStatus.tr(context: context), - hintText: LocaleKeys.married.tr(context: context), - isEnable: true, - dropdownItems: MaritalStatusTypeEnum.values.map((e) => appState.isArabic() ? e.typeAr : e.type).toList(), - selectedValue: maritalStatus != null ? (appState.isArabic() ? maritalStatus.typeAr : maritalStatus.type) : "", - onChange: authVM.onMaritalStatusChange, - isBorderAllowed: false, - hasSelectionCustomIcon: true, - isAllowRadius: false, - labelColor: AppColors.textColor, - padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 0, right: 0), - selectionCustomIcon: AppAssets.arrow_down, - leadingIcon: AppAssets.smart_phone, - ).withVerticalPadding(8); - }, - ) - : TextInputWidget( - labelText: LocaleKeys.maritalStatus.tr(context: context), - hintText: appState.isArabic() - ? (MaritalStatusTypeExtension.fromValue(appState.getNHICUserData.maritalStatusCode)?.typeAr ?? '') - : (MaritalStatusTypeExtension.fromValue(appState.getNHICUserData.maritalStatusCode)?.type ?? ''), - isEnable: true, - prefix: null, - isAllowRadius: false, - isBorderAllowed: false, - isAllowLeadingIcon: true, isReadOnly: true, labelColor: AppColors.textColor, - leadingIcon: AppAssets.smart_phone, - onChange: (value) {}) + leadingIcon: AppAssets.student_card) .paddingSymmetrical(0.h, 8.h), - // Show marital status error message if exists (for UAE users) - if (authVM!.isUserFromUAE() && authVM!.maritalStatusError != null) - Padding( - padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w), - child: Text( - authVM!.maritalStatusError!, - style: TextStyle( - color: AppColors.primaryRedColor, - fontSize: 12.f, + Divider(height: 1, color: AppColors.greyColor), + authVM!.isUserFromUAE() + ? Selector( + selector: (_, authViewModel) => authViewModel.genderType, + shouldRebuild: (previous, next) => previous != next, + builder: (context, genderType, child) { + final authVM = context.read(); + return DropdownWidget( + labelText: LocaleKeys.gender.tr(context: context), + hintText: LocaleKeys.malE.tr(context: context), + isEnable: true, + dropdownItems: GenderTypeEnum.values.map((e) => appState.isArabic() ? e.typeAr : e.type).toList(), + selectedValue: genderType != null ? (appState.isArabic() ? genderType.typeAr : genderType.type) : "", + onChange: authVM.onGenderChange, + isBorderAllowed: false, + hasSelectionCustomIcon: true, + isAllowRadius: false, + labelColor: AppColors.textColor, + padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 0, right: 0), + selectionCustomIcon: AppAssets.arrow_down, + leadingIcon: AppAssets.user_full, + ).withVerticalPadding(8); + }) + : TextInputWidget( + labelText: LocaleKeys.gender.tr(context: context), + hintText: (appState.getNHICUserData.gender ?? ""), + controller: null, + isEnable: true, + prefix: null, + isAllowRadius: false, + isBorderAllowed: false, + isAllowLeadingIcon: true, + isReadOnly: authVM!.isUserFromUAE() ? false : true, + leadingIcon: AppAssets.user_full, + labelColor: AppColors.textColor, + onChange: (value) {}) + .paddingSymmetrical(0.h, 8.h), + // Show gender error message if exists (for UAE users) + if (authVM!.isUserFromUAE() && authVM!.genderError != null) + Padding( + padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w), + child: Text( + authVM!.genderError!, + style: TextStyle( + color: AppColors.primaryRedColor, + fontSize: 12.f, + ), + ), + ), + Divider(height: 1, color: AppColors.greyColor), + authVM!.isUserFromUAE() + ? Selector( + selector: (_, authViewModel) => authViewModel.maritalStatus, + shouldRebuild: (previous, next) => previous != next, + builder: (context, maritalStatus, child) { + final authVM = context.read(); // For onChange + return DropdownWidget( + labelText: LocaleKeys.maritalStatus.tr(context: context), + hintText: LocaleKeys.married.tr(context: context), + isEnable: true, + dropdownItems: MaritalStatusTypeEnum.values.map((e) => appState.isArabic() ? e.typeAr : e.type).toList(), + selectedValue: maritalStatus != null ? (appState.isArabic() ? maritalStatus.typeAr : maritalStatus.type) : "", + onChange: authVM.onMaritalStatusChange, + isBorderAllowed: false, + hasSelectionCustomIcon: true, + isAllowRadius: false, + labelColor: AppColors.textColor, + padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 0, right: 0), + selectionCustomIcon: AppAssets.arrow_down, + leadingIcon: AppAssets.smart_phone, + ).withVerticalPadding(8); + }, + ) + : TextInputWidget( + labelText: LocaleKeys.maritalStatus.tr(context: context), + hintText: appState.isArabic() + ? (MaritalStatusTypeExtension.fromValue(appState.getNHICUserData.maritalStatusCode)?.typeAr ?? '') + : (MaritalStatusTypeExtension.fromValue(appState.getNHICUserData.maritalStatusCode)?.type ?? ''), + isEnable: true, + prefix: null, + isAllowRadius: false, + isBorderAllowed: false, + isAllowLeadingIcon: true, + isReadOnly: true, + labelColor: AppColors.textColor, + leadingIcon: AppAssets.smart_phone, + onChange: (value) {}) + .paddingSymmetrical(0.h, 8.h), + // Show marital status error message if exists (for UAE users) + if (authVM!.isUserFromUAE() && authVM!.maritalStatusError != null) + Padding( + padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w), + child: Text( + authVM!.maritalStatusError!, + style: TextStyle( + color: AppColors.primaryRedColor, + fontSize: 12.f, + ), + ), + ), + Divider(height: 1.h, color: AppColors.greyColor), + authVM!.isUserFromUAE() + ? Selector? countriesList, NationalityCountries? selectedCountry, bool isArabic})>( + selector: (context, authViewModel) { + final appState = getIt.get(); + return ( + countriesList: authViewModel.countriesList, + selectedCountry: authViewModel.pickedCountryByUAEUser, + isArabic: appState.isArabic(), + ); + }, + shouldRebuild: (previous, next) => + previous.countriesList != next.countriesList || + previous.selectedCountry != next.selectedCountry || + previous.isArabic != next.isArabic, + builder: (context, data, child) { + final authVM = context.read(); + return DropdownWidget( + labelText: LocaleKeys.country.tr(context: context), + hintText: LocaleKeys.uae.tr(context: context), + isEnable: true, + dropdownItems: (data.countriesList ?? []).map((e) => data.isArabic ? e.nameN ?? "" : e.name ?? "").toList(), + selectedValue: data.selectedCountry != null + ? data.isArabic + ? data.selectedCountry!.nameN ?? "" + : data.selectedCountry!.name ?? "" + : "", + onChange: authVM.onUAEUserCountrySelection, + isBorderAllowed: false, + hasSelectionCustomIcon: true, + labelColor: AppColors.textColor, + isAllowRadius: false, + padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 0, right: 0), + selectionCustomIcon: AppAssets.arrow_down, + leadingIcon: AppAssets.globe, + ).withVerticalPadding(8.h); + }, + ) + : TextInputWidget( + labelText: LocaleKeys.nationality.tr(context: context), + hintText: appState.isArabic() + ? (authVM!.countriesList! + .firstWhere((e) => e.id == (appState.getNHICUserData.nationalityCode ?? ""), + orElse: () => NationalityCountries()) + .nameN ?? + "") + : (authVM!.countriesList! + .firstWhere((e) => e.id == (appState.getNHICUserData.nationalityCode ?? ""), + orElse: () => NationalityCountries()) + .name ?? + ""), + isEnable: true, + prefix: null, + isAllowRadius: false, + isBorderAllowed: false, + isAllowLeadingIcon: true, + isReadOnly: true, + labelColor: AppColors.textColor, + leadingIcon: AppAssets.globe, + onChange: (value) {}) + .paddingSymmetrical(0.h, 8.h), + // Show country error message if exists (for UAE users) + if (authVM!.isUserFromUAE() && authVM!.countryError != null) + Padding( + padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w), + child: Text( + authVM!.countryError!, + style: TextStyle( + color: AppColors.primaryRedColor, + fontSize: 12.f, + ), + ), ), + Divider( + height: 1, + color: AppColors.greyColor, ), - ), - Divider(height: 1.h, color: AppColors.greyColor), - authVM!.isUserFromUAE() - ? Selector? countriesList, NationalityCountries? selectedCountry, bool isArabic})>( - selector: (context, authViewModel) { - final appState = getIt.get(); - return ( - countriesList: authViewModel.countriesList, - selectedCountry: authViewModel.pickedCountryByUAEUser, - isArabic: appState.isArabic(), - ); - }, - shouldRebuild: (previous, next) => previous.countriesList != next.countriesList || previous.selectedCountry != next.selectedCountry || previous.isArabic != next.isArabic, - builder: (context, data, child) { - final authVM = context.read(); - return DropdownWidget( - labelText: LocaleKeys.country.tr(context: context), - hintText: LocaleKeys.uae.tr(context: context), - isEnable: true, - dropdownItems: (data.countriesList ?? []).map((e) => data.isArabic ? e.nameN ?? "" : e.name ?? "").toList(), - selectedValue: data.selectedCountry != null - ? data.isArabic - ? data.selectedCountry!.nameN ?? "" - : data.selectedCountry!.name ?? "" - : "", - onChange: authVM.onUAEUserCountrySelection, - isBorderAllowed: false, - hasSelectionCustomIcon: true, - labelColor: AppColors.textColor, - isAllowRadius: false, - padding: EdgeInsets.only(top: 8.h, bottom: 8.h, left: 0, right: 0), - selectionCustomIcon: AppAssets.arrow_down, - leadingIcon: AppAssets.globe, - ).withVerticalPadding(8.h); - }, - ) - : TextInputWidget( - labelText: LocaleKeys.nationality.tr(context: context), - hintText: appState.isArabic() - ? (authVM!.countriesList!.firstWhere((e) => e.id == (appState.getNHICUserData.nationalityCode ?? ""), orElse: () => NationalityCountries()).nameN ?? "") - : (authVM!.countriesList!.firstWhere((e) => e.id == (appState.getNHICUserData.nationalityCode ?? ""), orElse: () => NationalityCountries()).name ?? ""), - isEnable: true, + TextInputWidget( + labelText: LocaleKeys.mobileNumber.tr(context: context), + hintText: appState.getUserRegistrationPayload.patientMobileNumber.toString(), + controller: null, + isEnable: false, prefix: null, isAllowRadius: false, isBorderAllowed: false, isAllowLeadingIcon: true, - isReadOnly: true, labelColor: AppColors.textColor, - leadingIcon: AppAssets.globe, - onChange: (value) {}) + isReadOnly: true, + leadingIcon: AppAssets.call) .paddingSymmetrical(0.h, 8.h), - // Show country error message if exists (for UAE users) - if (authVM!.isUserFromUAE() && authVM!.countryError != null) - Padding( - padding: EdgeInsets.only(left: 52.w, top: 4.h, bottom: 4.h, right: 16.w), - child: Text( - authVM!.countryError!, - style: TextStyle( - color: AppColors.primaryRedColor, - fontSize: 12.f, - ), + Divider( + height: 1.h, + color: AppColors.greyColor, ), - ), - Divider( - height: 1, - color: AppColors.greyColor, - ), - TextInputWidget( - labelText: LocaleKeys.mobileNumber.tr(context: context), - hintText: appState.getUserRegistrationPayload.patientMobileNumber.toString(), - controller: null, - isEnable: false, - prefix: null, - isAllowRadius: false, - isBorderAllowed: false, - isAllowLeadingIcon: true, - labelColor: AppColors.textColor, - isReadOnly: true, - leadingIcon: AppAssets.call) - .paddingSymmetrical(0.h, 8.h), - Divider( - height: 1.h, - color: AppColors.greyColor, + TextInputWidget( + labelText: LocaleKeys.dob.tr(context: context), + hintText: authVM!.isUserFromUAE() + ? (appState.getUserRegistrationPayload.dob ?? '') + : (appState.getNHICUserData.dateOfBirth ?? ""), + controller: authVM!.isUserFromUAE() ? authVM!.dobController : null, + isEnable: false, + prefix: null, + isBorderAllowed: false, + isAllowLeadingIcon: true, + isReadOnly: true, + labelColor: AppColors.textColor, + leadingIcon: AppAssets.birthday_cake, + selectionType: null, + ).paddingSymmetrical(0.h, 8.h), + ], ), - TextInputWidget( - labelText: LocaleKeys.dob.tr(context: context), - hintText: authVM!.isUserFromUAE() ? (appState.getUserRegistrationPayload.dob ?? '') : (appState.getNHICUserData.dateOfBirth ?? ""), - controller: authVM!.isUserFromUAE() ? authVM!.dobController : null, - isEnable: false, - prefix: null, - isBorderAllowed: false, - isAllowLeadingIcon: true, - isReadOnly: true, - labelColor: AppColors.textColor, - leadingIcon: AppAssets.birthday_cake, - selectionType: null, - ).paddingSymmetrical(0.h, 8.h), - ], - ), - ); + ); }, ), SizedBox(height: 50.h), diff --git a/lib/presentation/habib_wallet/withdraw_request_create_page.dart b/lib/presentation/habib_wallet/withdraw_request_create_page.dart index b069b441..cc0c9731 100644 --- a/lib/presentation/habib_wallet/withdraw_request_create_page.dart +++ b/lib/presentation/habib_wallet/withdraw_request_create_page.dart @@ -185,9 +185,17 @@ class _WithdrawRequestCreatePageState extends State { Widget _buildBankInputDetails() { return Consumer( builder: (context, viewModel, _) { + final hasError = viewModel.ibanError != null || viewModel.withdrawDescriptionError != null; return Container( padding: EdgeInsets.all(16.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + 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( @@ -195,7 +203,6 @@ class _WithdrawRequestCreatePageState extends State { hintText: "SA0000000000000000000000", controller: _bankDetailsController, focusNode: _ibanFocusNode, - leadingIconNeedsContainer: false, autoFocus: false, keyboardType: TextInputType.text, isEnable: true, @@ -207,7 +214,7 @@ class _WithdrawRequestCreatePageState extends State { isCountryDropDown: false, leadingIcon: AppAssets.bankIcon, fontFamily: "Poppins", - hasError: viewModel.ibanError != null, + hasError: false, errorMessage: viewModel.ibanError, onChange: (value) { viewModel.clearIbanError(); @@ -224,7 +231,7 @@ class _WithdrawRequestCreatePageState extends State { focusNode: _descriptionFocusNode, autoFocus: false, keyboardType: TextInputType.text, - leadingIconNeedsContainer: false, + // leadingIconNeedsContainer: false, isEnable: true, isReadOnly: false, prefix: null, @@ -234,7 +241,7 @@ class _WithdrawRequestCreatePageState extends State { isCountryDropDown: false, leadingIcon: AppAssets.descriptionIcon, fontFamily: "Poppins", - hasError: viewModel.withdrawDescriptionError != null, + hasError: false, errorMessage: viewModel.withdrawDescriptionError, onChange: (value) { viewModel.clearWithdrawDescriptionError(); @@ -301,10 +308,9 @@ class _WithdrawRequestCreatePageState extends State { onConfirmTap: () async { Navigator.pop(context); LoaderBottomSheet.showLoader(); - final user = _appState.getAuthenticatedUser(); final hospital = vm.selectedHospital; - await vm.submitWalletWithdrawal( + await vm.submitWalletWithdrawalRequest( iban: _bankDetailsController.text.trim(), remarks: _descriptionController.text.trim(), patientName: '${user?.firstName ?? ''} ${user?.lastName ?? ''}'.trim(), @@ -342,16 +348,36 @@ class _WithdrawRequestCreatePageState extends State { 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(context: context), + text: LocaleKeys.ok.tr(), onPressed: () { - Navigator.pop(context); - Navigator.pop(context); + 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, ), @@ -364,7 +390,7 @@ class _WithdrawRequestCreatePageState extends State { isCloseButtonVisible: false, isDismissible: false, isFullScreen: false, - callBackFunc: () {}, + isAutoDismiss: false, ); } @@ -381,7 +407,13 @@ class _WithdrawRequestCreatePageState extends State { Expanded( child: CollapsingListView( title: LocaleKeys.requestRefund.tr(context: context), - requests: () => Navigator.of(context).push(CustomPageRoute(page: RefundHistoryScreen())), + 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( @@ -391,8 +423,8 @@ class _WithdrawRequestCreatePageState extends State { if (habibWalletViewModel.selectedHospital != null) ...[ SizedBox(height: 16.h), _buildBankInputDetails(), - SizedBox(height: 16.h), - _buildImagePickerDropDown(), + // SizedBox(height: 16.h), + // _buildImagePickerDropDown(), ], ], ), @@ -407,3 +439,7 @@ class _WithdrawRequestCreatePageState extends State { ); } } + +// 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/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 2cafdc57..dc5414cd 100644 --- a/lib/presentation/my_invoices/widgets/invoice_list_card.dart +++ b/lib/presentation/my_invoices/widgets/invoice_list_card.dart @@ -11,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 '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; @@ -24,6 +28,46 @@ 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(); @@ -174,11 +218,7 @@ class InvoiceListCard extends StatelessWidget { child: CustomButton( text: LocaleKeys.requestRefund.tr(context: context), onPressed: () { - Navigator.of(context).push( - CustomPageRoute( - page: RefundRequestCreatePage(initialTabIndex: 0, invoice: getInvoicesListResponseModel), - ), - ); + _handleRefundInvoiceTap(context); }, icon: AppAssets.refundIcon, iconColor: AppColors.caloriesCalculatorColor, diff --git a/lib/presentation/refund_request/refund_history_screen.dart b/lib/presentation/refund_request/refund_history_screen.dart index 04f369d0..630c5dc0 100644 --- a/lib/presentation/refund_request/refund_history_screen.dart +++ b/lib/presentation/refund_request/refund_history_screen.dart @@ -3,8 +3,8 @@ 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_state.dart'; -import 'package:hmg_patient_app_new/core/dependencies.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'; @@ -12,43 +12,40 @@ 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 { - const RefundHistoryScreen({super.key}); + final int refundType; + + const RefundHistoryScreen({super.key, required this.refundType}); @override State createState() => _RefundHistoryScreenState(); } class _RefundHistoryScreenState extends State { - late AppState _appState; - @override void initState() { super.initState(); - _appState = getIt(); final vm = context.read(); scheduleMicrotask(() async { - await vm.getRefundInvoices(); + await vm.getRefundInvoices(refundType: widget.refundType); }); } - // ── Helpers ─────────────────────────────────���───────────────────────────── + // ── Helpers ────────────────────────────────────────────────────────────── String _formatDate(String? rawDate) { if (rawDate == null || rawDate.isEmpty) return ''; try { - // Handle WCF /Date(...)/ format - final match = RegExp(r'/Date\((-?\d+)').firstMatch(rawDate); - if (match != null) { - final ms = int.parse(match.group(1)!); - return DateFormat('MMM dd, yyyy').format(DateTime.fromMillisecondsSinceEpoch(ms)); - } - return DateFormat('MMM dd, yyyy').format(DateTime.parse(rawDate)); + final date = DateUtil.convertStringToDate(rawDate); + return DateFormat('MMM dd, yyyy').format(date); } catch (_) { return rawDate; } @@ -69,17 +66,49 @@ class _RefundHistoryScreenState extends State { // ── Card ────────────────────────────────────────────────────────────────── - Widget _buildInvoiceCard(RefundInvoiceModel? invoice, {bool isLoading = false}) { - final isArabic = _appState.isArabic(); + 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; + } - final clinicName = isLoading ? 'Clinic Name' : (isArabic ? (invoice?.clinicNameN ?? invoice?.clinicName) : invoice?.clinicName) ?? '-'; + 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; - final projectName = isLoading ? 'Hospital Name' : (isArabic ? (invoice?.projectNameN ?? invoice?.projectName) : invoice?.projectName) ?? '-'; + // 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); - final invoiceNo = isLoading ? 'INV-000000' : (invoice?.invoiceNumber ?? '-'); - final invoiceDate = isLoading ? 'Jan 01, 2024' : _formatDate(invoice?.invoiceDate); - final totalAmount = isLoading ? '0.00' : (invoice?.totalAmount?.toStringAsFixed(2) ?? '-'); - final patientShare = isLoading ? '0.00' : (invoice?.patientShare?.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), @@ -94,89 +123,67 @@ class _RefundHistoryScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Invoice No + Date row + // 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, - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - if (!isLoading) - LocaleKeys.invoiceNo.tr(context: context).toText12( - color: AppColors.textColorLight, - isBold: true, - ), - if (!isLoading) SizedBox(width: 4.w), - invoiceNo.toText14(isBold: true, isEnglishOnly: true).toShimmer2(isShow: isLoading), - ], - ), - invoiceDate.toText12(color: AppColors.textColorLight, isBold: true).toShimmer2(isShow: isLoading), + "${LocaleKeys.referenceNo.tr(context: context)} $referenceNo" + .toText16( + color: AppColors.textColor, + isBold: true, + ) + .toShimmer2(isShow: isLoading), ], ), - SizedBox(height: 12.h), - - // Chips — clinic & hospital + SizedBox(height: 8.h), Wrap( spacing: 6.w, runSpacing: 6.h, children: [ - AppCustomChipWidget( - icon: "", - labelText: clinicName, - ).toShimmer2(isShow: isLoading), - AppCustomChipWidget( - icon: "", - labelText: projectName, - ).toShimmer2(isShow: isLoading), + 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: 12.h), + SizedBox(height: 16.h), - // Amounts row - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.totalAmount - .tr(context: context) - .toText12(color: AppColors.textColorLight, isBold: true) - .toShimmer2(isShow: isLoading), - SizedBox(height: 4.h), - Utils.getPaymentAmountWithSymbol( - totalAmount.toText16(isBold: true, isEnglishOnly: true), - AppColors.blackColor, - 11, - isSaudiCurrency: true, - isExpanded: false, - ).toShimmer2(isShow: isLoading), - ], - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.patientShare - .tr(context: context) - .toText12(color: AppColors.textColorLight, isBold: true) - .toShimmer2(isShow: isLoading), - SizedBox(height: 4.h), - Utils.getPaymentAmountWithSymbol( - patientShare.toText16(isBold: true, isEnglishOnly: true), - AppColors.primaryRedColor, - 11, - isSaudiCurrency: true, - isExpanded: false, - ).toShimmer2(isShow: isLoading), - ], - ), - ), - ], - ), + // 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), ], ), ), @@ -227,7 +234,6 @@ class _RefundHistoryScreenState extends State { if (viewModel.refundInvoicesList.isEmpty) { return _buildEmptyState(); } - return ListView.separated( padding: EdgeInsets.only(top: 24.h), shrinkWrap: true, diff --git a/lib/presentation/refund_request/refund_request_create_page.dart b/lib/presentation/refund_request/refund_request_create_page.dart index 284d4581..fd64bb67 100644 --- a/lib/presentation/refund_request/refund_request_create_page.dart +++ b/lib/presentation/refund_request/refund_request_create_page.dart @@ -6,6 +6,7 @@ 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'; @@ -88,7 +89,7 @@ class _RefundRequestCreatePageState extends State { .map((item) => RefundableProcedure( procedureId: item.lineItemNo ?? 0, procedureName: item.procedureName ?? '', - patientShare: item.patientPaidAmount ?? 0.0, + patientPaidAmount: item.patientPaidAmount ?? 0.0, apiLineItemNo: item.lineItemNo, apiProcedureId: item.procedureId, )) @@ -266,11 +267,16 @@ class _RefundRequestCreatePageState extends State { // ── Bank Transfer Input ─────────────────────────────────────────────────── Widget _buildBankInputDetails(RefundRequestViewModel vm) { + final hasError = vm.ibanError != null || vm.descriptionError != null; return Container( padding: EdgeInsets.all(16.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + decoration: BoxDecoration( color: AppColors.whiteColor, - borderRadius: 24.r, + borderRadius: BorderRadius.circular(24.r), + border: Border.all( + color: hasError ? AppColors.primaryRedBorderColor : Colors.transparent, + width: 1, + ), ), child: Column( children: [ @@ -279,7 +285,6 @@ class _RefundRequestCreatePageState extends State { hintText: "SA0000000000000000000000", controller: _ibanController, focusNode: _ibanFocusNode, - leadingIconNeedsContainer: false, autoFocus: false, keyboardType: TextInputType.text, isEnable: true, @@ -291,7 +296,7 @@ class _RefundRequestCreatePageState extends State { isCountryDropDown: false, leadingIcon: AppAssets.bankIcon, fontFamily: "Poppins", - hasError: vm.ibanError != null, + hasError: false, errorMessage: vm.ibanError, onChange: (value) { vm.clearIbanError(); @@ -308,7 +313,6 @@ class _RefundRequestCreatePageState extends State { focusNode: _descriptionFocusNode, autoFocus: false, keyboardType: TextInputType.text, - leadingIconNeedsContainer: false, isEnable: true, isReadOnly: false, prefix: null, @@ -318,7 +322,7 @@ class _RefundRequestCreatePageState extends State { isCountryDropDown: false, leadingIcon: AppAssets.descriptionIcon, fontFamily: "Poppins", - hasError: vm.descriptionError != null, + hasError: false, errorMessage: vm.descriptionError, onChange: (value) { vm.clearDescriptionError(); @@ -378,7 +382,7 @@ class _RefundRequestCreatePageState extends State { Expanded(child: name.toText16(isBold: true)), SizedBox(width: 8.w), Utils.getPaymentAmountWithSymbol( - procedure.patientShare.toStringAsFixed(2).toText20( + procedure.patientPaidAmount.toStringAsFixed(2).toText20( isBold: true, isEnglishOnly: true, color: AppColors.textColor, @@ -397,26 +401,22 @@ class _RefundRequestCreatePageState extends State { onDone: (reason) => _refundRequestViewModel.setRefundReasonForProcedure(procedure, reason), ), child: Padding( - padding: EdgeInsets.only(top: 12.h), + padding: EdgeInsets.only(top: 16.h), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, + 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: procedure.refundReason != null ? AppColors.textColor : 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, - ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), ], ), ), @@ -438,9 +438,9 @@ class _RefundRequestCreatePageState extends State { // ── Bottom bar ──────────────────────────────────────────────────────────── - Widget _buildBottomBar(RefundRequestViewModel vm) { - final isStep2 = vm.currentStep == 1; - final isWallet = vm.selectedTab == RefundTabType.hmgWallet; + Widget _buildBottomBar(RefundRequestViewModel refundRequestViewModel) { + final isStep2 = refundRequestViewModel.currentStep == 1; + final isWallet = refundRequestViewModel.selectedTab == RefundTabType.hmgWallet; return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( @@ -451,15 +451,15 @@ class _RefundRequestCreatePageState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (!isWallet && vm.selectedProcedures.isNotEmpty) ...[ + if (!isWallet && refundRequestViewModel.selectedProcedures.isNotEmpty) ...[ Padding( padding: EdgeInsets.fromLTRB(24.w, 16.h, 24.w, 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.totalAmount.toText18(color: AppColors.textColor, isBold: true), + LocaleKeys.totalAmount.tr().toText18(color: AppColors.textColor, isBold: true), Utils.getPaymentAmountWithSymbol( - vm.totalRefundableAmount.toStringAsFixed(2).toText24( + refundRequestViewModel.totalRefundableAmount.toStringAsFixed(2).toText24( isBold: true, isEnglishOnly: true, color: AppColors.textColor, @@ -477,24 +477,24 @@ class _RefundRequestCreatePageState extends State { text: isStep2 ? LocaleKeys.submit.tr(context: context) : LocaleKeys.next.tr(context: context), onPressed: () { if (!isStep2) { - if (vm.selectedProcedures.isNotEmpty) vm.nextStep(); + if (refundRequestViewModel.selectedProcedures.isNotEmpty) refundRequestViewModel.nextStep(); } else { - final isBankTransfer = vm.selectedPaymentMethod == RefundTabType.invoices; + final isBankTransfer = refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices; if (isBankTransfer) { - final isValid = vm.validateBankTransferForm( + final isValid = refundRequestViewModel.validateBankTransferForm( iban: _ibanController.text, description: _descriptionController.text, ); if (!isValid) return; } - _handleInvoiceRefundRequestSubmit(vm); + _handleInvoiceRefundRequestSubmit(refundRequestViewModel); } }, isDisabled: isStep2 - ? (vm.selectedPaymentMethod == null || - (vm.selectedPaymentMethod == RefundTabType.invoices && + ? (refundRequestViewModel.selectedPaymentMethod == null || + (refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices && (_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty))) - : (vm.selectedProcedures.isEmpty || !vm.allSelectedProceduresHaveReasons), + : (refundRequestViewModel.selectedProcedures.isEmpty || !refundRequestViewModel.allSelectedProceduresHaveReasons), ).paddingSymmetrical(24.h, 24.h), ], ), @@ -537,14 +537,11 @@ class _RefundRequestCreatePageState extends State { // 1 = invoice refund refundProject: invoice.projectName ?? '', refundProjectId: invoice.projectId ?? 0, - - // TODO: It should be removed - refundPatientId: 4768571.toString(), + refundPatientId: patientId.toString(), refundPatientName: patientName, refundNationalId: patientNationalId, patientMobileNumber: patientMobileNumber, - refundInvoiceNo: 260000075.toString(), - // refundInvoiceNo: (invoice.invoiceNo ?? 0).toString(), TODO: It should be removed + refundInvoiceNo: (invoice.invoiceNo ?? 0).toString(), refundAppointmentNo: (invoice.appointmentNo ?? 0).toString(), refundDoctorId: invoice.doctorId ?? 0, refundDoctorName: invoice.doctorName ?? '', @@ -559,13 +556,13 @@ class _RefundRequestCreatePageState extends State { lineItemNo: p.apiLineItemNo ?? 0, procedureId: p.apiProcedureId ?? '', procedureName: p.procedureName, - patientPaidAmount: p.patientShare, + patientPaidAmount: p.patientPaidAmount, refundReason: p.refundReason ?? '', )) .toList(), ); - await vm.submitRefund( + await vm.submitInvoiceRefundRequest( request: request, onSuccess: (response) { LoaderBottomSheet.hideLoader(); @@ -597,17 +594,17 @@ class _RefundRequestCreatePageState extends State { children: [ Utils.getSuccessWidget(loadingText: LocaleKeys.requestSubmittedSuccessfully.tr(context: context)), if (refNo.isNotEmpty) ...[ + SizedBox(height: 8.h), Row( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, children: [ - LocaleKeys.hereIsYourRequestNumber.tr(context: context).toText14( + LocaleKeys.hereIsYourRequestNumber.tr().toText16( color: AppColors.textColorLight, - weight: FontWeight.w600, ), - SizedBox(width: 4.w), - refNo.toText16(isBold: true), + SizedBox(width: 6.w), + refNo.toText18(isBold: true, color: AppColors.textColor, isEnglishOnly: true), ], - ), + ).paddingSymmetrical(24.w, 0), ], SizedBox(height: 24.h), Row( @@ -619,6 +616,13 @@ class _RefundRequestCreatePageState extends State { 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, ), @@ -649,7 +653,18 @@ class _RefundRequestCreatePageState extends State { Expanded( child: CollapsingListView( title: LocaleKeys.requestRefund.tr(context: context), - requests: () => Navigator.of(context).push(CustomPageRoute(page: RefundHistoryScreen())), + 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( 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/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 a82879f6..f292755b 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -23,6 +23,8 @@ import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page. import 'package:hmg_patient_app_new/presentation/my_family/my_family.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'; @@ -108,6 +110,7 @@ class AppRoutes { 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(), @@ -176,5 +179,9 @@ class AppRoutes { 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/chip/app_custom_chip_widget.dart b/lib/widgets/chip/app_custom_chip_widget.dart index d5d96229..734ca2cc 100644 --- a/lib/widgets/chip/app_custom_chip_widget.dart +++ b/lib/widgets/chip/app_custom_chip_widget.dart @@ -57,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, diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index 7502404c..16a955c8 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -223,7 +223,7 @@ class TextInputWidget extends StatelessWidget { } Widget _buildLeadingIcon(BuildContext context) { - if (leadingIconNeedsContainer == true) { + if (leadingIconNeedsContainer) { return Container( height: 40.h, width: 40.h, -- 2.30.2 From 05730d3774143e941f3743e94b5be2c8ca4a893f Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Tue, 19 May 2026 17:56:47 +0300 Subject: [PATCH 6/6] fixed er 'pickFiles' can't be acc --- lib/core/dependencies.dart | 13 ++++++------- lib/main.dart | 8 +++----- lib/widgets/image_picker.dart | 4 ++-- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 9f8717c1..3c6e923e 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -351,20 +351,19 @@ class AppDependencies { 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( - () => ServicesPriceListViewModel(servicesPriceListRepo: getIt(), errorHandlerService: getIt())); - getIt.registerLazySingleton( - () => DateRangCalenderModel(appState: getIt(), navigationService: getIt(), dialogService: getIt())); getIt.registerLazySingleton(() => PayTabsViewModel(appState: getIt())); } diff --git a/lib/main.dart b/lib/main.dart index d1e8d102..ae7f7036 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,9 +11,6 @@ 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/active_prescriptions/active_prescriptions_view_model.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/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'; @@ -38,6 +35,7 @@ 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'; @@ -261,7 +259,6 @@ void main() async { ChangeNotifierProvider( create: (_) => getIt.get(), ), - ), ChangeNotifierProvider( create: (_) => getIt.get(), ), @@ -270,7 +267,8 @@ void main() async { ), ChangeNotifierProvider( create: (_) => getIt.get(), - ), ChangeNotifierProvider( + ), + ChangeNotifierProvider( create: (_) => getIt.get(), ), ChangeNotifierProvider( diff --git a/lib/widgets/image_picker.dart b/lib/widgets/image_picker.dart index 4d87929b..e849cc96 100644 --- a/lib/widgets/image_picker.dart +++ b/lib/widgets/image_picker.dart @@ -79,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', -- 2.30.2