Refund Request and Withdraw Module In Progress!

pull/322/head
faizatflutter 4 weeks ago
parent 8aa152f1a0
commit 198280fbde

@ -0,0 +1,7 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="10" fill="#EFEFF0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0002 9.25C19.2712 9.25 18.6011 9.43823 17.8597 9.76207C17.1407 10.0761 16.307 10.5401 15.2582 11.1238L11.1161 13.4289C10.5397 13.7497 10.0573 14.1526 9.72587 14.6959C9.39281 15.2418 9.25016 15.8673 9.25016 16.5691L9.25008 16.6352C9.24962 16.8906 9.24915 17.1497 9.31072 17.3815C9.46568 17.9648 9.83495 18.3408 10.2872 18.5422C10.6946 18.7238 11.1372 18.75 11.4694 18.75L28.5309 18.75C28.8631 18.75 29.3057 18.7238 29.7131 18.5422C30.1654 18.3408 30.5346 17.9648 30.6896 17.3815C30.7512 17.1497 30.7507 16.8906 30.7502 16.6352L30.7502 16.5691C30.7502 15.8673 30.6075 15.2418 30.2745 14.6959C29.9431 14.1526 29.4607 13.7497 28.8842 13.4289L24.7421 11.1238C23.6934 10.5401 22.8596 10.0761 22.1406 9.76206C21.3992 9.43823 20.7292 9.25 20.0002 9.25ZM19.9961 14C19.4438 14 18.9961 14.4477 18.9961 15C18.9961 15.5523 19.4438 16 19.9961 16H20.0051C20.5574 16 21.0051 15.5523 21.0051 15C21.0051 14.4477 20.5574 14 20.0051 14H19.9961Z" fill="#8F9AA3"/>
<path d="M9.25 29.75C9.25 28.0931 10.9289 26.75 13 26.75L27 26.75C29.0711 26.75 30.75 28.0931 30.75 29.75C30.75 30.3023 30.1904 30.75 29.5 30.75L10.5 30.75C9.80964 30.75 9.25 30.3023 9.25 29.75Z" fill="#8F9AA3"/>
<path d="M16.3501 25.5H13C12.5865 25.5 12.1848 25.5502 11.8006 25.6448C11.5358 25.71 11.4034 25.7426 11.3268 25.6825C11.2501 25.6224 11.2501 25.4944 11.2501 25.2383V20.216C11.2501 20.2021 11.2501 20.1952 11.2504 20.1904C11.2554 20.0851 11.3352 20.0053 11.4405 20.0002C11.4453 20 11.4534 20 11.4696 20H16.3501C16.5387 20 16.633 20 16.6915 20.0586C16.7501 20.1172 16.7501 20.2114 16.7501 20.4V25.1C16.7501 25.2886 16.7501 25.3828 16.6915 25.4414C16.633 25.5 16.5387 25.5 16.3501 25.5Z" fill="#8F9AA3"/>
<path d="M28.1996 25.6448C27.8153 25.5502 27.4135 25.5 27 25.5H23.6501C23.4616 25.5 23.3673 25.5 23.3087 25.4414C23.2501 25.3828 23.2501 25.2886 23.2501 25.1V20.4C23.2501 20.2114 23.2501 20.1172 23.3087 20.0586C23.3673 20 23.4616 20 23.6501 20L28.531 20C28.5469 20 28.5548 20 28.5595 20.0002C28.665 20.0052 28.745 20.0851 28.7499 20.1906L28.7501 20.1994L28.7501 20.2157V25.2384C28.7501 25.4945 28.7501 25.6226 28.6735 25.6826C28.5968 25.7427 28.4644 25.7101 28.1996 25.6448Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -0,0 +1,7 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="10" fill="#EFEFF0"/>
<path d="M11 10C10.4477 10 10 10.4477 10 11C10 11.5523 10.4477 12 11 12L19 12C19.5523 12 20 11.5523 20 11C20 10.4477 19.5523 10 19 10L11 10Z" fill="#8F9AA3"/>
<path d="M11 16C10.4477 16 10 16.4477 10 17C10 17.5523 10.4477 18 11 18H19C19.5523 18 20 17.5523 20 17C20 16.4477 19.5523 16 19 16L11 16Z" fill="#8F9AA3"/>
<path d="M10 23C10 22.4477 10.4477 22 11 22L29 22C29.5523 22 30 22.4477 30 23C30 23.5523 29.5523 24 29 24L11 24C10.4477 24 10 23.5523 10 23Z" fill="#8F9AA3"/>
<path d="M11 28C10.4477 28 10 28.4477 10 29C10 29.5523 10.4477 30 11 30L29 30C29.5523 30 30 29.5523 30 29C30 28.4477 29.5523 28 29 28L11 28Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 790 B

@ -1,3 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.5 7.5C18.5 8.60457 17.6046 9.5 16.5 9.5C15.3954 9.5 14.5 8.60457 14.5 7.5C14.5 6.39543 15.3954 5.5 16.5 5.5C17.6046 5.5 18.5 6.39543 18.5 7.5Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9255 1.5H12.0745C14.2504 1.49998 15.9852 1.49996 17.3453 1.68282C18.7497 1.87164 19.9035 2.27175 20.8159 3.18414C21.7283 4.09653 22.1284 5.25033 22.3172 6.65471C22.5 8.01485 22.5 9.74959 22.5 11.9256V12.0744C22.5 14.2504 22.5 15.9851 22.3172 17.3453C22.1284 18.7497 21.7283 19.9035 20.8159 20.8159C19.9035 21.7283 18.7497 22.1284 17.3453 22.3172C15.9851 22.5 14.2504 22.5 12.0744 22.5H11.9256C9.74959 22.5 8.01485 22.5 6.65471 22.3172C5.25033 22.1284 4.09653 21.7283 3.18414 20.8159C2.27175 19.9035 1.87164 18.7497 1.68282 17.3453C1.49996 15.9852 1.49998 14.2504 1.5 12.0745V11.9255C1.49998 9.74958 1.49996 8.01484 1.68282 6.65471C1.87164 5.25033 2.27175 4.09653 3.18414 3.18414C4.09653 2.27175 5.25033 1.87164 6.65471 1.68282C8.01485 1.49996 9.74958 1.49998 11.9255 1.5ZM6.92121 3.66499C5.73861 3.82399 5.0772 4.1195 4.59835 4.59835C4.1195 5.0772 3.82399 5.73861 3.66499 6.92121C3.50213 8.13258 3.5 9.73256 3.5 12C3.5 12.2688 3.50003 12.5282 3.50036 12.7786C3.78795 12.7608 4.07624 12.7521 4.36474 12.7527C7.17775 12.6952 9.92747 13.572 12.124 15.2364C13.9124 16.5915 15.2416 18.3991 15.9596 20.4377C16.3674 20.4138 16.7385 20.3808 17.0788 20.335C17.2221 20.3157 17.3578 20.2945 17.4864 20.2711C16.9795 18.6882 16.1544 17.2258 15.0687 15.9582C14.8393 15.6904 14.9291 15.27 15.2648 15.1622C16.122 14.887 16.9998 14.7468 17.8878 14.7501C18.7607 14.7494 19.6259 14.889 20.4723 15.162C20.4994 14.2767 20.5 13.2378 20.5 12C20.5 9.73256 20.4979 8.13258 20.335 6.92121C20.176 5.73861 19.8805 5.0772 19.4017 4.59835C18.9228 4.1195 18.2614 3.82399 17.0788 3.66499C15.8674 3.50213 14.2674 3.5 12 3.5C9.73256 3.5 8.13258 3.50213 6.92121 3.66499Z" fill="white"/>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -0,0 +1,4 @@
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.93712 2.78621e-06C4.05309 -5.24077e-05 3.28757 -0.00010021 2.67381 0.0856193C2.01162 0.178101 1.38072 0.387107 0.873027 0.91449C0.370475 1.43654 0.176225 2.07619 0.0895854 2.74561C0.00806761 3.37545 0.00810474 4.16382 0.00814813 5.08738L0.00815016 5.15177C0.00815016 5.40552 0.00591272 5.64931 0.00378245 5.88142C-0.00173354 6.48244 -0.00653088 7.00515 0.0300965 7.41981C0.0870866 8.06499 0.254658 8.66564 0.762836 9.19353C1.14586 9.59142 1.77891 9.60346 2.17679 9.22044C2.57468 8.83741 2.58672 8.20436 2.2037 7.80648C2.12657 7.72636 2.05658 7.63147 2.02234 7.24383C1.99474 6.93135 1.99819 6.55852 2.00318 6.02087C2.0055 5.77045 2.00815 5.48426 2.00815 5.15177C2.00815 4.14595 2.01012 3.48847 2.07304 3.00232C2.13273 2.54115 2.23113 2.38751 2.31389 2.30154C2.39151 2.22092 2.52427 2.12592 2.95045 2.06639C3.40942 2.00229 4.0335 2.00001 5.00488 2.00001L16.9951 2.00001C17.9664 2.00001 18.5905 2.00229 19.0495 2.0664C19.4757 2.12592 19.6084 2.22092 19.686 2.30155C19.7688 2.38751 19.8672 2.54115 19.9269 3.00232C19.9898 3.48847 19.9918 4.14595 19.9918 5.15177C19.9918 5.48233 19.9944 5.76713 19.9967 6.01653C20.0016 6.55612 20.005 6.93046 19.9773 7.2435C19.9429 7.63104 19.8729 7.72616 19.7956 7.80648C19.4125 8.20436 19.4246 8.83741 19.8225 9.22044C20.2203 9.60346 20.8534 9.59142 21.2364 9.19353C21.7444 8.66584 21.9123 8.06542 21.9695 7.42014C22.0063 7.00455 22.0016 6.48073 21.9961 5.87825C21.994 5.64711 21.9918 5.40439 21.9918 5.15177L21.9918 5.08739C21.9918 4.16387 21.9919 3.37543 21.9103 2.74561C21.8237 2.07619 21.6295 1.43654 21.1269 0.914491C20.6192 0.387108 19.9883 0.178102 19.3261 0.0856201C18.7124 -9.94951e-05 17.9468 -5.18116e-05 17.0628 3.38225e-06L4.93712 2.78621e-06Z" fill="#0B85F7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.1978 4C13.9586 3.99998 14.598 3.99996 15.1143 4.06403C15.6617 4.13197 16.1602 4.2806 16.5912 4.64146C17.0222 5.00232 17.2562 5.46697 17.4193 5.99389C17.5731 6.49084 17.6855 7.1203 17.8192 7.8693L18.4895 11.6226C18.7725 13.2078 18.9993 14.4775 19.0348 15.4829C19.0715 16.5216 18.9118 17.4176 18.2925 18.1573C17.6731 18.8971 16.8191 19.2118 15.7902 19.3582C14.7942 19.5 13.5044 19.5 11.8942 19.5H10.1065C8.49624 19.5 7.20645 19.5 6.21049 19.3582C5.18153 19.2118 4.32758 18.8971 3.70822 18.1573C3.08886 17.4176 2.92921 16.5216 2.96588 15.4829C3.00137 14.4775 3.22812 13.2078 3.51121 11.6227L4.18145 7.86934C4.31517 7.12034 4.42756 6.49084 4.58139 5.99389C4.74449 5.46697 4.97845 5.00232 5.40946 4.64146C5.84047 4.2806 6.339 4.13197 6.8864 4.06403C7.40264 3.99996 8.04206 3.99998 8.80289 4L13.1978 4ZM11 7.5C11.4142 7.5 11.75 7.83579 11.75 8.25L11.75 13.4393L12.9697 12.2197C13.2626 11.9268 13.7374 11.9268 14.0303 12.2197C14.3232 12.5126 14.3232 12.9874 14.0303 13.2803L11.5303 15.7803C11.2374 16.0732 10.7626 16.0732 10.4697 15.7803L7.96967 13.2803C7.67678 12.9874 7.67678 12.5126 7.96967 12.2197C8.26256 11.9268 8.73744 11.9268 9.03033 12.2197L10.25 13.4393L10.25 8.25C10.25 7.83579 10.5858 7.5 11 7.5Z" fill="#0B85F7"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="10" fill="#EFEFF0"/>
<path d="M26.5 15.5C26.5 16.6046 25.6046 17.5 24.5 17.5C23.3954 17.5 22.5 16.6046 22.5 15.5C22.5 14.3954 23.3954 13.5 24.5 13.5C25.6046 13.5 26.5 14.3954 26.5 15.5Z" fill="#8F9AA3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.9255 9.5H20.0745C22.2504 9.49998 23.9852 9.49996 25.3453 9.68282C26.7497 9.87164 27.9035 10.2717 28.8159 11.1841C29.7283 12.0965 30.1284 13.2503 30.3172 14.6547C30.5 16.0149 30.5 17.7496 30.5 19.9256V20.0744C30.5 22.2504 30.5 23.9851 30.3172 25.3453C30.1284 26.7497 29.7283 27.9035 28.8159 28.8159C27.9035 29.7283 26.7497 30.1284 25.3453 30.3172C23.9851 30.5 22.2504 30.5 20.0744 30.5H19.9256C17.7496 30.5 16.0149 30.5 14.6547 30.3172C13.2503 30.1284 12.0965 29.7283 11.1841 28.8159C10.2717 27.9035 9.87164 26.7497 9.68282 25.3453C9.49996 23.9852 9.49998 22.2504 9.5 20.0745V19.9255C9.49998 17.7496 9.49996 16.0148 9.68282 14.6547C9.87164 13.2503 10.2717 12.0965 11.1841 11.1841C12.0965 10.2717 13.2503 9.87164 14.6547 9.68282C16.0148 9.49996 17.7496 9.49998 19.9255 9.5ZM14.9212 11.665C13.7386 11.824 13.0772 12.1195 12.5984 12.5984C12.1195 13.0772 11.824 13.7386 11.665 14.9212C11.5021 16.1326 11.5 17.7326 11.5 20C11.5 20.2688 11.5 20.5282 11.5004 20.7786C11.7879 20.7608 12.0762 20.7521 12.3647 20.7527C15.1778 20.6952 17.9275 21.572 20.124 23.2364C21.9124 24.5915 23.2416 26.3991 23.9596 28.4377C24.3674 28.4138 24.7385 28.3808 25.0788 28.335C25.2221 28.3157 25.3578 28.2945 25.4864 28.2711C24.9795 26.6882 24.1544 25.2258 23.0687 23.9582C22.8393 23.6904 22.9291 23.27 23.2648 23.1622C24.122 22.887 24.9998 22.7468 25.8878 22.7501C26.7607 22.7494 27.6259 22.889 28.4723 23.162C28.4994 22.2767 28.5 21.2378 28.5 20C28.5 17.7326 28.4979 16.1326 28.335 14.9212C28.176 13.7386 27.8805 13.0772 27.4017 12.5984C26.9228 12.1195 26.2614 11.824 25.0788 11.665C23.8674 11.5021 22.2674 11.5 20 11.5C17.7326 11.5 16.1326 11.5021 14.9212 11.665Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@ -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": "اختر العمليات"
}

@ -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"
}

@ -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<AppLifecycleService>());
_httpClient = httpClient ?? HttpClientManager(lifecycleService: getIt<AppLifecycleService>());
@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));

@ -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";

@ -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';

@ -209,7 +209,7 @@ class AppDependencies {
);
getIt.registerLazySingleton<HabibWalletViewModel>(
() => HabibWalletViewModel(habibWalletRepo: getIt(), errorHandlerService: getIt()),
() => HabibWalletViewModel(habibWalletRepo: getIt(), errorHandlerService: getIt(), refundRequestRepo: getIt()),
);
getIt.registerLazySingleton<MedicalFileViewModel>(

@ -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<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),
fontFamily: fontFamily ??
(isEnglishOnly
? "Poppins"
: getIt.get<AppState>().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<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),
fontFamily: fontFamily ??
(isEnglishOnly
? "Poppins"
: getIt.get<AppState>().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<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),
style: TextStyle(
color: color ?? AppColors.blackColor,
fontSize: 17.f,
letterSpacing: -1,
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
fontFamily: getIt.get<AppState>().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<AppState>().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<AppState>().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<AppState>().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<AppState>().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<AppState>().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<AppState>().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<AppState>().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<AppState>().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<AppState>().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<AppState>().getLanguageCode() == "ar" ? 1.2 : 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.f, letterSpacing: -1, fontFamily: getIt.get<AppState>().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins', fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal,),
height: getIt.get<AppState>().getLanguageCode() == "ar" ? 1.2 : 32 / 32,
color: color ?? AppColors.blackColor,
fontSize: 32.f,
letterSpacing: -1,
fontFamily: getIt.get<AppState>().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<AppState>().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<AppState>().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() {

@ -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<void> 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<void> addAdvanceNumberRequest({required String advanceNumber, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async {
Future<void> addAdvanceNumberRequest(
{required String advanceNumber, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await habibWalletRepo.addAdvanceNumberRequest(advanceNumber: advanceNumber, paymentReference: paymentReference);
result.fold(

@ -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<String, dynamic> 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<String, dynamic> 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,
};
}

@ -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<String, dynamic> 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<SubmitRefundProcedureItem> 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<String, dynamic> 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}';
}
}

@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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?,
);
}
}

@ -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<String, dynamic>))
.toList()
? (json['RefundableInvoiceItems'] as List).map((item) => RefundableInvoiceItem.fromJson(item as Map<String, dynamic>)).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<String, dynamic> 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<RefundableInvoiceDetailsItem> refundableInvoiceDetailsItems;
final int? statusCode;
RefundableInvoicesDetailsObject({
this.errorCode,
this.invoiceNo,
this.message,
this.projectId,
this.refundableInvoiceDetailsItems = const [],
this.statusCode,
});
factory RefundableInvoicesDetailsObject.fromJson(Map<String, dynamic> 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<String, dynamic>))
.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<String, dynamic> 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<RefundableAdvancesItem> refundableAdvancesItems;
final int? statusCode;
RefundableAdvancesObject({
this.errorCode,
this.message,
this.refundableAdvancesItems = const [],
this.statusCode,
});
factory RefundableAdvancesObject.fromJson(Map<String, dynamic> 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<String, dynamic>)).toList()
: [],
statusCode: json['StatusCode'] as int?,
);
}
}

@ -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<Either<Failure, GenericApiModel<List<RefundableInvoiceItem>>>> getRefundableInvoices({required int projectID});
Future<Either<Failure, GenericApiModel<RefundableInvoicesDetailsObject>>> getRefundableInvoicesDetails({
required int invoiceNo,
required int projectId,
});
Future<Either<Failure, GenericApiModel<RefundableAdvancesObject>>> getRefundableAdvances();
Future<Either<Failure, GenericApiModel<RefundStatusModel>>> getRefundStatus({required String referenceNumber});
Future<Either<Failure, GenericApiModel<List<RefundInvoiceModel>>>> getRefundInvoices({required String patientId, String invoiceNumberQuery});
Future<Either<Failure, GenericApiModel<SubmitRefundResponseModel>>> submitRefund({required SubmitRefundRequestModel request});
}
class RefundRequestRepoImp implements RefundRequestRepo {
@ -18,11 +33,7 @@ class RefundRequestRepoImp implements RefundRequestRepo {
@override
Future<Either<Failure, GenericApiModel<List<RefundableInvoiceItem>>>> getRefundableInvoices({required int projectID}) async {
// ApiClient auto-injects: PatientID, TokenID, LanguageID, PatientOutSA,
// PatientTypeID, IPAdress, generalid, VersionID, Channel
Map<String, dynamic> body = {
"ProjectID": projectID,
};
Map<String, dynamic> body = {"ProjectID": projectID};
try {
GenericApiModel<List<RefundableInvoiceItem>>? apiResponse;
@ -69,4 +80,221 @@ class RefundRequestRepoImp implements RefundRequestRepo {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<RefundableInvoicesDetailsObject>>> getRefundableInvoicesDetails({
required int invoiceNo,
required int projectId,
}) async {
final Map<String, dynamic> body = {
"InvoiceNo": invoiceNo,
"ProjectID": projectId,
};
try {
GenericApiModel<RefundableInvoicesDetailsObject>? 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<String, dynamic>)
: RefundableInvoicesDetailsObject();
apiResponse = GenericApiModel<RefundableInvoicesDetailsObject>(
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<Either<Failure, GenericApiModel<RefundableAdvancesObject>>> getRefundableAdvances() async {
try {
GenericApiModel<RefundableAdvancesObject>? 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<String, dynamic>) : RefundableAdvancesObject();
apiResponse = GenericApiModel<RefundableAdvancesObject>(
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<Either<Failure, GenericApiModel<RefundStatusModel>>> getRefundStatus({required String referenceNumber}) async {
final Map<String, dynamic> body = {
"Refund_ReferenceNumber": referenceNumber,
};
try {
GenericApiModel<RefundStatusModel>? 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<String, dynamic>) : RefundStatusModel();
apiResponse = GenericApiModel<RefundStatusModel>(
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<Either<Failure, GenericApiModel<List<RefundInvoiceModel>>>> getRefundInvoices({
required String patientId,
String invoiceNumberQuery = "",
}) async {
final Map<String, dynamic> body = {
"Refund_PatientId": patientId,
"Refund_InvoiceNumberQuery": invoiceNumberQuery,
};
try {
GenericApiModel<List<RefundInvoiceModel>>? 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<RefundInvoiceModel> invoices =
listJson != null ? (listJson as List).map((item) => RefundInvoiceModel.fromJson(item as Map<String, dynamic>)).toList() : [];
apiResponse = GenericApiModel<List<RefundInvoiceModel>>(
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<Either<Failure, GenericApiModel<SubmitRefundResponseModel>>> submitRefund({required SubmitRefundRequestModel request}) async {
try {
GenericApiModel<SubmitRefundResponseModel>? 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<String, dynamic>);
apiResponse = GenericApiModel<SubmitRefundResponseModel>(
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()));
}
}
}

@ -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<RefundableProcedure> 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<void> 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<void> 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<void> 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<RefundInvoiceModel> refundInvoicesList = [];
bool isRefundInvoicesLoading = false;
Future<void> getRefundInvoices({
String? patientId,
String invoiceNumberQuery = "",
Function(List<RefundInvoiceModel>)? 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<void> 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");
}
}
},
);
}
}

@ -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';
}

@ -376,7 +376,7 @@ class _ComprehensiveCheckupPageState extends State<ComprehensiveCheckupPage> {
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),

@ -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<HabibWalletPage> {
@override
Widget build(BuildContext context) {
habibWalletVM = Provider.of<HabibWalletViewModel>(context, listen: false);
AppState _appState = getIt.get<AppState>();
AppState appState = getIt.get<AppState>();
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
@ -56,11 +57,10 @@ class _HabibWalletState extends State<HabibWalletPage> {
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<HabibWalletPage> {
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<HabibWalletPage> {
LocaleKeys.balanceAmount.tr(context: context).toText14(isBold: true, color: Colors.white),
SizedBox(height: 4.h),
Consumer<HabibWalletViewModel>(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<HabibWalletPage> {
},
),
),
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<HabibWalletPage> {
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);
},

@ -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<WithdrawRequestCreatePage> createState() => _WithdrawRequestCreatePageState();
}
class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
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<AppState>();
_habibWalletViewModel = context.read<HabibWalletViewModel>();
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<HabibWalletViewModel>(
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<HabibWalletViewModel>(
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<HabibWalletViewModel>(
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(),
],
),
);
},
);
}
}

@ -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<ServicesPage> {
HmgServicesComponentModel(
5, LocaleKeys.completeCheckup.tr(), "", AppAssets.comprehensiveCheckup, bgColor: AppColors.bgGreenColor, true, route: null, onTap: () async {
if (getIt.get<AppState>().isAuthenticated) {
getIt.get<NavigationService>().pushPageRoute(AppRoutes.comprehensiveCheckupPage);
Navigator.of(getIt<NavigationService>().navigatorKey.currentContext!).push(CustomPageRoute(page: ComprehensiveCheckupPage()));
} else {
Navigator.of(getIt<NavigationService>().navigatorKey.currentContext!).push(
CustomPageRoute(

@ -147,11 +147,14 @@ class _LandingPageState extends State<LandingPage> {
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<LandingPage> {
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<LandingPage> {
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,

@ -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<MedicalFilePage> {
).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,

@ -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<AppState>();
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<AppState>().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,
),
),
]
],
),
],
],
),
),

@ -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<RefundHistoryScreen> createState() => _RefundHistoryScreenState();
}
class _RefundHistoryScreenState extends State<RefundHistoryScreen> {
late AppState _appState;
@override
void initState() {
super.initState();
_appState = getIt<AppState>();
final vm = context.read<RefundRequestViewModel>();
scheduleMicrotask(() async {
await vm.getRefundInvoices();
});
}
// Helpers <EFBFBD><EFBFBD><EFBFBD>
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<RefundRequestViewModel>(
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),
),
);
}
}

@ -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<RefundRequestCreatePage> createState() => _RefundRequestCreatePageState();
}
class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
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<RefundableProcedure> _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<AppState>();
WidgetsBinding.instance.addPostFrameCallback((_) {
// Load hospital list the same way RechargeWalletPage does
_habibWalletVM.getProjectsList();
_refundRequestViewModel = context.read<RefundRequestViewModel>();
_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<RefundRequestCreatePage> {
itemWidth,
isActive ? AppColors.primaryRedColor : AppColors.greyColor,
isThumb,
4.h,
6.h,
);
}),
);
@ -82,63 +127,211 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
// Step 1 exactly mirrors RechargeWalletPage hospital row
Widget _buildStep1Content(RefundRequestViewModel vm) {
final isArabic = _appState.isArabic();
return Consumer<RefundRequestViewModel>(
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<RefundTabType>(
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<RefundRequestCreatePage> {
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<RefundRequestCreatePage> {
);
}
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<RefundRequestCreatePage> {
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<RefundRequestCreatePage> {
),
],
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<RefundRequestCreatePage> {
// 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>();
_habibWalletViewModel = context.read<HabibWalletViewModel>();
return Consumer<RefundRequestViewModel>(
builder: (context, vm, _) {
_vm = vm;
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: Column(
@ -369,38 +649,18 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
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<RefundRequestCreatePage> {
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),
),
),
),
],
),

@ -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<String> 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<String> onDone;
const _RefundReasonSheetContent({
required this.selectedReason,
required this.onDone,
});
@override
State<_RefundReasonSheetContent> createState() => _RefundReasonSheetContentState();
}
class _RefundReasonSheetContentState extends State<_RefundReasonSheetContent> {
String? _selected;
static List<String> _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<String>(
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),
],
);
}
}

@ -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<String, WidgetBuilder> 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();
},
};
}

@ -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,

@ -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,
),

@ -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<bool> Function()? checkCameraPermission,
Future<bool> 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(

@ -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<Country> 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,

Loading…
Cancel
Save