From 198280fbdea120c758ee022e4e6031f31f3fe938 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Wed, 13 May 2026 15:59:33 +0300 Subject: [PATCH] Refund Request and Withdraw Module In Progress! --- assets/images/svg/bank_icon.svg | 7 + assets/images/svg/description_icon.svg | 7 + assets/images/svg/image_icon.svg | 1 + assets/images/svg/refund_icon.svg | 4 + assets/images/svg/select_image_icon.svg | 5 + assets/langs/ar-SA.json | 36 +- assets/langs/en-US.json | 38 +- lib/core/api/api_client.dart | 14 +- lib/core/api_consts.dart | 5 + lib/core/app_assets.dart | 26 +- lib/core/dependencies.dart | 2 +- lib/extensions/string_extensions.dart | 215 +++++- .../habib_wallet/habib_wallet_view_model.dart | 95 ++- .../get_invoices_list_response_model.dart | 84 +- .../submit_refund_request_model.dart | 98 +++ .../refund_request_list_response_model.dart | 91 +++ .../refundable_invoices_response_model.dart | 119 ++- .../refund_request/refund_request_repo.dart | 238 +++++- .../refund_request_view_model.dart | 346 ++++++++- lib/generated/locale_keys.g.dart | 27 + .../comprehensive_checkup_page.dart | 2 +- .../habib_wallet/habib_wallet_page.dart | 62 +- .../withdraw_request_create_page.dart | 325 ++++++++ .../hmg_services/services_page.dart | 3 +- lib/presentation/home/landing_page.dart | 19 +- .../medical_file/medical_file_page.dart | 29 +- .../widgets/invoice_list_card.dart | 101 ++- .../refund_request/refund_history_screen.dart | 258 +++++++ .../refund_request_create_page.dart | 721 ++++++++++++------ .../widgets/refund_reason_bottom_sheet.dart | 111 +++ lib/routes/app_routes.dart | 11 +- lib/widgets/buttons/custom_button.dart | 13 +- lib/widgets/chip/app_custom_chip_widget.dart | 58 +- lib/widgets/image_picker.dart | 14 +- lib/widgets/input_widget.dart | 112 +-- 35 files changed, 2794 insertions(+), 503 deletions(-) create mode 100644 assets/images/svg/bank_icon.svg create mode 100644 assets/images/svg/description_icon.svg create mode 100644 assets/images/svg/refund_icon.svg create mode 100644 assets/images/svg/select_image_icon.svg create mode 100644 lib/presentation/habib_wallet/withdraw_request_create_page.dart create mode 100644 lib/presentation/refund_request/refund_history_screen.dart create mode 100644 lib/presentation/refund_request/widgets/refund_reason_bottom_sheet.dart diff --git a/assets/images/svg/bank_icon.svg b/assets/images/svg/bank_icon.svg new file mode 100644 index 00000000..5c5df589 --- /dev/null +++ b/assets/images/svg/bank_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/svg/description_icon.svg b/assets/images/svg/description_icon.svg new file mode 100644 index 00000000..d3f17c33 --- /dev/null +++ b/assets/images/svg/description_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/svg/image_icon.svg b/assets/images/svg/image_icon.svg index 73945847..779f1e93 100644 --- a/assets/images/svg/image_icon.svg +++ b/assets/images/svg/image_icon.svg @@ -1,3 +1,4 @@ + diff --git a/assets/images/svg/refund_icon.svg b/assets/images/svg/refund_icon.svg new file mode 100644 index 00000000..f0eabc53 --- /dev/null +++ b/assets/images/svg/refund_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/select_image_icon.svg b/assets/images/svg/select_image_icon.svg new file mode 100644 index 00000000..df9ef185 --- /dev/null +++ b/assets/images/svg/select_image_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 6ba4a959..6a0dedf5 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1158,6 +1158,7 @@ "transportOptions": "خيارات النقل", "selectHospitalForAdvancePayment": "يرجى تحديد المستشفى أولاً.", "recharge": "شحن المحفظة", + "withdraw": "سحب", "activityLevel": "مستوى النشاط", "selectActivityLevel": "حدد مستوى النشاط", "almostInactive": "غير نشط تقريبًا (بدون تمارين)", @@ -1846,5 +1847,38 @@ "fileSizeExceedsLimit": "يجب ألا يتجاوز حجم الملف 1 ميغابايت", "completedPrescriptionOrder": "تمت الخدمة", "submitEReferral": "تقديم الطلب", - "requestRefund": "طلب استرداد" + "requestRefund": "طلب استرداد", + "uploadImage": "رفع صورة", + "selectImage": "اختر صورة", + "selectOptions": "اختر الخيارات", + "iban": "رقم الآيبان", + "addIbanDetails": "أضف تفاصيل الآيبان", + "description": "الوصف", + "enterDescription": "أدخل الوصف", + "submitRequest": "تقديم الطلب", + "ibanIsRequired": "حقل رقم الآيبان مطلوب", + "descriptionIsRequired": "الوصف مطلوب", + "invalidIbanFormat": "صيغة الآيبان غير صحيحة. يجب أن تبدأ بـ SA يليها 20 رقمًا (مثال: SA0380000000608010167519)", + "refundHistory": "سجل الاسترداد", + "noRefundHistoryFound": "لا يوجد سجل استرداد حتى الآن.", + "invoiceNo": "رقم الفاتورة", + "totalAmount": "المبلغ الإجمالي", + "patientShare": "نصيب المريض", + "selectProcedure": "حدد العمليات", + "noProceduresAvailableForRefund": "لا توجد عمليات متاحة للاسترداد", + "selectAll": "تحديد الكل", + "selectRefundMethod": "اختر طريقة الاسترداد", + "hmgWallet": "محفظة HMG", + "bankTransfer": "تحويل بنكي", + "selectRefundReason": "اختر سبب الاسترداد", + "refundReason": "سبب الاسترداد", + "done": "تم", + "refundReason1": "تم معالجة مريض الشركة كمدفوع ذاتي وإعادة فوترته", + "refundReason2": "المريض لا يريد إجراء سريرياً", + "refundReason3": "المريض لم ينتظر الاستشارة / يريد طبيباً آخر", + "refundReason4": "المريض أوقف الاشتراك في الباقة", + "refundReason5": "الخدمة لم تُنجز / غير متاحة بسبب عطل الجهاز / رفض المريض", + "refundReason6": "تم إلغاء الإجراء السريري أو تغييره من قِبل الطبيب المحيل", + "refundReason7": "تم إلغاء إجراء وحدة الخدمة / المختبر / الأشعة أو تغييره من قِبل الطبيب المحيل", + "selectProcedures": "اختر العمليات" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 312a8121..5d5fe9a8 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1149,6 +1149,7 @@ "transportOptions": "Transport Options", "selectHospitalForAdvancePayment": "Please select the hospital you want to make an advance payment for.", "recharge": "Add Funds", + "withdraw": "Withdraw", "activityLevel": "Activity Level", "selectActivityLevel": "Select Activity Level", "almostInactive": "Almost Inactive (no exercise)", @@ -1262,6 +1263,8 @@ "selectLocationPrescriptionDelivery": "Please select the location for prescription delivery", "noRadiologyOrders": "No Radiology Orders Found", "ageIsRequired": "Age is required", + "ibanIsRequired": "IBAN field is required", + "descriptionIsRequired": "Description is required", "invalidAge": "Invalid age", "ageMustBeBetween11And120": "Age must be between 11 and 120", "heightIsRequired": "Height is required", @@ -1836,7 +1839,40 @@ "fileSizeExceedsLimit": "File size must not exceed 1 MB", "completedPrescriptionOrder": "Completed", "submitEReferral": "Submit", - "requestRefund": "Request Refund" + "requestRefund": "Request Refund", + "uploadImage": "Upload Image", + "selectImage": "Select Image", + "selectOptions": "Select Options", + "iban": "IBAN", + "addIbanDetails": "Add IBAN Details", + "description": "Description", + "enterDescription": "Enter Description", + "submitRequest": "Submit Request", + "ibanIsRequired": "IBAN is required", + "descriptionIsRequired": "Description is required", + "invalidIbanFormat": "Invalid IBAN. Must start with SA followed by 20 digits (e.g. SA0380000000608010167519)", + "refundHistory": "Refund History", + "noRefundHistoryFound": "You don't have any refund history yet.", + "invoiceNo": "Invoice No.", + "totalAmount": "Total Amount", + "patientShare": "Patient Share", + "selectProcedure": "Select Procedure", + "noProceduresAvailableForRefund": "No procedures available for refund", + "selectAll": "Select all", + "selectRefundMethod": "Select Refund Method", + "hmgWallet": "HMG Wallet", + "bankTransfer": "Bank Transfer", + "selectRefundReason": "Select Refund Reason", + "refundReason": "Refund Reason", + "done": "Done", + "refundReason1": "Company patient treated as self-paying and re-invoiced", + "refundReason2": "Patient does not want a clinical procedure", + "refundReason3": "Patient does not wait for consultation/wants another doctor", + "refundReason4": "Patient discontinued the package", + "refundReason5": "Service not done/not available due to machine breakdown/patient refusal", + "refundReason6": "Clinical procedure cancelled or changed by requesting doctor", + "refundReason7": "Service unit/lab/radiology procedure cancelled or changed by requesting doctor", + "selectProcedures": "Select Procedures" } diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 64074ff8..8b1e288f 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:developer'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; @@ -95,8 +96,7 @@ class ApiClientImp implements ApiClient { required AppState appState, HttpClientManager? httpClient, }) : _appState = appState, - _httpClient = httpClient ?? - HttpClientManager(lifecycleService: getIt()); + _httpClient = httpClient ?? HttpClientManager(lifecycleService: getIt()); @override post( @@ -208,8 +208,9 @@ class ApiClientImp implements ApiClient { body['TokenID'] = "@dm!n"; } - // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 1018977; + // TODO: REMOVE THIS MUST + body['TokenID'] = "@dm!n"; + body['PatientID'] = 4768571; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; // body['SessionID'] = "45786230487560q"; @@ -244,7 +245,8 @@ class ApiClientImp implements ApiClient { final dynamic requestBody = isBodyPlainText ? body : json.encode(body); debugPrint("uri: ${Uri.parse(url.trim())}"); var requestBodyJSON = json.encode(body); - debugPrint("body: $requestBodyJSON", wrapWidth: 2048); + // debugPrint("body: $requestBodyJSON", wrapWidth: 2048); + log("body: $requestBodyJSON"); http.Response response; try { @@ -253,7 +255,7 @@ class ApiClientImp implements ApiClient { body: requestBody, headers: headers, ); - // debugPrint("response: ${response.body}", wrapWidth: 2048); + log("response: ${response.body.toString()}"); } on SocketException catch (e) { final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr(); onFailure(message, -1, failureType: ConnectivityFailure(message)); diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 15541878..0d615789 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -271,6 +271,11 @@ class ApiConsts { //Refund Request static String getRefundableInvoices = "Services/Outps.svc/REST/getRefundableInvoices"; + static String getRefundableInvoicesDetails = "Services/Outps.svc/REST/getRefundableInvoicesDetails"; + static String getRefundableAdvances = "Services/Outps.svc/REST/getRefundableAdvances"; + static String getRefundStatus = "Services/COCWS.svc/REST/GetRefundStatus"; + static String getRefundInvoices = "Services/COCWS.svc/REST/GetRefundInvoices"; + static String submitRefund = "Services/COCWS.svc/REST/SubmitRefund"; static String getAiAnalysisURL = "https://api.labpredict.com"; static String getAiRadiologyAnalysis = "$getAiAnalysisURL/radiology"; diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 2612bde9..813ec01c 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -257,16 +257,15 @@ class AppAssets { //smartwatch static const String watchActivity = '$svgBasePath/watch_activity.svg'; static const String watchActivityTrailing = '$svgBasePath/watch_activity_trailing.svg'; - static const String watchSteps= '$svgBasePath/watch_steps.svg'; - static const String watchStepsTrailing= '$svgBasePath/watch_steps_trailing.svg'; - static const String watchSleep= '$svgBasePath/watch_sleep.svg'; - static const String watchSleepTrailing= '$svgBasePath/watch_sleep_trailing.svg'; - static const String watchBmi= '$svgBasePath/watch_bmi.svg'; - static const String watchBmiTrailing= '$svgBasePath/watch_bmi_trailing.svg'; - static const String watchWeight= '$svgBasePath/watch_weight.svg'; - static const String watchWeightTrailing= '$svgBasePath/watch_weight_trailing.svg'; - static const String watchHeight= '$svgBasePath/watch_height.svg'; - + static const String watchSteps = '$svgBasePath/watch_steps.svg'; + static const String watchStepsTrailing = '$svgBasePath/watch_steps_trailing.svg'; + static const String watchSleep = '$svgBasePath/watch_sleep.svg'; + static const String watchSleepTrailing = '$svgBasePath/watch_sleep_trailing.svg'; + static const String watchBmi = '$svgBasePath/watch_bmi.svg'; + static const String watchBmiTrailing = '$svgBasePath/watch_bmi_trailing.svg'; + static const String watchWeight = '$svgBasePath/watch_weight.svg'; + static const String watchWeightTrailing = '$svgBasePath/watch_weight_trailing.svg'; + static const String watchHeight = '$svgBasePath/watch_height.svg'; //bottom navigation// static const String homeBottom = '$svgBasePath/home_bottom.svg'; @@ -386,6 +385,11 @@ class AppAssets { static const String bmicalc = '$svgBasePath/bmi1.svg'; static const String bmrcalc = '$svgBasePath/bmr1.svg'; + static const String refundIcon = '$svgBasePath/refund_icon.svg'; + static const String bankIcon = '$svgBasePath/bank_icon.svg'; + static const String descriptionIcon = '$svgBasePath/description_icon.svg'; + static const String selectImageIcon = '$svgBasePath/select_image_icon.svg'; + // PNGS // static const String hmgLogo = '$pngBasePath/hmg_logo.png'; static const String liveCareService = '$pngBasePath/livecare_service.png'; @@ -427,8 +431,6 @@ class AppAssets { static const String bmiFullBodyFemale = '$pngBasePath/female_bmi.png'; static const String defaultMEP = '$pngBasePath/default_MEP.png'; - - static const String ambulance_section = '$pngBasePath/ambulance_section.png'; static const String rrt_section = '$pngBasePath/rrt_section.png'; static const String er_checkin = '$pngBasePath/er_checkin.png'; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index 1e65535a..c2fa02c1 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -209,7 +209,7 @@ class AppDependencies { ); getIt.registerLazySingleton( - () => HabibWalletViewModel(habibWalletRepo: getIt(), errorHandlerService: getIt()), + () => HabibWalletViewModel(habibWalletRepo: getIt(), errorHandlerService: getIt(), refundRequestRepo: getIt()), ); getIt.registerLazySingleton( diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 469745cb..91fc3ef4 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -18,13 +18,20 @@ extension CapExtension on String { String get needTranslation => this; String get capitalizeFirstofEach => trim().isNotEmpty ? trim().toLowerCase().split(" ").map((str) => str.inCaps).join(" ") : ""; - } extension EmailValidator on String { Widget get toWidget => Text(this); - Widget toText8({bool isEnglishOnly = false, Color? color, FontWeight? fontWeight, bool isBold = false, int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow}) => Text( + Widget toText8( + {bool isEnglishOnly = false, + Color? color, + FontWeight? fontWeight, + bool isBold = false, + int? maxlines, + FontStyle? fontStyle, + TextOverflow? textOverflow}) => + Text( this, maxLines: maxlines, overflow: textOverflow, @@ -74,7 +81,8 @@ extension EmailValidator on String { int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow, - double letterSpacing = 0, bool isEnglishOnly = false}) => + double letterSpacing = 0, + bool isEnglishOnly = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, @@ -91,7 +99,17 @@ extension EmailValidator on String { decorationColor: color ?? AppColors.blackColor), ); - Widget toText11({Color? color, FontWeight? weight, bool isUnderLine = false, bool isCenter = false, bool isBold = false, int maxLine = 0, double letterSpacing = 0, bool isEnglishOnly = false,}) => Text( + Widget toText11({ + Color? color, + FontWeight? weight, + bool isUnderLine = false, + bool isCenter = false, + bool isBold = false, + int maxLine = 0, + double letterSpacing = 0, + bool isEnglishOnly = false, + }) => + Text( this, textAlign: isCenter ? TextAlign.center : null, maxLines: (maxLine > 0) ? maxLine : null, @@ -107,7 +125,8 @@ extension EmailValidator on String { ); Widget toText12( - {bool isEnglishOnly = false, Color? color, + {bool isEnglishOnly = false, + Color? color, bool isUnderLine = false, TextAlign textAlignment = TextAlign.start, bool isBold = false, @@ -176,7 +195,15 @@ extension EmailValidator on String { ), ); - Widget toText13({Color? color, bool isUnderLine = false, bool isBold = false, bool isCenter = false, int maxLine = 0, FontWeight? weight, double? letterSpacing = 0}) => Text( + Widget toText13( + {Color? color, + bool isUnderLine = false, + bool isBold = false, + bool isCenter = false, + int maxLine = 0, + FontWeight? weight, + double? letterSpacing = 0}) => + Text( this, textAlign: isCenter ? TextAlign.center : null, maxLines: (maxLine > 0) ? maxLine : null, @@ -213,11 +240,24 @@ extension EmailValidator on String { height: height, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), decoration: isUnderLine ? TextDecoration.underline : null, - fontFamily: fontFamily ?? (isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'), + fontFamily: fontFamily ?? + (isEnglishOnly + ? "Poppins" + : getIt.get().getLanguageCode() == "ar" + ? 'CairoArabic' + : 'Poppins'), decorationColor: color ?? AppColors.blackColor), ); - Widget toText15({Color? color, bool isUnderLine = false, bool isBold = false, bool isCenter = false, FontWeight? weight, int? maxlines, double? letterSpacing = -1}) => Text( + Widget toText15( + {Color? color, + bool isUnderLine = false, + bool isBold = false, + bool isCenter = false, + FontWeight? weight, + int? maxlines, + double? letterSpacing = -1}) => + Text( this, textAlign: isCenter ? TextAlign.center : null, maxLines: maxlines, @@ -229,7 +269,9 @@ extension EmailValidator on String { decoration: isUnderLine ? TextDecoration.underline : null), ); - Widget toText16({bool isEnglishOnly = false, Color? color, + Widget toText16( + {bool isEnglishOnly = false, + Color? color, bool isUnderLine = false, bool isBold = false, bool isCenter = false, @@ -254,85 +296,194 @@ extension EmailValidator on String { overflow: textOverflow, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), decoration: isUnderLine ? TextDecoration.underline : null, - fontFamily: fontFamily ?? (isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'), + fontFamily: fontFamily ?? + (isEnglishOnly + ? "Poppins" + : getIt.get().getLanguageCode() == "ar" + ? 'CairoArabic' + : 'Poppins'), decorationColor: decorationColor ?? AppColors.errorColor), ); Widget toText17({bool isEnglishOnly = false, Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, - style: TextStyle(color: color ?? AppColors.blackColor, fontSize: 17.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, fontFamily: getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'), + style: TextStyle( + color: color ?? AppColors.blackColor, + fontSize: 17.f, + letterSpacing: -1, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + fontFamily: getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'), ); - Widget toText18({Color? color, FontWeight? weight, bool isBold = false, bool isCenter = false, int? maxlines, TextOverflow? textOverflow, bool isEnglishOnly = false,}) => Text( + Widget toText18({ + Color? color, + FontWeight? weight, + bool isBold = false, + bool isCenter = false, + int? maxlines, + TextOverflow? textOverflow, + bool isEnglishOnly = false, + }) => + Text( maxLines: maxlines, textAlign: isCenter ? TextAlign.center : null, this, overflow: textOverflow, - style: TextStyle(fontSize: 18.f, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: -0.4, fontFamily: (isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),), + style: TextStyle( + fontSize: 18.f, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), + color: color ?? AppColors.blackColor, + letterSpacing: -0.4, + fontFamily: (isEnglishOnly + ? "Poppins" + : getIt.get().getLanguageCode() == "ar" + ? 'CairoArabic' + : 'Poppins'), + ), ); Widget toText19({Color? color, bool isBold = false}) => Text( this, - style: TextStyle(fontSize: 19.f, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, color: color ?? AppColors.blackColor, letterSpacing: -0.4), + style: TextStyle( + fontSize: 19.f, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, color: color ?? AppColors.blackColor, letterSpacing: -0.4), ); Widget toText20({ Color? color, FontWeight? weight, bool isBold = false, + bool isEnglishOnly = false, }) => Text( this, - style: TextStyle(fontSize: 20.f, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: -0.4), + style: TextStyle( + fontFamily: (isEnglishOnly + ? "Poppins" + : getIt.get().getLanguageCode() == "ar" + ? 'CairoArabic' + : 'Poppins'), + fontSize: 20.f, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), + color: color ?? AppColors.blackColor, + letterSpacing: -0.4), ); Widget toText21({Color? color, bool isBold = false, FontWeight? weight, int? maxlines}) => Text( this, maxLines: maxlines, - style: TextStyle(color: color ?? AppColors.blackColor, fontSize: 21.f, letterSpacing: -1, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), + style: TextStyle( + color: color ?? AppColors.blackColor, + fontSize: 21.f, + letterSpacing: -1, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), ); Widget toText22({Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, - style: TextStyle(height: 1, color: color ?? AppColors.blackColor, fontSize: 22.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), + style: TextStyle( + height: 1, + color: color ?? AppColors.blackColor, + fontSize: 22.f, + letterSpacing: -1, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); - Widget toText24({Color? color, bool isBold = false, bool isCenter = false, FontWeight? fontWeight, double? letterSpacing, bool isEnglishOnly = false,}) => Text( + Widget toText24({ + Color? color, + bool isBold = false, + bool isCenter = false, + FontWeight? fontWeight, + double? letterSpacing, + bool isEnglishOnly = false, + }) => + Text( this, textAlign: isCenter ? TextAlign.center : null, style: TextStyle( - fontFamily: (isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'), - height: 23 / 24, color: color ?? AppColors.blackColor, fontSize: 24.f, letterSpacing: letterSpacing ?? -1, fontWeight: isBold ? FontWeight.bold : fontWeight ?? FontWeight.normal), + fontFamily: (isEnglishOnly + ? "Poppins" + : getIt.get().getLanguageCode() == "ar" + ? 'CairoArabic' + : 'Poppins'), + height: 23 / 24, + color: color ?? AppColors.blackColor, + fontSize: 24.f, + letterSpacing: letterSpacing ?? -1, + fontWeight: isBold ? FontWeight.bold : fontWeight ?? FontWeight.normal), ); - Widget toText26({Color? color, bool isBold = false, double? height, bool isCenter = false, FontWeight? weight, double? letterSpacing, bool isEnglishOnly = false,}) => Text( + Widget toText26({ + Color? color, + bool isBold = false, + double? height, + bool isCenter = false, + FontWeight? weight, + double? letterSpacing, + bool isEnglishOnly = false, + }) => + Text( this, textAlign: isCenter ? TextAlign.center : null, style: TextStyle( - fontFamily: (isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'), - height: height ?? 23 / 26, color: color ?? AppColors.blackColor, fontSize: 26.f, letterSpacing: letterSpacing ?? -1, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), + fontFamily: (isEnglishOnly + ? "Poppins" + : getIt.get().getLanguageCode() == "ar" + ? 'CairoArabic' + : 'Poppins'), + height: height ?? 23 / 26, + color: color ?? AppColors.blackColor, + fontSize: 26.f, + letterSpacing: letterSpacing ?? -1, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), ); - Widget toText28({bool isEnglishOnly = false, Color? color, bool isBold = false, double? height, bool isCenter = false, double? letterSpacing}) => Text( + Widget toText28({bool isEnglishOnly = false, Color? color, bool isBold = false, double? height, bool isCenter = false, double? letterSpacing}) => + Text( this, textAlign: isCenter ? TextAlign.center : null, - style: TextStyle(height: height ?? 23 / 28, color: color ?? AppColors.blackColor, fontSize: 28.f, letterSpacing: letterSpacing ?? -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, fontFamily: getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'), + style: TextStyle( + height: height ?? 23 / 28, + color: color ?? AppColors.blackColor, + fontSize: 28.f, + letterSpacing: letterSpacing ?? -1, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + fontFamily: getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'), ); Widget toText32({bool isEnglishOnly = false, FontWeight? weight, Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, - style: TextStyle( - - height: getIt.get().getLanguageCode() == "ar" ? 1.2 : 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.f, letterSpacing: -1, fontFamily: getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins', fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal,), + height: getIt.get().getLanguageCode() == "ar" ? 1.2 : 32 / 32, + color: color ?? AppColors.blackColor, + fontSize: 32.f, + letterSpacing: -1, + fontFamily: getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins', + fontWeight: isBold ? FontWeight.bold : weight ?? FontWeight.normal, + ), ); - Widget toText44({Color? color, bool isBold = false, bool isEnglishOnly = false,}) => Text( + Widget toText44({ + Color? color, + bool isBold = false, + bool isEnglishOnly = false, + }) => + Text( this, - style: TextStyle(height: 32 / 32, color: color ?? AppColors.blackColor, fontSize: 44.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, fontFamily: (isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'CairoArabic' : 'Poppins'),), + style: TextStyle( + height: 32 / 32, + color: color ?? AppColors.blackColor, + fontSize: 44.f, + letterSpacing: -1, + fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + fontFamily: (isEnglishOnly + ? "Poppins" + : getIt.get().getLanguageCode() == "ar" + ? 'CairoArabic' + : 'Poppins'), + ), ); Widget toSectionHeading({String upperHeading = "", String lowerHeading = ""}) { @@ -368,7 +519,9 @@ extension EmailValidator on String { } bool isValidEmail() { - return RegExp(r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$').hasMatch(this); + return RegExp( + r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$') + .hasMatch(this); } String toFormattedDate() { diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart index 01dc2357..0353868e 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -3,10 +3,21 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_repo.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/models/patient_advance_balance_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/refund_request_repo.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; class HabibWalletViewModel extends ChangeNotifier { + HabibWalletRepo habibWalletRepo; + RefundRequestRepo refundRequestRepo; + ErrorHandlerService errorHandlerService; + + HabibWalletViewModel({ + required this.habibWalletRepo, + required this.errorHandlerService, + required this.refundRequestRepo, + }); + bool isWalletAmountLoading = false; num habibWalletAmount = 0; num walletRechargeAmount = 0; @@ -14,6 +25,10 @@ class HabibWalletViewModel extends ChangeNotifier { bool isBottomSheetContentLoading = false; + // ── Refundable Advances ─────────────────────────────────────────────────── + bool isAdvancesLoading = false; + double totalAdvancesBalance = 0.0; + bool isSearchedFileNumberDataShown = false; int currentIndex = 0; @@ -22,9 +37,6 @@ class HabibWalletViewModel extends ChangeNotifier { HospitalsModel? selectedHospital; - HabibWalletRepo habibWalletRepo; - ErrorHandlerService errorHandlerService; - String fileNumber = ''; String depositorName = ''; String mobileNumber = ''; @@ -40,6 +52,7 @@ class HabibWalletViewModel extends ChangeNotifier { // Getters for field errors String? get amountError => _amountError; + String? get hospitalError => _hospitalError; String yahalaAccountNumber = ''; @@ -63,6 +76,50 @@ class HabibWalletViewModel extends ChangeNotifier { notifyListeners(); } + // ==================== WITHDRAW VALIDATION ERROR TRACKING ==================== + String? _ibanError; + String? _withdrawDescriptionError; + + String? get ibanError => _ibanError; + + String? get withdrawDescriptionError => _withdrawDescriptionError; + + void clearIbanError() { + _ibanError = null; + notifyListeners(); + } + + void clearWithdrawDescriptionError() { + _withdrawDescriptionError = null; + notifyListeners(); + } + + bool validateWithdrawForm({required String iban, required String description}) { + bool isValid = true; + _ibanError = null; + _withdrawDescriptionError = null; + + if (iban.trim().isEmpty) { + _ibanError = LocaleKeys.ibanIsRequired.tr(); + isValid = false; + } else { + // KSA IBAN format: SA followed by 2 check digits + 18 alphanumeric chars = 22 chars total + final ksaIbanRegex = RegExp(r'^SA\d{2}[0-9A-Z]{18}$', caseSensitive: false); + if (!ksaIbanRegex.hasMatch(iban.trim().replaceAll(' ', ''))) { + _ibanError = LocaleKeys.invalidIbanFormat.tr(); + isValid = false; + } + } + + if (description.trim().isEmpty) { + _withdrawDescriptionError = LocaleKeys.descriptionIsRequired.tr(); + isValid = false; + } + + notifyListeners(); + return isValid; + } + // Validate recharge form - shows only first error bool validateRechargeForm(String amount) { // Clear previous errors @@ -98,7 +155,27 @@ class HabibWalletViewModel extends ChangeNotifier { return true; } - HabibWalletViewModel({required this.habibWalletRepo, required this.errorHandlerService}); + Future fetchAdvancesForHospital() async { + isAdvancesLoading = true; + totalAdvancesBalance = 0.0; + notifyListeners(); + + final result = await refundRequestRepo.getRefundableAdvances(); + result.fold( + (failure) { + isAdvancesLoading = false; + notifyListeners(); + errorHandlerService.handleError(failure: failure); + }, + (apiResponse) { + isAdvancesLoading = false; + if (apiResponse.data != null) { + totalAdvancesBalance = apiResponse.data!.refundableAdvancesItems.fold(0.0, (sum, item) => sum + (item.balanceAmount ?? 0.0)); + } + notifyListeners(); + }, + ); + } initHabibWalletProvider() { isWalletAmountLoading = true; @@ -218,7 +295,12 @@ class HabibWalletViewModel extends ChangeNotifier { Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await habibWalletRepo.HISCreateAdvancePayment( - paymentMethodName: paymentMethodName, paidAmount: paidAmount, paymentReference: paymentReference, patientID: patientID, projectID: projectID, depositorName: depositorName); + paymentMethodName: paymentMethodName, + paidAmount: paidAmount, + paymentReference: paymentReference, + patientID: patientID, + projectID: projectID, + depositorName: depositorName); result.fold( (failure) async => await errorHandlerService.handleError(failure: failure), @@ -235,7 +317,8 @@ class HabibWalletViewModel extends ChangeNotifier { ); } - Future addAdvanceNumberRequest({required String advanceNumber, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async { + Future addAdvanceNumberRequest( + {required String advanceNumber, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await habibWalletRepo.addAdvanceNumberRequest(advanceNumber: advanceNumber, paymentReference: paymentReference); result.fold( diff --git a/lib/features/my_invoices/models/get_invoices_list_response_model.dart b/lib/features/my_invoices/models/get_invoices_list_response_model.dart index 6e515feb..a3c9858c 100644 --- a/lib/features/my_invoices/models/get_invoices_list_response_model.dart +++ b/lib/features/my_invoices/models/get_invoices_list_response_model.dart @@ -18,6 +18,7 @@ class GetInvoicesListResponseModel { double? decimalDoctorRate; String? doctorImageUrl; int? doctorRate; + bool? isAllowRefund; int? patientNumber; String? projectName; @@ -39,6 +40,7 @@ class GetInvoicesListResponseModel { this.decimalDoctorRate, this.doctorImageUrl, this.doctorRate, + this.isAllowRefund, this.patientNumber, this.projectName, }); @@ -48,46 +50,48 @@ class GetInvoicesListResponseModel { String toRawJson() => json.encode(toJson()); factory GetInvoicesListResponseModel.fromJson(Map json) => GetInvoicesListResponseModel( - setupId: json["SetupId"], - projectId: json["ProjectID"], - patientId: json["PatientID"], - appointmentNo: json["AppointmentNo"], - appointmentDate: json["AppointmentDate"], - appointmentDateN: json["AppointmentDateN"], - clinicId: json["ClinicID"], - doctorId: json["DoctorID"], - invoiceNo: json["InvoiceNo"], - status: json["Status"], - arrivedOn: json["ArrivedOn"], - doctorName: json["DoctorName"], - doctorNameN: json["DoctorNameN"], - clinicName: json["ClinicName"], - decimalDoctorRate: json["DecimalDoctorRate"]?.toDouble(), - doctorImageUrl: json["DoctorImageURL"], - doctorRate: json["DoctorRate"], - patientNumber: json["PatientNumber"], - projectName: json["ProjectName"], - ); + setupId: json["SetupId"], + projectId: json["ProjectID"], + patientId: json["PatientID"], + appointmentNo: json["AppointmentNo"], + appointmentDate: json["AppointmentDate"], + appointmentDateN: json["AppointmentDateN"], + clinicId: json["ClinicID"], + doctorId: json["DoctorID"], + invoiceNo: json["InvoiceNo"], + status: json["Status"], + arrivedOn: json["ArrivedOn"], + doctorName: json["DoctorName"], + doctorNameN: json["DoctorNameN"], + clinicName: json["ClinicName"], + decimalDoctorRate: json["DecimalDoctorRate"]?.toDouble(), + doctorImageUrl: json["DoctorImageURL"], + doctorRate: json["DoctorRate"], + isAllowRefund: json["IsAllowRefund"], + patientNumber: json["PatientNumber"], + projectName: json["ProjectName"], + ); Map toJson() => { - "SetupId": setupId, - "ProjectID": projectId, - "PatientID": patientId, - "AppointmentNo": appointmentNo, - "AppointmentDate": appointmentDate, - "AppointmentDateN": appointmentDateN, - "ClinicID": clinicId, - "DoctorID": doctorId, - "InvoiceNo": invoiceNo, - "Status": status, - "ArrivedOn": arrivedOn, - "DoctorName": doctorName, - "DoctorNameN": doctorNameN, - "ClinicName": clinicName, - "DecimalDoctorRate": decimalDoctorRate, - "DoctorImageURL": doctorImageUrl, - "DoctorRate": doctorRate, - "PatientNumber": patientNumber, - "ProjectName": projectName, - }; + "SetupId": setupId, + "ProjectID": projectId, + "PatientID": patientId, + "AppointmentNo": appointmentNo, + "AppointmentDate": appointmentDate, + "AppointmentDateN": appointmentDateN, + "ClinicID": clinicId, + "DoctorID": doctorId, + "InvoiceNo": invoiceNo, + "Status": status, + "ArrivedOn": arrivedOn, + "DoctorName": doctorName, + "DoctorNameN": doctorNameN, + "ClinicName": clinicName, + "DecimalDoctorRate": decimalDoctorRate, + "DoctorImageURL": doctorImageUrl, + "DoctorRate": doctorRate, + "IsAllowRefund": isAllowRefund, + "PatientNumber": patientNumber, + "ProjectName": projectName, + }; } diff --git a/lib/features/refund_request/models/req_models/submit_refund_request_model.dart b/lib/features/refund_request/models/req_models/submit_refund_request_model.dart index 8b137891..f1c5ed57 100644 --- a/lib/features/refund_request/models/req_models/submit_refund_request_model.dart +++ b/lib/features/refund_request/models/req_models/submit_refund_request_model.dart @@ -1 +1,99 @@ +class SubmitRefundProcedureItem { + final int lineItemNo; + final String procedureId; + final String procedureName; + final double patientPaidAmount; + final String refundReason; + const SubmitRefundProcedureItem({ + required this.lineItemNo, + required this.procedureId, + required this.procedureName, + required this.patientPaidAmount, + required this.refundReason, + }); + + Map toJson() => { + "LineItemNo": lineItemNo, + "ProcedureID": procedureId, + "ProcedureName": procedureName, + "PatientPaidAmount": patientPaidAmount, + "RefundReason": refundReason, + }; + + @override + String toString() { + return 'SubmitRefundProcedureItem{lineItemNo: $lineItemNo, procedureId: $procedureId, procedureName: $procedureName, patientPaidAmount: $patientPaidAmount, refundReason: $refundReason}'; + } +} + +class SubmitRefundRequestModel { + final int projectId; + final String refundType; + final String refundProject; + final int refundProjectId; + final String refundPatientId; + final String patientMobileNumber; + final String refundPatientName; + final String refundNationalId; + final String refundInvoiceNo; + final String refundAppointmentNo; + final int refundDoctorId; + final String refundDoctorName; + final int refundClinicId; + final String refundClinicName; + final String refundDestination; + final String refundIban; + final String refundRemarks; + final double refundTotalAmount; + final List refundProceduresList; + + const SubmitRefundRequestModel({ + required this.projectId, + required this.refundType, + required this.refundProject, + required this.refundProjectId, + required this.refundPatientId, + required this.patientMobileNumber, + required this.refundPatientName, + required this.refundNationalId, + required this.refundInvoiceNo, + required this.refundAppointmentNo, + required this.refundDoctorId, + required this.refundDoctorName, + required this.refundClinicId, + required this.refundClinicName, + required this.refundDestination, + required this.refundIban, + required this.refundRemarks, + required this.refundTotalAmount, + required this.refundProceduresList, + }); + + Map toJson() => { + "ProjectID": projectId, + "Refund_RefundType": refundType, + "Refund_Project": refundProject, + "Refund_ProjectId": refundProjectId, + "Refund_PatientId": refundPatientId, + "PatientMobileNumber": patientMobileNumber, + "Refund_PatientName": refundPatientName, + "Refund_NationalId": refundNationalId, + "Refund_InvoiceNo": refundInvoiceNo, + "Refund_AppointmentNo": refundAppointmentNo, + "Refund_DoctorId": refundDoctorId, + "Refund_DoctorName": refundDoctorName, + "Refund_ClinicId": refundClinicId, + "Refund_ClinicName": refundClinicName, + "Refund_RefundDestination": refundDestination, + "Refund_IBAN": refundIban, + "Refund_Remarks": refundRemarks, + "Refund_TotalRefundAmount": refundTotalAmount, + "Refund_ProceduresList": refundProceduresList.map((p) => p.toJson()).toList(), + }; + + @override + String toString() { + return 'SubmitRefundRequestModel{projectId: $projectId, refundType: $refundType, refundProject: $refundProject, refundProjectId: $refundProjectId, refundPatientId: $refundPatientId, patientMobileNumber: $patientMobileNumber, refundPatientName: $refundPatientName, refundNationalId: $refundNationalId, refundInvoiceNo: $refundInvoiceNo, refundAppointmentNo: $refundAppointmentNo, refundDoctorId: $refundDoctorId, refundDoctorName: $refundDoctorName, refundClinicId: $refundClinicId, refundClinicName: $refundClinicName, refundDestination: $refundDestination, refundIban: $refundIban, refundRemarks: $refundRemarks, refundTotalAmount: $refundTotalAmount, refundProceduresList: $refundProceduresList}'; + } +} diff --git a/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart b/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart index 8b137891..d8b6ffc9 100644 --- a/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart +++ b/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart @@ -1 +1,92 @@ +class SubmitRefundResponseModel { + final String? message; + final String? refundType; + final String? patientId; + final String? refNo; + SubmitRefundResponseModel({ + this.message, + this.refundType, + this.patientId, + this.refNo, + }); + + factory SubmitRefundResponseModel.fromJson(Map json) { + return SubmitRefundResponseModel( + message: json['message'] as String?, + refundType: json['refundType'] as String?, + patientId: json['patientId']?.toString(), + refNo: json['refNo']?.toString(), + ); + } +} + +class RefundStatusModel { + final String? errorMessage; + final String? lastUpdated; + final bool? pendingVerification; + final String? referenceNumber; + final String? requestStatusName; + final String? status; + final bool? verified; + + RefundStatusModel({ + this.errorMessage, + this.lastUpdated, + this.pendingVerification, + this.referenceNumber, + this.requestStatusName, + this.status, + this.verified, + }); + + factory RefundStatusModel.fromJson(Map json) { + return RefundStatusModel( + errorMessage: json['ErrorMessage'] as String?, + lastUpdated: json['LastUpdated'] as String?, + pendingVerification: json['PendingVerification'] as bool?, + referenceNumber: json['ReferenceNumber'] as String?, + requestStatusName: json['RequestStatusName'] as String?, + status: json['Status'] as String?, + verified: json['Verified'] as bool?, + ); + } +} + +class RefundInvoiceModel { + final String? invoiceNumber; + final String? invoiceDate; + final double? totalAmount; + final double? patientShare; + final String? clinicName; + final String? clinicNameN; + final int? projectId; + final String? projectName; + final String? projectNameN; + + RefundInvoiceModel({ + this.invoiceNumber, + this.invoiceDate, + this.totalAmount, + this.patientShare, + this.clinicName, + this.clinicNameN, + this.projectId, + this.projectName, + this.projectNameN, + }); + + factory RefundInvoiceModel.fromJson(Map json) { + return RefundInvoiceModel( + invoiceNumber: json['InvoiceNumber'] as String?, + invoiceDate: json['InvoiceDate'] as String?, + totalAmount: (json['TotalAmount'] as num?)?.toDouble(), + patientShare: (json['PatientShare'] as num?)?.toDouble(), + clinicName: json['ClinicName'] as String?, + clinicNameN: json['ClinicNameN'] as String?, + projectId: json['ProjectID'] as int?, + projectName: json['ProjectName'] as String?, + projectNameN: json['ProjectNameN'] as String?, + ); + } +} diff --git a/lib/features/refund_request/models/resp_models/refundable_invoices_response_model.dart b/lib/features/refund_request/models/resp_models/refundable_invoices_response_model.dart index 1df3e76b..2409f3ec 100644 --- a/lib/features/refund_request/models/resp_models/refundable_invoices_response_model.dart +++ b/lib/features/refund_request/models/resp_models/refundable_invoices_response_model.dart @@ -18,9 +18,7 @@ class RefundableInvoicesResponseModel { errorCode: json['ErrorCode'] as String?, message: json['Message'] as String?, refundableInvoiceItems: json['RefundableInvoiceItems'] != null - ? (json['RefundableInvoiceItems'] as List) - .map((item) => RefundableInvoiceItem.fromJson(item as Map)) - .toList() + ? (json['RefundableInvoiceItems'] as List).map((item) => RefundableInvoiceItem.fromJson(item as Map)).toList() : null, statusCode: json['StatusCode'] as int?, ); @@ -52,8 +50,7 @@ class RefundableInvoiceItem { this.projectID, }); - factory RefundableInvoiceItem.fromRawJson(String str) => - RefundableInvoiceItem.fromJson(json.decode(str)); + factory RefundableInvoiceItem.fromRawJson(String str) => RefundableInvoiceItem.fromJson(json.decode(str)); String toRawJson() => json.encode(toJson()); @@ -88,3 +85,115 @@ class RefundableInvoiceItem { } } +class RefundableInvoiceDetailsItem { + final int? lineItemNo; + final double? patientPaidAmount; + final String? procedureId; + final String? procedureName; + + RefundableInvoiceDetailsItem({ + this.lineItemNo, + this.patientPaidAmount, + this.procedureId, + this.procedureName, + }); + + factory RefundableInvoiceDetailsItem.fromJson(Map json) { + return RefundableInvoiceDetailsItem( + lineItemNo: json['LineItemNo'] as int?, + patientPaidAmount: (json['PatientPaidAmount'] as num?)?.toDouble(), + procedureId: json['ProcedureID'] as String?, + procedureName: json['ProcedureName'] as String?, + ); + } +} + +class RefundableInvoicesDetailsObject { + final String? errorCode; + final int? invoiceNo; + final String? message; + final int? projectId; + final List refundableInvoiceDetailsItems; + final int? statusCode; + + RefundableInvoicesDetailsObject({ + this.errorCode, + this.invoiceNo, + this.message, + this.projectId, + this.refundableInvoiceDetailsItems = const [], + this.statusCode, + }); + + factory RefundableInvoicesDetailsObject.fromJson(Map json) { + return RefundableInvoicesDetailsObject( + errorCode: json['ErrorCode'] as String?, + invoiceNo: json['InvoiceNo'] as int?, + message: json['Message'] as String?, + projectId: json['ProjectID'] as int?, + refundableInvoiceDetailsItems: json['RefundableInvoiceDetailsItems'] != null + ? (json['RefundableInvoiceDetailsItems'] as List) + .map((item) => RefundableInvoiceDetailsItem.fromJson(item as Map)) + .toList() + : [], + statusCode: json['StatusCode'] as int?, + ); + } +} + +class RefundableAdvancesItem { + final String? advanceDate; + final int? advanceNo; + final double? balanceAmount; + final double? paidAmount; + final int? patientId; + final String? patientName; + final int? projectId; + + RefundableAdvancesItem({ + this.advanceDate, + this.advanceNo, + this.balanceAmount, + this.paidAmount, + this.patientId, + this.patientName, + this.projectId, + }); + + factory RefundableAdvancesItem.fromJson(Map json) { + return RefundableAdvancesItem( + advanceDate: json['AdvanceDate'] as String?, + advanceNo: json['AdvanceNo'] as int?, + balanceAmount: (json['BalanceAmount'] as num?)?.toDouble(), + paidAmount: (json['PaidAmount'] as num?)?.toDouble(), + patientId: json['PatientID'] as int?, + patientName: json['PatientName'] as String?, + projectId: json['ProjectID'] as int?, + ); + } +} + +class RefundableAdvancesObject { + final String? errorCode; + final String? message; + final List refundableAdvancesItems; + final int? statusCode; + + RefundableAdvancesObject({ + this.errorCode, + this.message, + this.refundableAdvancesItems = const [], + this.statusCode, + }); + + factory RefundableAdvancesObject.fromJson(Map json) { + return RefundableAdvancesObject( + errorCode: json['ErrorCode'] as String?, + message: json['Message'] as String?, + refundableAdvancesItems: json['RefundableAdvancesItems'] != null + ? (json['RefundableAdvancesItems'] as List).map((item) => RefundableAdvancesItem.fromJson(item as Map)).toList() + : [], + statusCode: json['StatusCode'] as int?, + ); + } +} diff --git a/lib/features/refund_request/refund_request_repo.dart b/lib/features/refund_request/refund_request_repo.dart index f8e603b6..03797e54 100644 --- a/lib/features/refund_request/refund_request_repo.dart +++ b/lib/features/refund_request/refund_request_repo.dart @@ -3,11 +3,26 @@ import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/req_models/submit_refund_request_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_model.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; abstract class RefundRequestRepo { Future>>> getRefundableInvoices({required int projectID}); + + Future>> getRefundableInvoicesDetails({ + required int invoiceNo, + required int projectId, + }); + + Future>> getRefundableAdvances(); + + Future>> getRefundStatus({required String referenceNumber}); + + Future>>> getRefundInvoices({required String patientId, String invoiceNumberQuery}); + + Future>> submitRefund({required SubmitRefundRequestModel request}); } class RefundRequestRepoImp implements RefundRequestRepo { @@ -18,11 +33,7 @@ class RefundRequestRepoImp implements RefundRequestRepo { @override Future>>> getRefundableInvoices({required int projectID}) async { - // ApiClient auto-injects: PatientID, TokenID, LanguageID, PatientOutSA, - // PatientTypeID, IPAdress, generalid, VersionID, Channel - Map body = { - "ProjectID": projectID, - }; + Map body = {"ProjectID": projectID}; try { GenericApiModel>? apiResponse; @@ -69,4 +80,221 @@ class RefundRequestRepoImp implements RefundRequestRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> getRefundableInvoicesDetails({ + required int invoiceNo, + required int projectId, + }) async { + final Map body = { + "InvoiceNo": invoiceNo, + "ProjectID": projectId, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getRefundableInvoicesDetails, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final detailsJson = response['RefundableInvoicesDetailsObject']; + final RefundableInvoicesDetailsObject details = detailsJson != null + ? RefundableInvoicesDetailsObject.fromJson(detailsJson as Map) + : RefundableInvoicesDetailsObject(); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: details, + ); + } catch (e) { + loggerService.logError("getRefundableInvoicesDetails parsing error: $e"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("getRefundableInvoicesDetails exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getRefundableAdvances() async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getRefundableAdvances, + body: {}, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final advancesJson = response['RefundableAdvancesObject']; + final RefundableAdvancesObject advances = + advancesJson != null ? RefundableAdvancesObject.fromJson(advancesJson as Map) : RefundableAdvancesObject(); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: advances, + ); + } catch (e) { + loggerService.logError("getRefundableAdvances parsing error: $e"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("getRefundableAdvances exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> getRefundStatus({required String referenceNumber}) async { + final Map body = { + "Refund_ReferenceNumber": referenceNumber, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getRefundStatus, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final dataJson = response['Refund_StatusItem']; + final RefundStatusModel model = dataJson != null ? RefundStatusModel.fromJson(dataJson as Map) : RefundStatusModel(); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: model, + ); + } catch (e) { + loggerService.logError("getRefundStatus parsing error: $e"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("getRefundStatus exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>>> getRefundInvoices({ + required String patientId, + String invoiceNumberQuery = "", + }) async { + final Map body = { + "Refund_PatientId": patientId, + "Refund_InvoiceNumberQuery": invoiceNumberQuery, + }; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getRefundInvoices, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final listJson = response['RefundInvoicesList']; + final List invoices = + listJson != null ? (listJson as List).map((item) => RefundInvoiceModel.fromJson(item as Map)).toList() : []; + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: invoices, + ); + } catch (e) { + loggerService.logError("getRefundInvoices parsing error: $e"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("getRefundInvoices exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> submitRefund({required SubmitRefundRequestModel request}) async { + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.submitRefund, + body: request.toJson(), + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final model = SubmitRefundResponseModel.fromJson(response as Map); + apiResponse = GenericApiModel( + messageStatus: 1, + statusCode: statusCode, + errorMessage: errorMessage, + data: model, + ); + } catch (e) { + loggerService.logError("submitRefund parsing error: $e"); + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + loggerService.logError("submitRefund exception: $e"); + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/refund_request/refund_request_view_model.dart b/lib/features/refund_request/refund_request_view_model.dart index 030b2a70..dcd954b6 100644 --- a/lib/features/refund_request/refund_request_view_model.dart +++ b/lib/features/refund_request/refund_request_view_model.dart @@ -1,12 +1,27 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/hospital_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/req_models/submit_refund_request_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/refund_request_repo.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; enum RefundTabType { invoices, hmgWallet } +extension RefundTabTypeExtension on RefundTabType { + String toDisplayString() { + switch (this) { + case RefundTabType.hmgWallet: + return LocaleKeys.hmgWallet.tr(); + case RefundTabType.invoices: + return LocaleKeys.bankTransfer.tr(); + } + } +} + /// Lightweight model representing a refundable procedure line item. class RefundableProcedure { final int procedureId; @@ -14,6 +29,11 @@ class RefundableProcedure { final String? procedureNameN; final double patientShare; bool isSelected; + String? refundReason; + + /// Raw values from the API — used when building the SubmitRefund payload + final int? apiLineItemNo; + final String? apiProcedureId; RefundableProcedure({ required this.procedureId, @@ -21,6 +41,9 @@ class RefundableProcedure { this.procedureNameN, required this.patientShare, this.isSelected = false, + this.refundReason, + this.apiLineItemNo, + this.apiProcedureId, }); } @@ -80,6 +103,22 @@ class RefundRequestViewModel extends ChangeNotifier { } } + void resetState() { + currentStep = 0; + procedures.clear(); + selectedPaymentMethod = null; + selectedRefundReason = null; + isProceduresLoading = false; + isInvoiceDetailsLoading = false; + invoiceDetails = null; + _ibanError = null; + _descriptionError = null; + // Use Future.microtask to avoid calling notifyListeners while tree is locked (e.g. during dispose) + Future.microtask(() { + if (hasListeners) notifyListeners(); + }); + } + // ── Hospital selection ──────────────────────────────────────────────────── HospitalsModel? selectedHospital; @@ -176,8 +215,26 @@ class RefundRequestViewModel extends ChangeNotifier { notifyListeners(); } + void setRefundReasonForProcedure(RefundableProcedure procedure, String reason) { + procedure.refundReason = reason; + notifyListeners(); + } + + bool get isAllSelected => procedures.isNotEmpty && procedures.every((p) => p.isSelected); + + void toggleSelectAll() { + final selectAll = !isAllSelected; + for (final p in procedures) { + p.isSelected = selectAll; + } + notifyListeners(); + } + List get selectedProcedures => procedures.where((p) => p.isSelected).toList(); + bool get allSelectedProceduresHaveReasons => + selectedProcedures.isNotEmpty && selectedProcedures.every((p) => p.refundReason != null && p.refundReason!.isNotEmpty); + // ── Calculation ─────────────────────────────────────────────────────────── double get totalRefundableAmount { double total = 0.0; @@ -187,8 +244,295 @@ class RefundRequestViewModel extends ChangeNotifier { return total; } + // ── Bank Details & Description ──────────────────────────────────────────── + String bankDetails = ''; + String description = ''; + + // ── Bank Transfer Error Tracking ────────────────────────────────────────── + String? _ibanError; + String? _descriptionError; + + String? get ibanError => _ibanError; + + String? get descriptionError => _descriptionError; + + void clearIbanError() { + _ibanError = null; + notifyListeners(); + } + + void clearDescriptionError() { + _descriptionError = null; + notifyListeners(); + } + + bool validateBankTransferForm({required String iban, required String description}) { + bool isValid = true; + _ibanError = null; + _descriptionError = null; + + if (iban.trim().isEmpty) { + _ibanError = LocaleKeys.ibanIsRequired.tr(); + isValid = false; + } else { + final ksaIbanRegex = RegExp(r'^SA\d{2}[0-9A-Z]{20}$', caseSensitive: false); + if (!ksaIbanRegex.hasMatch(iban.trim().replaceAll(' ', ''))) { + _ibanError = LocaleKeys.invalidIbanFormat.tr(); + isValid = false; + } + } + + if (description.trim().isEmpty) { + _descriptionError = LocaleKeys.descriptionIsRequired.tr(); + isValid = false; + } + + notifyListeners(); + return isValid; + } + + // ── Payment Method ──────────────────────────────────────────────────────── + RefundTabType? selectedPaymentMethod; + + void setPaymentMethod(RefundTabType method) { + selectedPaymentMethod = method; + notifyListeners(); + } + + // ── Refund Reason ───────────────────────────────────────────────────────── + String? selectedRefundReason; + + void setRefundReason(String? reason) { + selectedRefundReason = reason; + notifyListeners(); + } + + /// Used by text controllers in the UI layer to trigger a rebuild. + void triggerRebuild() => notifyListeners(); + + void setBankDetails(String value) { + bankDetails = value; + notifyListeners(); + } + + void setDescription(String value) { + description = value; + notifyListeners(); + } + // ── Validation ──────────────────────────────────────────────────────────── bool get isStep1Valid => selectedHospital != null && selectedInvoice != null; - bool get isStep2Valid => selectedProcedures.isNotEmpty; + bool get isStep2Valid => selectedProcedures.isNotEmpty && selectedPaymentMethod != null; + + // ── Invoice Details ─────────────────────────────────────────────────────── + RefundableInvoicesDetailsObject? invoiceDetails; + bool isInvoiceDetailsLoading = false; + + Future getRefundableInvoicesDetails({ + required int invoiceNo, + required int projectId, + Function(RefundableInvoicesDetailsObject)? onSuccess, + Function(String)? onError, + }) async { + isInvoiceDetailsLoading = true; + isProceduresLoading = true; + invoiceDetails = null; + procedures.clear(); + notifyListeners(); + + final result = await refundRequestRepo.getRefundableInvoicesDetails(invoiceNo: invoiceNo, projectId: projectId); + + result.fold( + (failure) { + isInvoiceDetailsLoading = false; + isProceduresLoading = false; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (apiResponse) { + isInvoiceDetailsLoading = false; + isProceduresLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + invoiceDetails = apiResponse.data; + notifyListeners(); + if (onSuccess != null) onSuccess(apiResponse.data!); + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? LocaleKeys.failed.tr()); + } + } + }, + ); + } + + // ── Refundable Advances ─────────────────────────────────────────────────── + RefundableAdvancesObject? refundableAdvances; + bool isRefundableAdvancesLoading = false; + + Future getRefundableAdvances({ + Function(RefundableAdvancesObject)? onSuccess, + Function(String)? onError, + }) async { + isRefundableAdvancesLoading = true; + refundableAdvances = null; + notifyListeners(); + + final result = await refundRequestRepo.getRefundableAdvances(); + + result.fold( + (failure) { + isRefundableAdvancesLoading = false; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (apiResponse) { + isRefundableAdvancesLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + refundableAdvances = apiResponse.data; + notifyListeners(); + if (onSuccess != null) onSuccess(apiResponse.data!); + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? "Failed to load refundable advances"); + } + } + }, + ); + } + + // ── Refund Status ───────────────────────────────────────────────────────── + RefundStatusModel? refundStatus; + bool isRefundStatusLoading = false; + + Future getRefundStatus({ + required String referenceNumber, + Function(RefundStatusModel)? onSuccess, + Function(String)? onError, + }) async { + isRefundStatusLoading = true; + refundStatus = null; + notifyListeners(); + + final result = await refundRequestRepo.getRefundStatus(referenceNumber: referenceNumber); + + result.fold( + (failure) { + isRefundStatusLoading = false; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (apiResponse) { + isRefundStatusLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + refundStatus = apiResponse.data; + notifyListeners(); + if (onSuccess != null) onSuccess(apiResponse.data!); + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? "Failed to load refund status"); + } + } + }, + ); + } + + // ── Refund Invoices ─────────────────────────────────────────────────────── + List refundInvoicesList = []; + bool isRefundInvoicesLoading = false; + + Future getRefundInvoices({ + String? patientId, + String invoiceNumberQuery = "", + Function(List)? onSuccess, + Function(String)? onError, + }) async { + isRefundInvoicesLoading = true; + refundInvoicesList.clear(); + notifyListeners(); + + // final resolvedPatientId = patientId ?? appState.getAuthenticatedUser()?.patientId?.toString() ?? ""; + final resolvedPatientId = 4768571; + final result = await refundRequestRepo.getRefundInvoices( + patientId: resolvedPatientId.toString(), + invoiceNumberQuery: invoiceNumberQuery, + ); + + result.fold( + (failure) { + isRefundInvoicesLoading = false; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (apiResponse) { + isRefundInvoicesLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + refundInvoicesList = apiResponse.data!; + notifyListeners(); + if (onSuccess != null) onSuccess(apiResponse.data!); + } else { + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? "Failed to load refund invoices"); + } + } + }, + ); + } + + // ── Submit Refund ───────────────────────────────────────────────────────── + bool isSubmitting = false; + + Future submitRefund({ + required SubmitRefundRequestModel request, + Function(SubmitRefundResponseModel)? onSuccess, + Function(String)? onError, + }) async { + isSubmitting = true; + notifyListeners(); + + final result = await refundRequestRepo.submitRefund(request: request); + + result.fold( + (failure) { + isSubmitting = false; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (apiResponse) { + isSubmitting = false; + notifyListeners(); + if (apiResponse.data != null) { + if (onSuccess != null) onSuccess(apiResponse.data!); + } else { + if (onError != null) { + onError(apiResponse.errorMessage ?? "Failed to submit refund"); + } + } + }, + ); + } } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 84cff5ad..cf221cff 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1156,6 +1156,7 @@ abstract class LocaleKeys { static const transportOptions = 'transportOptions'; static const selectHospitalForAdvancePayment = 'selectHospitalForAdvancePayment'; static const recharge = 'recharge'; + static const withdraw = 'withdraw'; static const activityLevel = 'activityLevel'; static const selectActivityLevel = 'selectActivityLevel'; static const almostInactive = 'almostInactive'; @@ -1839,4 +1840,30 @@ abstract class LocaleKeys { static const completedPrescriptionOrder = 'completedPrescriptionOrder'; static const submitEReferral = 'submitEReferral'; static const requestRefund = 'requestRefund'; + static const uploadImage = 'uploadImage'; + static const selectImage = 'selectImage'; + static const selectOptions = 'selectOptions'; + static const iban = 'iban'; + static const addIbanDetails = 'addIbanDetails'; + static const enterDescription = 'enterDescription'; + static const ibanIsRequired = 'ibanIsRequired'; + static const descriptionIsRequired = 'descriptionIsRequired'; + static const invalidIbanFormat = 'invalidIbanFormat'; + static const refundHistory = 'refundHistory'; + static const noRefundHistoryFound = 'noRefundHistoryFound'; + static const selectProcedures = 'selectProcedures'; + static const noProceduresAvailableForRefund = 'noProceduresAvailableForRefund'; + static const selectAll = 'selectAll'; + static const selectRefundMethod = 'selectRefundMethod'; + static const hmgWallet = 'hmgWallet'; + static const bankTransfer = 'bankTransfer'; + static const selectRefundReason = 'selectRefundReason'; + static const refundReason = 'refundReason'; + static const refundReason1 = 'refundReason1'; + static const refundReason2 = 'refundReason2'; + static const refundReason3 = 'refundReason3'; + static const refundReason4 = 'refundReason4'; + static const refundReason5 = 'refundReason5'; + static const refundReason6 = 'refundReason6'; + static const refundReason7 = 'refundReason7'; } diff --git a/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart b/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart index 0761ab6c..06056764 100644 --- a/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart +++ b/lib/presentation/comprehensive_checkup/comprehensive_checkup_page.dart @@ -376,7 +376,7 @@ class _ComprehensiveCheckupPageState extends State { borderRadius: 24.r, hasShadow: true, ), - padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 12.h), + padding: EdgeInsets.only(left: 16.w, right: 16.w, top: 12.h, bottom: 16.h), child: CustomButton( borderWidth: 0, text: LocaleKeys.next.tr(context: context), diff --git a/lib/presentation/habib_wallet/habib_wallet_page.dart b/lib/presentation/habib_wallet/habib_wallet_page.dart index c21e7a62..56cbec35 100644 --- a/lib/presentation/habib_wallet/habib_wallet_page.dart +++ b/lib/presentation/habib_wallet/habib_wallet_page.dart @@ -3,17 +3,18 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; -import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/recharge_wallet_page.dart'; -import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/presentation/habib_wallet/withdraw_request_create_page.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; @@ -41,7 +42,7 @@ class _HabibWalletState extends State { @override Widget build(BuildContext context) { habibWalletVM = Provider.of(context, listen: false); - AppState _appState = getIt.get(); + AppState appState = getIt.get(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( @@ -56,11 +57,10 @@ class _HabibWalletState extends State { width: double.infinity, height: 180.h, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - // color: AppColors.blackBgColor, - color: Color(0xFF2E3039), - borderRadius: 24.r, - hasShadow: true - ), + // color: AppColors.blackBgColor, + color: Color(0xFF2E3039), + borderRadius: 24.r, + hasShadow: true), child: Padding( padding: EdgeInsets.all(16.h), child: Column( @@ -73,8 +73,9 @@ class _HabibWalletState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - "${_appState.getAuthenticatedUser()!.firstName!} ${_appState.getAuthenticatedUser()!.lastName!}".toText19(isBold: true, color: Colors.white), - "${LocaleKeys.fileno.tr(context: context)}: ${_appState.getAuthenticatedUser()!.patientId!}" + "${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}" + .toText19(isBold: true, color: Colors.white), + "${LocaleKeys.fileno.tr(context: context)}: ${appState.getAuthenticatedUser()!.patientId!}" .toText14(isBold: true, color: AppColors.greyTextColor, isEnglishOnly: true), ], ).expanded, @@ -85,13 +86,8 @@ class _HabibWalletState extends State { LocaleKeys.balanceAmount.tr(context: context).toText14(isBold: true, color: Colors.white), SizedBox(height: 4.h), Consumer(builder: (context, habibWalletVM, child) { - return Utils.getPaymentAmountWithSymbol2( - habibWalletVM.habibWalletAmount, - textColor: Colors.white, - iconColor: Colors.white, - iconSize: 16, - isExpanded: false, - letterSpacing: -1) + return Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletAmount, + textColor: Colors.white, iconColor: Colors.white, iconSize: 16, isExpanded: false, letterSpacing: -1) .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.h, width: 80.h, height: 24.h); }), ], @@ -127,6 +123,23 @@ class _HabibWalletState extends State { }, ), ), + SizedBox(width: 8.w), + Flexible( + child: CustomButton( + height: 40.h, + icon: AppAssets.refundIcon, + iconSize: 24.w, + backgroundColor: AppColors.successColor, + textColor: Colors.white, + text: LocaleKeys.withdraw.tr(context: context), + borderWidth: 0.w, + fontWeight: FontWeight.w600, + borderColor: Colors.transparent, + padding: EdgeInsets.fromLTRB(4, 0, 12, 0), + fontSize: 14.f, + onPressed: () => Navigator.of(context).push(CustomPageRoute(page: WithdrawRequestCreatePage())), + ), + ), ], ), SizedBox(height: 16.h), @@ -146,9 +159,18 @@ class _HabibWalletState extends State { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: habibWalletVM.habibWalletBalanceList[index].projectDescription!.toText16(isBold: true, color: AppColors.textColor)), - Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletBalanceList[index].patientAdvanceBalanceAmount!, //did the changes because of negative value - textColor: AppColors.textColor, iconColor: AppColors.textColor, iconSize: 18.h, isExpanded: false, fontSize: 28.f, fontWeight: FontWeight.w600, letterSpacing: -1), + Expanded( + child: habibWalletVM.habibWalletBalanceList[index].projectDescription! + .toText16(isBold: true, color: AppColors.textColor)), + Utils.getPaymentAmountWithSymbol2( + habibWalletVM.habibWalletBalanceList[index].patientAdvanceBalanceAmount!, //did the changes because of negative value + textColor: AppColors.textColor, + iconColor: AppColors.textColor, + iconSize: 18.h, + isExpanded: false, + fontSize: 28.f, + fontWeight: FontWeight.w600, + letterSpacing: -1), ], ).paddingSymmetrical(0, 12.h); }, diff --git a/lib/presentation/habib_wallet/withdraw_request_create_page.dart b/lib/presentation/habib_wallet/withdraw_request_create_page.dart new file mode 100644 index 00000000..11f422e4 --- /dev/null +++ b/lib/presentation/habib_wallet/withdraw_request_create_page.dart @@ -0,0 +1,325 @@ +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/presentation/refund_request/refund_history_screen.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/image_picker.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:provider/provider.dart'; + +class WithdrawRequestCreatePage extends StatefulWidget { + const WithdrawRequestCreatePage({super.key}); + + @override + State createState() => _WithdrawRequestCreatePageState(); +} + +class _WithdrawRequestCreatePageState extends State { + late HabibWalletViewModel _habibWalletViewModel; + late AppState _appState; + + final TextEditingController _bankDetailsController = TextEditingController(); + final TextEditingController _descriptionController = TextEditingController(); + final FocusNode _ibanFocusNode = FocusNode(); + final FocusNode _descriptionFocusNode = FocusNode(); + + @override + void initState() { + super.initState(); + _appState = getIt.get(); + _habibWalletViewModel = context.read(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _habibWalletViewModel.getProjectsList(); + }); + } + + @override + void dispose() { + _bankDetailsController.dispose(); + _descriptionController.dispose(); + _ibanFocusNode.dispose(); + _descriptionFocusNode.dispose(); + super.dispose(); + } + + bool get _isFormValid { + final vm = _habibWalletViewModel; + return vm.selectedHospital != null && + !vm.isAdvancesLoading && + _bankDetailsController.text.trim().isNotEmpty && + _descriptionController.text.trim().isNotEmpty; + } + + // ── Bottom bar ──────────────────────────────────────────────────────────── + + Widget _buildBottomBar() { + return Consumer( + builder: (context, vm, _) { + final isValid = vm.selectedHospital != null && + !vm.isAdvancesLoading && + _bankDetailsController.text.trim().isNotEmpty && + _descriptionController.text.trim().isNotEmpty; + + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CustomButton( + text: LocaleKeys.submitRequest.tr(context: context), + isDisabled: !isValid, + onPressed: () { + final isFormValid = vm.validateWithdrawForm( + iban: _bankDetailsController.text, + description: _descriptionController.text, + ); + if (isFormValid) { + // TODO: Submit withdraw request + } + }, + ).paddingSymmetrical(24.h, 24.h), + ], + ), + ); + }, + ); + } + + Widget _buildProjectSelectionAndBalance(HabibWalletViewModel habibWalletViewModel) { + final isArabic = _appState.isArabic(); + return Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.select_hospital_icon, + width: 40.h, + height: 40.h, + applyThemeColor: false, + ), + SizedBox(width: 8.h), + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + LocaleKeys.hospital.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), + (habibWalletViewModel.selectedHospital != null + ? ((isArabic + ? (habibWalletViewModel.selectedHospital!.nameN?.toString() ?? habibWalletViewModel.selectedHospital!.name) + : habibWalletViewModel.selectedHospital!.name) ?? + LocaleKeys.selectHospital.tr(context: context)) + : LocaleKeys.selectHospital.tr(context: context)) + .toText14(color: AppColors.textColor, isBold: true), + ]), + ], + ), + Utils.buildSvgWithAssets( + icon: AppAssets.arrow_down, + width: 25.h, + height: 25.h, + ), + ], + ).onPress(() { + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.selectHospital.tr(context: context), + isDismissible: false, + child: SelectHospitalBottomSheet( + onHospitalSelected: (hospital) { + habibWalletViewModel.setSelectedHospital(hospital); + habibWalletViewModel.fetchAdvancesForHospital(); + }, + ), + callBackFunc: () {}, + ); + }), + // Only show divider + amount once hospital is selected + if (habibWalletViewModel.selectedHospital != null) ...[ + Divider(color: AppColors.bottomNAVBorder, thickness: 1).paddingOnly(top: 16.h), + habibWalletViewModel.isAdvancesLoading + ? Container( + height: 60.h, + // width: 160.w, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(12.r), + ), + ).toShimmer2(isShow: true, radius: 12.r) + : Utils.getPaymentAmountWithSymbol( + habibWalletViewModel.totalAdvancesBalance.toStringAsFixed(2).toText44(isBold: true), + AppColors.blackColor, + 18.w, + isSaudiCurrency: true, + isExpanded: false, + ), + ], + ], + ), + ); + } + + Widget _buildBankInputDetails() { + return Consumer( + builder: (context, viewModel, _) { + return Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Column( + children: [ + TextInputWidget( + labelText: LocaleKeys.addIbanDetails.tr(), + hintText: "SA0000000000000000000000", + controller: _bankDetailsController, + focusNode: _ibanFocusNode, + leadingIconNeedsContainer: false, + autoFocus: false, + keyboardType: TextInputType.text, + isEnable: true, + isReadOnly: false, + prefix: null, + isBorderAllowed: false, + isAllowLeadingIcon: true, + fontSize: 14.f, + isCountryDropDown: false, + leadingIcon: AppAssets.bankIcon, + fontFamily: "Poppins", + hasError: viewModel.ibanError != null, + errorMessage: viewModel.ibanError, + onChange: (value) { + viewModel.clearIbanError(); + }, + onSubmitted: (value) { + _descriptionFocusNode.requestFocus(); + }, + ), + Divider(color: AppColors.bottomNAVBorder, thickness: 1).withVerticalPadding(16.h), + TextInputWidget( + labelText: LocaleKeys.description.tr(), + hintText: LocaleKeys.enterDescription.tr(), + controller: _descriptionController, + focusNode: _descriptionFocusNode, + autoFocus: false, + keyboardType: TextInputType.text, + leadingIconNeedsContainer: false, + isEnable: true, + isReadOnly: false, + prefix: null, + isBorderAllowed: false, + isAllowLeadingIcon: true, + fontSize: 14.f, + isCountryDropDown: false, + leadingIcon: AppAssets.descriptionIcon, + fontFamily: "Poppins", + hasError: viewModel.withdrawDescriptionError != null, + errorMessage: viewModel.withdrawDescriptionError, + onChange: (value) { + viewModel.clearWithdrawDescriptionError(); + }, + ), + ], + ), + ); + }, + ); + } + + Widget _buildImagePickerDropDown() { + return Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.selectImageIcon, + width: 40.h, + height: 40.h, + applyThemeColor: false, + ), + SizedBox(width: 8.h), + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + LocaleKeys.uploadImage.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), + LocaleKeys.selectImage.tr(context: context).toText14(color: AppColors.textColor, isBold: true), + ]), + ], + ), + Utils.buildSvgWithAssets( + icon: AppAssets.arrow_down, + width: 25.h, + height: 25.h, + ), + ], + ).onPress(() { + ImageOptions.showImageOptionsNew( + context, + false, + title: LocaleKeys.selectOptions.tr(context: context), + (String image, File file) {}, + ); + }), + ); + } + +// ── Build ───────────────────────────────────────────────────────────────── + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, habibWalletViewModel, _) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: LocaleKeys.requestRefund.tr(context: context), + requests: () => Navigator.of(context).push(CustomPageRoute(page: RefundHistoryScreen())), + child: SingleChildScrollView( + padding: EdgeInsets.only(bottom: 32.h, left: 24.w, right: 24.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildProjectSelectionAndBalance(habibWalletViewModel), + if (habibWalletViewModel.selectedHospital != null) ...[ + SizedBox(height: 16.h), + _buildBankInputDetails(), + SizedBox(height: 16.h), + _buildImagePickerDropDown(), + ], + ], + ), + ), + ), + ), + _buildBottomBar(), + ], + ), + ); + }, + ); + } +} diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index a4a9ee10..0dadf584 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -20,6 +20,7 @@ import 'package:hmg_patient_app_new/features/weather/weather_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/blood_donation/blood_donation_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; +import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/comprehensive_checkup_page.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/contact_us.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/emergency_services_page.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/habib_wallet_page.dart'; @@ -101,7 +102,7 @@ class _ServicesPageState extends State { HmgServicesComponentModel( 5, LocaleKeys.completeCheckup.tr(), "", AppAssets.comprehensiveCheckup, bgColor: AppColors.bgGreenColor, true, route: null, onTap: () async { if (getIt.get().isAuthenticated) { - getIt.get().pushPageRoute(AppRoutes.comprehensiveCheckupPage); + Navigator.of(getIt().navigatorKey.currentContext!).push(CustomPageRoute(page: ComprehensiveCheckupPage())); } else { Navigator.of(getIt().navigatorKey.currentContext!).push( CustomPageRoute( diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 5ea5b96b..ae270353 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -147,11 +147,14 @@ class _LandingPageState extends State { notificationsViewModel.initNotificationsViewModel(); insuranceViewModel.initInsuranceProvider(); - if(!appState.isRatedVisible) { - appointmentRatingViewModel.getLastRatingAppointment(onSuccess: (response) { - if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) { - appointmentRatingViewModel.getAppointmentDetails(appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!, appointmentRatingViewModel.appointmentRatedList.last.projectID!, - onSuccess: ((response) { + if (!appState.isRatedVisible) { + appointmentRatingViewModel.getLastRatingAppointment( + onSuccess: (response) { + if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) { + appointmentRatingViewModel.getAppointmentDetails( + appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!, + appointmentRatingViewModel.appointmentRatedList.last.projectID!, + onSuccess: ((response) { appointmentRatingViewModel.setClinicOrDoctor(false); appointmentRatingViewModel.setTitle(LocaleKeys.rateDoctor.tr(context: context)); appointmentRatingViewModel.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr(context: context)); @@ -452,7 +455,9 @@ class _LandingPageState extends State { height: isTablet ? 290.h : 255.h, child: ListView.separated( scrollDirection: Axis.horizontal, - itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length < 3 ? myAppointmentsVM.patientAppointmentsHistoryList.length : 3, + itemCount: myAppointmentsVM.patientAppointmentsHistoryList.length < 3 + ? myAppointmentsVM.patientAppointmentsHistoryList.length + : 3, shrinkWrap: true, padding: EdgeInsets.only(left: 16.h, right: 16.h), itemBuilder: (context, index) { @@ -840,7 +845,7 @@ class _LandingPageState extends State { children: [ LocaleKeys.myBalanceSubtitle.tr(context: context).toText16(isBold: true).paddingSymmetrical(24.h, 0.h), SizedBox( - height: 170.h, + height: 175.h, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: 2, diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 560768b5..8f2ce569 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -59,7 +59,6 @@ import 'package:hmg_patient_app_new/presentation/my_invoices/my_invoices_list.da import 'package:hmg_patient_app_new/presentation/prescriptions/prescriptions_list_page.dart'; import 'package:hmg_patient_app_new/presentation/radiology/radiology_orders_page.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/todo_page.dart'; -import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_create_page.dart'; import 'package:hmg_patient_app_new/presentation/vital_sign/vital_sign_page.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; @@ -662,20 +661,20 @@ class _MedicalFilePageState extends State { ).onPress(() { Navigator.of(context).push(CustomPageRoute(page: MyInvoicesList())); }), - MedicalFileCard( - label: LocaleKeys.requestRefund.tr(context: context), - textColor: AppColors.blackColor, - backgroundColor: AppColors.whiteColor, - svgIcon: AppAssets.ancillary_orders_list_icon, - isLargeText: true, - iconSize: 36.w, - ).onPress(() { - Navigator.of(context).push( - CustomPageRoute( - page: const RefundRequestCreatePage(initialTabIndex: 0), - ), - ); - }), + // MedicalFileCard( + // label: LocaleKeys.requestRefund.tr(context: context), + // textColor: AppColors.blackColor, + // backgroundColor: AppColors.whiteColor, + // svgIcon: AppAssets.ancillary_orders_list_icon, + // isLargeText: true, + // iconSize: 36.w, + // ).onPress(() { + // Navigator.of(context).push( + // CustomPageRoute( + // page: const RefundRequestCreatePage(initialTabIndex: 0), + // ), + // ); + // }), MedicalFileCard( label: LocaleKeys.ancillaryOrdersListNew.tr(context: context), textColor: AppColors.blackColor, diff --git a/lib/presentation/my_invoices/widgets/invoice_list_card.dart b/lib/presentation/my_invoices/widgets/invoice_list_card.dart index 465c1c0b..2cafdc57 100644 --- a/lib/presentation/my_invoices/widgets/invoice_list_card.dart +++ b/lib/presentation/my_invoices/widgets/invoice_list_card.dart @@ -1,3 +1,5 @@ +import 'dart:ui' as ui; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; @@ -10,11 +12,11 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoices_list_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_create_page.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; - -import 'dart:ui' as ui; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; class InvoiceListCard extends StatelessWidget { final GetInvoicesListResponseModel getInvoicesListResponseModel; @@ -26,11 +28,7 @@ class InvoiceListCard extends StatelessWidget { Widget build(BuildContext context) { AppState appState = getIt(); return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: true, - ), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), child: Padding( padding: EdgeInsets.all(14.h), child: Column( @@ -49,7 +47,7 @@ class InvoiceListCard extends StatelessWidget { textColor: AppColors.textColor, ), AppCustomChipWidget( - labelText: appState.isArabic() ? "العيادات الخارجيه" :"Out Patient", + labelText: appState.isArabic() ? "العيادات الخارجيه" : "Out Patient", backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.1), textColor: AppColors.warningColorYellow, ), @@ -98,9 +96,13 @@ class InvoiceListCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ (getIt().isArabic() - ? (getInvoicesListResponseModel.doctorNameN ?? getInvoicesListResponseModel.doctorName ?? LocaleKeys.doctor.tr(context: context)) - : (getInvoicesListResponseModel.doctorName ?? getInvoicesListResponseModel.doctorNameN ?? LocaleKeys.doctor.tr(context: context)) - ).toText16(isBold: true), + ? (getInvoicesListResponseModel.doctorNameN ?? + getInvoicesListResponseModel.doctorName ?? + LocaleKeys.doctor.tr(context: context)) + : (getInvoicesListResponseModel.doctorName ?? + getInvoicesListResponseModel.doctorNameN ?? + LocaleKeys.doctor.tr(context: context))) + .toText16(isBold: true), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, @@ -128,7 +130,8 @@ class InvoiceListCard extends StatelessWidget { child: AppCustomChipWidget( labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), icon: AppAssets.doctor_calendar_icon, - labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(getInvoicesListResponseModel.appointmentDate), false), + labelText: + DateUtil.formatDateToDate(DateUtil.convertStringToDate(getInvoicesListResponseModel.appointmentDate), false), isEnglishOnly: true, ), ), @@ -139,25 +142,61 @@ class InvoiceListCard extends StatelessWidget { ), ], ), - SizedBox(height: 16.h), - CustomButton( - // text: LocaleKeys.viewInvoiceDetails.tr(context: context), - text: Utils.isVidaPlusProject(getInvoicesListResponseModel.projectId ?? 0) ? LocaleKeys.sendEmail.tr(context: context) : LocaleKeys.downloadInvoice.tr(context: context), - onPressed: () { - if (onTap != null) { - onTap!(); - } - }, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), - borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), - textColor: AppColors.primaryRedColor, - fontSize: 14.f, - fontWeight: FontWeight.w600, - borderRadius: 12.r, - padding: EdgeInsets.symmetric(horizontal: 10.w), - height: 40.h, - iconSize: 14.h, - ), + if (onTap != null) ...[ + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: CustomButton( + // text: LocaleKeys.viewInvoiceDetails.tr(context: context), + text: Utils.isVidaPlusProject(getInvoicesListResponseModel.projectId ?? 0) + ? LocaleKeys.sendEmail.tr(context: context) + : LocaleKeys.downloadInvoice.tr(context: context), + onPressed: () { + if (onTap != null) { + onTap!(); + } + }, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.1), + borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), + textColor: AppColors.primaryRedColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + iconSize: 14.h, + ), + ), + if (getInvoicesListResponseModel.isAllowRefund == true) ...[ + SizedBox(width: 8.w), + Expanded( + child: CustomButton( + text: LocaleKeys.requestRefund.tr(context: context), + onPressed: () { + Navigator.of(context).push( + CustomPageRoute( + page: RefundRequestCreatePage(initialTabIndex: 0, invoice: getInvoicesListResponseModel), + ), + ); + }, + icon: AppAssets.refundIcon, + iconColor: AppColors.caloriesCalculatorColor, + backgroundColor: AppColors.blueColor.withValues(alpha: 0.14), + borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), + textColor: AppColors.caloriesCalculatorColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + iconSize: 14.h, + ), + ), + ] + ], + ), + ], ], ), ), diff --git a/lib/presentation/refund_request/refund_history_screen.dart b/lib/presentation/refund_request/refund_history_screen.dart new file mode 100644 index 00000000..04f369d0 --- /dev/null +++ b/lib/presentation/refund_request/refund_history_screen.dart @@ -0,0 +1,258 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:provider/provider.dart'; + +class RefundHistoryScreen extends StatefulWidget { + const RefundHistoryScreen({super.key}); + + @override + State createState() => _RefundHistoryScreenState(); +} + +class _RefundHistoryScreenState extends State { + late AppState _appState; + + @override + void initState() { + super.initState(); + _appState = getIt(); + final vm = context.read(); + scheduleMicrotask(() async { + await vm.getRefundInvoices(); + }); + } + + // ── Helpers ─────────────────────────────────���───────────────────────────── + + String _formatDate(String? rawDate) { + if (rawDate == null || rawDate.isEmpty) return ''; + try { + // Handle WCF /Date(...)/ format + final match = RegExp(r'/Date\((-?\d+)').firstMatch(rawDate); + if (match != null) { + final ms = int.parse(match.group(1)!); + return DateFormat('MMM dd, yyyy').format(DateTime.fromMillisecondsSinceEpoch(ms)); + } + return DateFormat('MMM dd, yyyy').format(DateTime.parse(rawDate)); + } catch (_) { + return rawDate; + } + } + + // ── Shimmer ─────────────────────────────────────────────────────────────── + + Widget _buildLoadingShimmer() { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.only(top: 24.h), + itemCount: 4, + separatorBuilder: (_, __) => SizedBox(height: 16.h), + itemBuilder: (_, __) => _buildInvoiceCard(null, isLoading: true), + ); + } + + // ── Card ────────────────────────────────────────────────────────────────── + + Widget _buildInvoiceCard(RefundInvoiceModel? invoice, {bool isLoading = false}) { + final isArabic = _appState.isArabic(); + + final clinicName = isLoading ? 'Clinic Name' : (isArabic ? (invoice?.clinicNameN ?? invoice?.clinicName) : invoice?.clinicName) ?? '-'; + + final projectName = isLoading ? 'Hospital Name' : (isArabic ? (invoice?.projectNameN ?? invoice?.projectName) : invoice?.projectName) ?? '-'; + + final invoiceNo = isLoading ? 'INV-000000' : (invoice?.invoiceNumber ?? '-'); + final invoiceDate = isLoading ? 'Jan 01, 2024' : _formatDate(invoice?.invoiceDate); + final totalAmount = isLoading ? '0.00' : (invoice?.totalAmount?.toStringAsFixed(2) ?? '-'); + final patientShare = isLoading ? '0.00' : (invoice?.patientShare?.toStringAsFixed(2) ?? '-'); + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Invoice No + Date row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (!isLoading) + LocaleKeys.invoiceNo.tr(context: context).toText12( + color: AppColors.textColorLight, + isBold: true, + ), + if (!isLoading) SizedBox(width: 4.w), + invoiceNo.toText14(isBold: true, isEnglishOnly: true).toShimmer2(isShow: isLoading), + ], + ), + invoiceDate.toText12(color: AppColors.textColorLight, isBold: true).toShimmer2(isShow: isLoading), + ], + ), + + SizedBox(height: 12.h), + + // Chips — clinic & hospital + Wrap( + spacing: 6.w, + runSpacing: 6.h, + children: [ + AppCustomChipWidget( + icon: "", + labelText: clinicName, + ).toShimmer2(isShow: isLoading), + AppCustomChipWidget( + icon: "", + labelText: projectName, + ).toShimmer2(isShow: isLoading), + ], + ), + + SizedBox(height: 12.h), + + // Amounts row + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.totalAmount + .tr(context: context) + .toText12(color: AppColors.textColorLight, isBold: true) + .toShimmer2(isShow: isLoading), + SizedBox(height: 4.h), + Utils.getPaymentAmountWithSymbol( + totalAmount.toText16(isBold: true, isEnglishOnly: true), + AppColors.blackColor, + 11, + isSaudiCurrency: true, + isExpanded: false, + ).toShimmer2(isShow: isLoading), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.patientShare + .tr(context: context) + .toText12(color: AppColors.textColorLight, isBold: true) + .toShimmer2(isShow: isLoading), + SizedBox(height: 4.h), + Utils.getPaymentAmountWithSymbol( + patientShare.toText16(isBold: true, isEnglishOnly: true), + AppColors.primaryRedColor, + 11, + isSaudiCurrency: true, + isExpanded: false, + ).toShimmer2(isShow: isLoading), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } + + // ── Empty ───────────────────────────────────────────────────────────────── + + Widget _buildEmptyState() { + return Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 40.h), + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 12.r, + hasShadow: false, + ), + child: Utils.getNoDataWidget( + context, + noDataText: LocaleKeys.noRefundHistoryFound.tr(context: context), + isSmallWidget: true, + width: 62.w, + height: 62.h, + ), + ), + ), + ); + } + + // ── Build ───────────────────────────────────────────────────────────────── + + @override + Widget build(BuildContext context) { + return CollapsingListView( + title: LocaleKeys.refundHistory.tr(context: context), + isLeading: true, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Consumer( + builder: (context, viewModel, _) { + if (viewModel.isRefundInvoicesLoading) { + return _buildLoadingShimmer(); + } + + if (viewModel.refundInvoicesList.isEmpty) { + return _buildEmptyState(); + } + + return ListView.separated( + padding: EdgeInsets.only(top: 24.h), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: viewModel.refundInvoicesList.length, + separatorBuilder: (_, __) => SizedBox(height: 12.h), + itemBuilder: (context, index) { + final invoice = viewModel.refundInvoicesList[index]; + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: _buildInvoiceCard(invoice), + ), + ), + ); + }, + ); + }, + ), + ], + ).paddingSymmetrical(24.w, 0), + ), + ); + } +} diff --git a/lib/presentation/refund_request/refund_request_create_page.dart b/lib/presentation/refund_request/refund_request_create_page.dart index 76ae7520..284d4581 100644 --- a/lib/presentation/refund_request/refund_request_create_page.dart +++ b/lib/presentation/refund_request/refund_request_create_page.dart @@ -9,50 +9,95 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoices_list_response_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/req_models/submit_refund_request_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/habib_wallet/widgets/select_hospital_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/presentation/my_invoices/widgets/invoice_list_card.dart'; +import 'package:hmg_patient_app_new/presentation/refund_request/refund_history_screen.dart'; +import 'package:hmg_patient_app_new/presentation/refund_request/widgets/refund_reason_bottom_sheet.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; -import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/radio_list_tile_widget.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:hmg_patient_app_new/widgets/stepper/stepper_widget.dart'; import 'package:provider/provider.dart'; class RefundRequestCreatePage extends StatefulWidget { + final GetInvoicesListResponseModel invoice; + /// 0 = Invoices (default), 1 = HMG Wallet final int initialTabIndex; - const RefundRequestCreatePage({super.key, this.initialTabIndex = 0}); + const RefundRequestCreatePage({ + super.key, + required this.invoice, + this.initialTabIndex = 0, + }); @override State createState() => _RefundRequestCreatePageState(); } class _RefundRequestCreatePageState extends State { - late RefundRequestViewModel _vm; - late HabibWalletViewModel _habibWalletVM; + late RefundRequestViewModel _refundRequestViewModel; + late HabibWalletViewModel _habibWalletViewModel; late AppState _appState; - // TODO: Replace with actual procedure list fetched from API per invoice - List _mockProceduresForInvoice(int invoiceNo) => [ - RefundableProcedure(procedureId: 101, procedureName: 'Consultation', procedureNameN: 'استشارة', patientShare: 120.0), - RefundableProcedure(procedureId: 102, procedureName: 'Blood Test – CBC', procedureNameN: 'تحليل دم شامل', patientShare: 85.50), - RefundableProcedure(procedureId: 103, procedureName: 'X-Ray Chest', procedureNameN: 'أشعة سينية صدر', patientShare: 200.0), - RefundableProcedure(procedureId: 104, procedureName: 'ECG', procedureNameN: 'تخطيط قلب', patientShare: 150.0), - ]; + final TextEditingController _ibanController = TextEditingController(); + final TextEditingController _descriptionController = TextEditingController(); + final FocusNode _ibanFocusNode = FocusNode(); + final FocusNode _descriptionFocusNode = FocusNode(); @override void initState() { super.initState(); _appState = getIt.get(); WidgetsBinding.instance.addPostFrameCallback((_) { - // Load hospital list the same way RechargeWalletPage does - _habibWalletVM.getProjectsList(); + _refundRequestViewModel = context.read(); + _ibanController.addListener(() => _refundRequestViewModel.triggerRebuild()); + _descriptionController.addListener(() => _refundRequestViewModel.triggerRebuild()); + _loadInvoiceDetails(); }); } + @override + void dispose() { + _ibanController.dispose(); + _descriptionController.dispose(); + _ibanFocusNode.dispose(); + _descriptionFocusNode.dispose(); + _refundRequestViewModel.resetState(); + super.dispose(); + } + + void _loadInvoiceDetails() { + final invoiceNo = widget.invoice.invoiceNo; + final projectId = widget.invoice.projectId; + if (invoiceNo == null || projectId == null) return; + + _refundRequestViewModel.getRefundableInvoicesDetails( + invoiceNo: invoiceNo, + projectId: projectId, + onSuccess: (details) { + final procedures = details.refundableInvoiceDetailsItems + .map((item) => RefundableProcedure( + procedureId: item.lineItemNo ?? 0, + procedureName: item.procedureName ?? '', + patientShare: item.patientPaidAmount ?? 0.0, + apiLineItemNo: item.lineItemNo, + apiProcedureId: item.procedureId, + )) + .toList(); + _refundRequestViewModel.setProcedures(procedures); + }, + ); + } + // ── Stepper ─────────────────────────────────────────────────────────────── Widget _buildStepper(RefundRequestViewModel vm) { @@ -70,7 +115,7 @@ class _RefundRequestCreatePageState extends State { itemWidth, isActive ? AppColors.primaryRedColor : AppColors.greyColor, isThumb, - 4.h, + 6.h, ); }), ); @@ -82,63 +127,211 @@ class _RefundRequestCreatePageState extends State { // ── Step 1 — exactly mirrors RechargeWalletPage hospital row ───────────── Widget _buildStep1Content(RefundRequestViewModel vm) { - final isArabic = _appState.isArabic(); return Consumer( builder: (context, refundVM, _) { - return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.select_hospital_icon, - width: 40.h, - height: 40.h, - applyThemeColor: false, - ), - SizedBox(width: 8.h), - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - LocaleKeys.hospital.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), - (refundVM.selectedHospital != null - ? ((isArabic - ? (refundVM.selectedHospital!.nameN?.toString() ?? refundVM.selectedHospital!.name) - : refundVM.selectedHospital!.name) ?? - LocaleKeys.selectHospital.tr(context: context)) - : LocaleKeys.selectHospital.tr(context: context)) - .toText14(color: AppColors.textColor, isBold: true), - ]), - ], + final isArabic = _appState.isArabic(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.selectProcedures.tr(context: context).toText20(isBold: true), + SizedBox(height: 24.h), + if (vm.isProceduresLoading) + ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => SizedBox(height: 16.h), + itemCount: 4, + itemBuilder: (_, index) => Container( + height: 40.h, + margin: EdgeInsets.only(bottom: 24.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.h, + ), + ).toShimmer2(isShow: true, radius: 16.r), + ) + else if (vm.procedures.isEmpty) + Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 24.h), + child: LocaleKeys.noProceduresAvailableForRefund.tr().toText14(color: AppColors.greyTextColor), ), - Utils.buildSvgWithAssets( - icon: AppAssets.arrow_down, - width: 25.h, - height: 25.h, + ) + else + AnimationLimiter( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: false), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Select All row + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _refundRequestViewModel.toggleSelectAll(), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + SizedBox( + width: 24.w, + height: 24.w, + child: Transform.scale( + scale: 24.w / 22, + child: Checkbox( + value: vm.isAllSelected, + onChanged: (_) => _refundRequestViewModel.toggleSelectAll(), + activeColor: AppColors.primaryRedColor, + side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.r)), + ), + ), + ), + SizedBox(width: 8.w), + LocaleKeys.selectAll.tr(context: context).toText16(isBold: true), + ], + ), + ), + ), + ), + Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w), + ...List.generate( + vm.procedures.length, + (index) => _buildProcedureCard(vm.procedures[index], index, vm.procedures.length), + ), + ], + ), ), - ], - ).onPress(() { - showCommonBottomSheetWithoutHeight( - context, - title: LocaleKeys.selectHospital.tr(context: context), - isDismissible: false, - child: SelectHospitalBottomSheet( - onHospitalSelected: (hospital) => _vm.setSelectedHospital(hospital), + ), + ], + ); + }, + ); + } + + // ── Step 2 — payment method selection ──────────────────────────────────── + + Widget _buildStep2Content(RefundRequestViewModel vm) { + final paymentOptions = [ + RefundTabType.hmgWallet, + RefundTabType.invoices, + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.selectRefundMethod.tr(context: context).toText20(isBold: true), + SizedBox(height: 24.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: paymentOptions.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final type = paymentOptions[index]; + return Column( + children: [ + RadioListTileWidget( + value: type, + groupValue: vm.selectedPaymentMethod, + title: type.toDisplayString(), + onChanged: (value) { + if (value != null) vm.setPaymentMethod(value); + }, ), - callBackFunc: () {}, + if (index != paymentOptions.length - 1) + Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w), + ], ); - }), + }, ), - ); - }, + ), + if (vm.selectedPaymentMethod == RefundTabType.invoices) ...[ + SizedBox(height: 16.h), + _buildBankInputDetails(vm), + ], + ], + ); + } + + // ── Bank Transfer Input ─────────────────────────────────────────────────── + + Widget _buildBankInputDetails(RefundRequestViewModel vm) { + return Container( + padding: EdgeInsets.all(16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + ), + child: Column( + children: [ + TextInputWidget( + labelText: LocaleKeys.addIbanDetails.tr(), + hintText: "SA0000000000000000000000", + controller: _ibanController, + focusNode: _ibanFocusNode, + leadingIconNeedsContainer: false, + autoFocus: false, + keyboardType: TextInputType.text, + isEnable: true, + isReadOnly: false, + prefix: null, + isBorderAllowed: false, + isAllowLeadingIcon: true, + fontSize: 14.f, + isCountryDropDown: false, + leadingIcon: AppAssets.bankIcon, + fontFamily: "Poppins", + hasError: vm.ibanError != null, + errorMessage: vm.ibanError, + onChange: (value) { + vm.clearIbanError(); + }, + onSubmitted: (value) { + _descriptionFocusNode.requestFocus(); + }, + ), + Divider(color: AppColors.bottomNAVBorder, thickness: 1).withVerticalPadding(16.h), + TextInputWidget( + labelText: LocaleKeys.description.tr(), + hintText: LocaleKeys.enterDescription.tr(), + controller: _descriptionController, + focusNode: _descriptionFocusNode, + autoFocus: false, + keyboardType: TextInputType.text, + leadingIconNeedsContainer: false, + isEnable: true, + isReadOnly: false, + prefix: null, + isBorderAllowed: false, + isAllowLeadingIcon: true, + fontSize: 14.f, + isCountryDropDown: false, + leadingIcon: AppAssets.descriptionIcon, + fontFamily: "Poppins", + hasError: vm.descriptionError != null, + errorMessage: vm.descriptionError, + onChange: (value) { + vm.clearDescriptionError(); + }, + ), + ], + ), ); } // ── Step 2 — procedure checkboxes ───────────────────────────────────────── - Widget _buildProcedureCard(RefundableProcedure procedure, int index) { + Widget _buildProcedureCard(RefundableProcedure procedure, int index, int length) { final isArabic = _appState.isArabic(); final name = isArabic && procedure.procedureNameN != null ? procedure.procedureNameN! : procedure.procedureName; @@ -151,46 +344,87 @@ class _RefundRequestCreatePageState extends State { child: AnimatedContainer( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, - margin: EdgeInsets.only(bottom: 12.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: procedure.isSelected ? AppColors.primaryRedColor.withAlpha(12) : AppColors.whiteColor, - borderRadius: 16.h, - hasShadow: true, - ), child: Material( color: Colors.transparent, child: InkWell( - onTap: () => _vm.toggleProcedureSelection(procedure), - borderRadius: BorderRadius.circular(16.h), + onTap: () => _refundRequestViewModel.toggleProcedureSelection(procedure), child: Padding( - padding: EdgeInsets.all(14.h), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Checkbox( - value: procedure.isSelected, - onChanged: (_) => _vm.toggleProcedureSelection(procedure), - activeColor: AppColors.primaryRedColor, - side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1.5), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: VisualDensity.compact, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4.r), - ), - ), - SizedBox(width: 8.w), - Expanded(child: name.toText14(isBold: true)), - SizedBox(width: 8.w), - Utils.getPaymentAmountWithSymbol( - procedure.patientShare.toStringAsFixed(2).toText14( - isBold: true, - isEnglishOnly: true, - color: AppColors.primaryRedColor, + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 24.w, + height: 24.w, + child: Transform.scale( + scale: 24.w / 22, + child: Checkbox( + value: procedure.isSelected, + onChanged: (_) => _refundRequestViewModel.toggleProcedureSelection(procedure), + activeColor: AppColors.primaryRedColor, + side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4.r), + ), + ), ), - AppColors.textColorLight, - 13, - isSaudiCurrency: true, + ), + SizedBox(width: 8.w), + Expanded(child: name.toText16(isBold: true)), + SizedBox(width: 8.w), + Utils.getPaymentAmountWithSymbol( + procedure.patientShare.toStringAsFixed(2).toText20( + isBold: true, + isEnglishOnly: true, + color: AppColors.textColor, + ), + AppColors.textColor, + 10.f, + isSaudiCurrency: true, + ), + ], ), + if (procedure.isSelected) ...[ + InkWell( + onTap: () => showRefundReasonBottomSheet( + context, + selectedReason: procedure.refundReason, + onDone: (reason) => _refundRequestViewModel.setRefundReasonForProcedure(procedure, reason), + ), + child: Padding( + padding: EdgeInsets.only(top: 12.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.refundReason.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), + (procedure.refundReason ?? LocaleKeys.selectRefundReason.tr(context: context)).toText14( + color: procedure.refundReason != null ? AppColors.textColor : AppColors.greyTextColor, isBold: true), + ], + ), + ), + Utils.buildSvgWithAssets( + icon: AppAssets.arrow_down, + width: 25.h, + height: 25.h, + ), + ], + ), + ), + ), + ], + if (index != length - 1) ...[ + Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingOnly(top: 16.h), + ] ], ), ), @@ -202,69 +436,6 @@ class _RefundRequestCreatePageState extends State { ); } - Widget _buildStep2Content(RefundRequestViewModel vm) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w), - child: Text( - 'Select Procedures', - style: TextStyle( - fontSize: 20.f, - fontWeight: FontWeight.w600, - color: AppColors.blackColor, - letterSpacing: -0.5, - ), - ), - ), - SizedBox(height: 24.h), - if (vm.isProceduresLoading) - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: EdgeInsets.symmetric(horizontal: 24.w), - itemCount: 4, - itemBuilder: (_, index) => Container( - height: 56.h, - margin: EdgeInsets.only(bottom: 12.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 16.h, - ), - ).toShimmer2(isShow: true, radius: 16.r), - ) - else if (vm.procedures.isEmpty) - Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 24.h), - child: 'No procedures available for refund'.toText14(color: AppColors.greyTextColor), - ), - ) - else - AnimationLimiter( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w), - child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.bgScaffoldColor, - borderRadius: 24.h, - hasShadow: false, - ), - padding: EdgeInsets.all(12.w), - child: Column( - children: List.generate( - vm.procedures.length, - (index) => _buildProcedureCard(vm.procedures[index], index), - ), - ), - ), - ), - ), - ], - ); - } - // ── Bottom bar ──────────────────────────────────────────────────────────── Widget _buildBottomBar(RefundRequestViewModel vm) { @@ -280,21 +451,18 @@ class _RefundRequestCreatePageState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (isStep2 && !isWallet && vm.selectedProcedures.isNotEmpty) ...[ + if (!isWallet && vm.selectedProcedures.isNotEmpty) ...[ Padding( padding: EdgeInsets.fromLTRB(24.w, 16.h, 24.w, 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - 'Total Refundable Amount'.toText12( - color: AppColors.textColorLight, - isBold: true, - ), + LocaleKeys.totalAmount.toText18(color: AppColors.textColor, isBold: true), Utils.getPaymentAmountWithSymbol( - vm.totalRefundableAmount.toStringAsFixed(2).toText18( + vm.totalRefundableAmount.toStringAsFixed(2).toText24( isBold: true, isEnglishOnly: true, - color: AppColors.primaryRedColor, + color: AppColors.textColor, ), AppColors.textColorLight, 14, @@ -305,15 +473,28 @@ class _RefundRequestCreatePageState extends State { ), ], CustomButton( + icon: isStep2 ? AppAssets.refundIcon : null, text: isStep2 ? LocaleKeys.submit.tr(context: context) : LocaleKeys.next.tr(context: context), onPressed: () { if (!isStep2) { - if (isWallet || vm.isStep1Valid) vm.nextStep(); + if (vm.selectedProcedures.isNotEmpty) vm.nextStep(); } else { - // TODO: Submit API call + final isBankTransfer = vm.selectedPaymentMethod == RefundTabType.invoices; + if (isBankTransfer) { + final isValid = vm.validateBankTransferForm( + iban: _ibanController.text, + description: _descriptionController.text, + ); + if (!isValid) return; + } + _handleInvoiceRefundRequestSubmit(vm); } }, - isDisabled: (!isWallet && !isStep2 && !vm.isStep1Valid) || (isStep2 && !isWallet && !vm.isStep2Valid), + isDisabled: isStep2 + ? (vm.selectedPaymentMethod == null || + (vm.selectedPaymentMethod == RefundTabType.invoices && + (_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty))) + : (vm.selectedProcedures.isEmpty || !vm.allSelectedProceduresHaveReasons), ).paddingSymmetrical(24.h, 24.h), ], ), @@ -322,46 +503,145 @@ class _RefundRequestCreatePageState extends State { // ── HMG Wallet placeholder ───────────────────────────────────────────────── - Widget _buildHmgWalletContent() { - return KeyedSubtree( - key: const ValueKey('hmgWallet'), + Widget buildInvoiceCard() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: InvoiceListCard(onTap: null, getInvoicesListResponseModel: widget.invoice), + ); + } + + // ── Submit ──────────────────────────────────────────────────────────────── + + void _handleInvoiceRefundRequestSubmit(RefundRequestViewModel vm) { + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.notice.tr(context: context), + child: Utils.getWarningWidget( + loadingText: LocaleKeys.confirmSubmitRequest.tr(context: context), + isShowActionButtons: true, + onCancelTap: () => Navigator.pop(context), + onConfirmTap: () async { + Navigator.pop(context); + LoaderBottomSheet.showLoader(); + + final isBankTransfer = vm.selectedPaymentMethod == RefundTabType.invoices; + final invoice = widget.invoice; + final userPayload = _appState.getAuthenticatedUser(); + final patientId = userPayload!.patientId; + final patientName = "${userPayload.firstName} ${userPayload.lastName}"; + final patientMobileNumber = "${userPayload.mobileNumber}"; + final patientNationalId = userPayload.patientIdentificationNo.toString(); + final request = SubmitRefundRequestModel( + projectId: invoice.projectId ?? 0, + refundType: '1', + // 1 = invoice refund + refundProject: invoice.projectName ?? '', + refundProjectId: invoice.projectId ?? 0, + + // TODO: It should be removed + refundPatientId: 4768571.toString(), + refundPatientName: patientName, + refundNationalId: patientNationalId, + patientMobileNumber: patientMobileNumber, + refundInvoiceNo: 260000075.toString(), + // refundInvoiceNo: (invoice.invoiceNo ?? 0).toString(), TODO: It should be removed + refundAppointmentNo: (invoice.appointmentNo ?? 0).toString(), + refundDoctorId: invoice.doctorId ?? 0, + refundDoctorName: invoice.doctorName ?? '', + refundClinicId: invoice.clinicId ?? 0, + refundClinicName: invoice.clinicName ?? '', + refundDestination: isBankTransfer ? '2' : '1', + refundIban: isBankTransfer ? _ibanController.text.trim() : '', + refundRemarks: isBankTransfer ? _descriptionController.text.trim() : '', + refundTotalAmount: vm.totalRefundableAmount, + refundProceduresList: vm.selectedProcedures + .map((p) => SubmitRefundProcedureItem( + lineItemNo: p.apiLineItemNo ?? 0, + procedureId: p.apiProcedureId ?? '', + procedureName: p.procedureName, + patientPaidAmount: p.patientShare, + refundReason: p.refundReason ?? '', + )) + .toList(), + ); + + await vm.submitRefund( + request: request, + onSuccess: (response) { + LoaderBottomSheet.hideLoader(); + _showSuccessBottomSheet(response.refNo ?? ''); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + ); + }, + ); + }, + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + + void _showSuccessBottomSheet(String refNo) { + showCommonBottomSheetWithoutHeight( + context, child: Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w), - child: Container( - width: double.infinity, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: true, - ), - padding: EdgeInsets.all(24.w), - child: Column( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.wallet, - iconColor: AppColors.primaryRedColor, - width: 48.h, - height: 48.h, + padding: EdgeInsets.all(16.w), + child: Column( + children: [ + Utils.getSuccessWidget(loadingText: LocaleKeys.requestSubmittedSuccessfully.tr(context: context)), + if (refNo.isNotEmpty) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LocaleKeys.hereIsYourRequestNumber.tr(context: context).toText14( + color: AppColors.textColorLight, + weight: FontWeight.w600, + ), + SizedBox(width: 4.w), + refNo.toText16(isBold: true), + ], ), - SizedBox(height: 16.h), - 'HMG Wallet Refund'.toText16(isBold: true), - SizedBox(height: 8.h), - 'Refund requests to HMG Wallet will be processed within 1-3 business days.'.toText14(color: AppColors.greyTextColor, isCenter: true), ], - ), + SizedBox(height: 24.h), + Row( + children: [ + Expanded( + child: CustomButton( + height: 56.h, + text: LocaleKeys.ok.tr(), + onPressed: () { + Navigator.pop(context); // pop success sheet + Navigator.pop(context); // pop refund page + }, + textColor: AppColors.whiteColor, + ), + ), + ], + ), + ], ), ), + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false, + isAutoDismiss: false, ); } - // ── Build ───────────────────────────────────────────────────────────────── +// ── Build ───────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { - _habibWalletVM = context.read(); + _habibWalletViewModel = context.read(); return Consumer( builder: (context, vm, _) { - _vm = vm; return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: Column( @@ -369,38 +649,18 @@ class _RefundRequestCreatePageState extends State { Expanded( child: CollapsingListView( title: LocaleKeys.requestRefund.tr(context: context), - history: () async { - // TODO: Navigate to refund history page - }, + requests: () => Navigator.of(context).push(CustomPageRoute(page: RefundHistoryScreen())), child: SingleChildScrollView( padding: EdgeInsets.only(bottom: 32.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Tab bar - Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w), - child: CustomTabBar( - initialIndex: widget.initialTabIndex, - activeTextColor: AppColors.primaryRedColor, - activeBackgroundColor: AppColors.primaryRedColor.withAlpha(25), - tabs: [ - CustomTabBarModel(null, 'Invoices'), - CustomTabBarModel(null, 'HMG Wallet'), - ], - onTabChange: (index) { - vm.setTab(index == 0 ? RefundTabType.invoices : RefundTabType.hmgWallet); - vm.goToStep(0); - }, - ), - ), - SizedBox(height: 24.h), // Stepper (invoices tab only) if (vm.selectedTab == RefundTabType.invoices) ...[ + buildInvoiceCard(), _buildStepper(vm), - SizedBox(height: 24.h), + SizedBox(height: 16.h), ], - // Content AnimatedSwitcher( duration: const Duration(milliseconds: 350), @@ -414,20 +674,21 @@ class _RefundRequestCreatePageState extends State { child: child, ), ), - child: vm.selectedTab == RefundTabType.hmgWallet - ? _buildHmgWalletContent() - : vm.currentStep == 0 - ? KeyedSubtree( - key: const ValueKey('step1'), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 24.w), - child: _buildStep1Content(vm), - ), - ) - : KeyedSubtree( - key: const ValueKey('step2'), - child: _buildStep2Content(vm), - ), + child: vm.currentStep == 0 + ? KeyedSubtree( + key: const ValueKey('step1'), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: _buildStep1Content(vm), + ), + ) + : KeyedSubtree( + key: const ValueKey('step2'), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: _buildStep2Content(vm), + ), + ), ), ], ), diff --git a/lib/presentation/refund_request/widgets/refund_reason_bottom_sheet.dart b/lib/presentation/refund_request/widgets/refund_reason_bottom_sheet.dart new file mode 100644 index 00000000..5fa8e5ad --- /dev/null +++ b/lib/presentation/refund_request/widgets/refund_reason_bottom_sheet.dart @@ -0,0 +1,111 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/radio_list_tile_widget.dart'; + +void showRefundReasonBottomSheet( + BuildContext context, { + required String? selectedReason, + required ValueChanged onDone, +}) { + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.selectRefundReason.tr(context: context), + isDismissible: true, + child: _RefundReasonSheetContent( + selectedReason: selectedReason, + onDone: onDone, + ), + callBackFunc: () {}, + ); +} + +class _RefundReasonSheetContent extends StatefulWidget { + final String? selectedReason; + final ValueChanged onDone; + + const _RefundReasonSheetContent({ + required this.selectedReason, + required this.onDone, + }); + + @override + State<_RefundReasonSheetContent> createState() => _RefundReasonSheetContentState(); +} + +class _RefundReasonSheetContentState extends State<_RefundReasonSheetContent> { + String? _selected; + + static List _reasons(BuildContext context) => [ + LocaleKeys.refundReason1.tr(context: context), + LocaleKeys.refundReason2.tr(context: context), + LocaleKeys.refundReason3.tr(context: context), + LocaleKeys.refundReason4.tr(context: context), + LocaleKeys.refundReason5.tr(context: context), + LocaleKeys.refundReason6.tr(context: context), + LocaleKeys.refundReason7.tr(context: context), + ]; + + @override + void initState() { + super.initState(); + _selected = widget.selectedReason; + } + + @override + Widget build(BuildContext context) { + final reasons = _reasons(context); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: reasons.length, + separatorBuilder: (_, __) => Divider( + color: AppColors.bottomNAVBorder, + height: 1, + thickness: 1, + ).paddingSymmetrical(16.w, 0.w), + itemBuilder: (context, index) { + final reason = reasons[index]; + return RadioListTileWidget( + value: reason, + groupValue: _selected, + title: reason, + onChanged: (value) { + if (value != null) setState(() => _selected = value); + }, + ); + }, + ), + ), + SizedBox(height: 16.h), + CustomButton( + text: LocaleKeys.done.tr(context: context), + isDisabled: _selected == null, + onPressed: () { + if (_selected != null) { + widget.onDone(_selected!); + Navigator.of(context).pop(); + } + }, + ).paddingSymmetrical(0, 0), + SizedBox(height: 8.h), + ], + ); + } +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 9170e039..a82879f6 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoices_list_response_model.dart'; import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; import 'package:hmg_patient_app_new/presentation/authentication/register.dart'; import 'package:hmg_patient_app_new/presentation/authentication/register_step2.dart'; @@ -10,6 +11,7 @@ import 'package:hmg_patient_app_new/presentation/comprehensive_checkup/comprehen import 'package:hmg_patient_app_new/presentation/covid19test/covid19_landing_page.dart'; import 'package:hmg_patient_app_new/presentation/e_referral/new_e_referral.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/emergency_services_page.dart'; +import 'package:hmg_patient_app_new/presentation/habib_wallet/withdraw_request_create_page.dart'; import 'package:hmg_patient_app_new/presentation/health_calculators_and_converts/health_calculators_page.dart'; import 'package:hmg_patient_app_new/presentation/health_trackers/add_health_tracker_entry_page.dart'; import 'package:hmg_patient_app_new/presentation/health_trackers/health_tracker_detail_page.dart'; @@ -104,6 +106,8 @@ class AppRoutes { // Refund Request static const String refundRequestCreatePage = '/refundRequestCreatePage'; + static const String createWithdrawRequest = '/createWithdrawRequest'; + static const String refundHistoryScreen = '/refundHistoryScreen'; static Map get routes => { initialRoute: (context) => SplashPage(), @@ -166,8 +170,11 @@ class AppRoutes { child: const ParkingPage(), ), refundRequestCreatePage: (context) { - final args = ModalRoute.of(context)?.settings.arguments as int?; - return RefundRequestCreatePage(initialTabIndex: args ?? 0); + final args = ModalRoute.of(context)?.settings.arguments as GetInvoicesListResponseModel; + return RefundRequestCreatePage(invoice: args); + }, + createWithdrawRequest: (context) { + return WithdrawRequestCreatePage(); }, }; } diff --git a/lib/widgets/buttons/custom_button.dart b/lib/widgets/buttons/custom_button.dart index d868755a..3f4902e0 100644 --- a/lib/widgets/buttons/custom_button.dart +++ b/lib/widgets/buttons/custom_button.dart @@ -81,13 +81,18 @@ class CustomButton extends StatelessWidget { children: [ if (icon != null) Padding( - padding: text.isNotEmpty ? EdgeInsets.only(right: 4.w, left: 4.w) : EdgeInsets.zero, - child: Utils.buildSvgWithAssets(icon: icon!, iconColor: iconColor, isDisabled: isButtonDisabled, width: iconS, height: iconS, applyThemeColor: applyThemeColor), + padding: text.isNotEmpty ? EdgeInsets.only(right: 6.w, left: 6.w) : EdgeInsets.zero, + child: Utils.buildSvgWithAssets( + icon: icon!, + iconColor: isButtonDisabled ? AppColors.greyTextColor : iconColor, + isDisabled: isButtonDisabled, + width: iconS, + height: iconS, + applyThemeColor: applyThemeColor), ), if (text.isNotEmpty && icon != null) Flexible( - child: - Text( + child: Text( text, overflow: textOverflow ?? TextOverflow.ellipsis, maxLines: 1, diff --git a/lib/widgets/chip/app_custom_chip_widget.dart b/lib/widgets/chip/app_custom_chip_widget.dart index 58ce21a0..d5d96229 100644 --- a/lib/widgets/chip/app_custom_chip_widget.dart +++ b/lib/widgets/chip/app_custom_chip_widget.dart @@ -8,29 +8,28 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:smooth_corner/smooth_corner.dart'; class AppCustomChipWidget extends StatelessWidget { - const AppCustomChipWidget({ - super.key, - this.labelText, - this.textColor, - this.backgroundColor, - this.iconSize, - this.icon = "", - this.iconColor, - this.richText, - this.iconHasColor = true, - this.shape, - this.deleteIcon, - this.deleteIconSize, - this.deleteIconColor, - this.deleteIconHasColor = false, - this.padding = EdgeInsets.zero, - this.isIconPNG = false, - this.onChipTap, - this.labelPadding, - this.onDeleteTap, - this.applyThemeColor = true, - this.isEnglishOnly = false - }); + const AppCustomChipWidget( + {super.key, + this.labelText, + this.textColor, + this.backgroundColor, + this.iconSize, + this.icon = "", + this.iconColor, + this.richText, + this.iconHasColor = true, + this.shape, + this.deleteIcon, + this.deleteIconSize, + this.deleteIconColor, + this.deleteIconHasColor = false, + this.padding = EdgeInsets.zero, + this.isIconPNG = false, + this.onChipTap, + this.labelPadding, + this.onDeleteTap, + this.applyThemeColor = true, + this.isEnglishOnly = false}); final String? labelText; final Widget? richText; @@ -89,7 +88,8 @@ class AppCustomChipWidget extends StatelessWidget { applyThemeColor: applyThemeColor, ) : SizedBox.shrink(), - label: richText ?? (labelText?? "").toText10(isBold: true, letterSpacing: 0, color: resolvedTextColor, isEnglishOnly: isEnglishOnly), + label: + richText ?? (labelText ?? "").toText10(isBold: true, letterSpacing: 0, color: resolvedTextColor, isEnglishOnly: isEnglishOnly), padding: padding, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, labelPadding: labelPadding ?? EdgeInsetsDirectional.only(end: deleteIcon?.isNotEmpty == true ? 2.w : 8.w), @@ -119,7 +119,9 @@ class AppCustomChipWidget extends StatelessWidget { ) : Chip( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - label: richText ?? (labelText?? "").toText10(isBold: true, letterSpacing: 0, color: resolvedTextColor, isCenter: true, isEnglishOnly: isEnglishOnly), + label: richText ?? + (labelText ?? "") + .toText10(isBold: true, letterSpacing: 0, color: resolvedTextColor, isCenter: true, isEnglishOnly: isEnglishOnly), padding: EdgeInsets.zero, backgroundColor: resolvedBackgroundColor, shape: shape ?? @@ -133,7 +135,11 @@ class AppCustomChipWidget extends StatelessWidget { ? InkWell( onTap: onDeleteTap, child: Utils.buildSvgWithAssets( - icon: deleteIcon!, width: iconS, height: iconS, iconColor: deleteIconHasColor ? resolvedDeleteIconColor : null, applyThemeColor: applyThemeColor)) + icon: deleteIcon!, + width: iconS, + height: iconS, + iconColor: deleteIconHasColor ? resolvedDeleteIconColor : null, + applyThemeColor: applyThemeColor)) : null, onDeleted: deleteIcon?.isNotEmpty == true ? () {} : null, ), diff --git a/lib/widgets/image_picker.dart b/lib/widgets/image_picker.dart index 3fa7d061..68d65bde 100644 --- a/lib/widgets/image_picker.dart +++ b/lib/widgets/image_picker.dart @@ -2,12 +2,12 @@ import 'dart:convert'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/attachment_options.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:image_picker/image_picker.dart'; import 'package:image_picker_android/image_picker_android.dart'; @@ -21,12 +21,13 @@ class ImageOptions { BuildContext context, bool showFilesOption, Function(String, File) image, { + String? title, Future Function()? checkCameraPermission, Future Function()? checkGalleryPermission, }) { showCommonBottomSheetWithoutHeight( context, - title: LocaleKeys.profilePicture.tr(context: context), + title: title ?? LocaleKeys.profilePicture.tr(context: context), child: AttachmentOptions( showFilesOption: showFilesOption, onCameraTap: () async { @@ -223,7 +224,8 @@ void galleryImageAndroid(Function(String, File) image) async { } try { - final XFile? pickedFile = await _pickerPlatform.getImageFromSource(source: ImageSource.gallery, options: const ImagePickerOptions(imageQuality: 20)); + final XFile? pickedFile = + await _pickerPlatform.getImageFromSource(source: ImageSource.gallery, options: const ImagePickerOptions(imageQuality: 20)); if (pickedFile == null) return; @@ -269,7 +271,9 @@ class _BottomSheet extends StatelessWidget { Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(vertical: 12.0), - decoration: BoxDecoration(color: Theme.of(context).scaffoldBackgroundColor, borderRadius: const BorderRadius.only(topLeft: Radius.circular(16.0), topRight: Radius.circular(16.0))), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: const BorderRadius.only(topLeft: Radius.circular(16.0), topRight: Radius.circular(16.0))), child: SafeArea( top: false, child: Column( diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index e77cf96d..7502404c 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -1,3 +1,5 @@ +import 'dart:ui' as ui; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -16,7 +18,6 @@ import 'package:hmg_patient_app_new/widgets/date_range_selector/date_range_calen import 'package:hmg_patient_app_new/widgets/date_range_selector/models/calendar_design_type.dart'; import 'package:hmg_patient_app_new/widgets/dropdown/country_dropdown_widget.dart'; import 'package:hmg_patient_app_new/widgets/time_picker_widget.dart'; -import 'dart:ui' as ui; class TextInputWidget extends StatelessWidget { final String labelText; @@ -54,51 +55,53 @@ class TextInputWidget extends StatelessWidget { final Color? hintColor; final bool? isHideSwitcher; final bool? isArrowTrailing; - + final bool leadingIconNeedsContainer; final String? fontFamily; // final List countryList; // final Function(Country)? onCountryChange; - TextInputWidget( - {super.key, - required this.labelText, - required this.hintText, - this.controller, - this.onChange, - this.onCalendarTypeChanged, - this.prefix, - this.isEnable = true, - this.isBorderAllowed = true, - this.isAllowRadius = true, - this.isReadOnly = false, - this.keyboardType = TextInputType.number, - this.focusNode, - this.autoFocus = false, - this.padding, - this.isAllowLeadingIcon = false, - this.leadingIcon, - this.isCountryDropDown = false, - this.hasError = false, - this.errorMessage, - this.onCountryChange, - this.selectionType, - this.fontSize, - this.isWalletAmountInput = false, - this.suffix, - this.labelColor, - this.onSubmitted, - this.hintColor, - // multiline defaults - this.isMultiline = false, - this.minLines = 3, - this.maxLines = 6, - this.isHideSwitcher, - this.isArrowTrailing, - this.fontFamily, - // this.countryList = const [], - // this.onCountryChange, - this.btnTitle}); + TextInputWidget({ + super.key, + required this.labelText, + required this.hintText, + this.controller, + this.onChange, + this.onCalendarTypeChanged, + this.prefix, + this.isEnable = true, + this.isBorderAllowed = true, + this.isAllowRadius = true, + this.isReadOnly = false, + this.keyboardType = TextInputType.number, + this.focusNode, + this.autoFocus = false, + this.padding, + this.isAllowLeadingIcon = false, + this.leadingIcon, + this.isCountryDropDown = false, + this.hasError = false, + this.errorMessage, + this.onCountryChange, + this.selectionType, + this.fontSize, + this.isWalletAmountInput = false, + this.suffix, + this.labelColor, + this.onSubmitted, + this.hintColor, + // multiline defaults + this.isMultiline = false, + this.minLines = 3, + this.maxLines = 6, + this.isHideSwitcher, + this.isArrowTrailing, + this.fontFamily, + this.leadingIconNeedsContainer = true, + // this.countryList = const [], + // this.onCountryChange, + this.btnTitle, + }); final FocusNode _focusNode = FocusNode(); @@ -220,16 +223,19 @@ class TextInputWidget extends StatelessWidget { } Widget _buildLeadingIcon(BuildContext context) { - return Container( - height: 40.h, - width: 40.h, - // margin: EdgeInsets.symmetric(horizontal: 10.w), - padding: EdgeInsets.all(8.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - borderRadius: 12.r, - color: AppColors.greyColor, - ), - child: Utils.buildSvgWithAssets(icon: leadingIcon!, applyThemeColor: false)); + if (leadingIconNeedsContainer == true) { + return Container( + height: 40.h, + width: 40.h, + // margin: EdgeInsets.symmetric(horizontal: 10.w), + padding: EdgeInsets.all(8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + borderRadius: 12.r, + color: AppColors.greyColor, + ), + child: Utils.buildSvgWithAssets(icon: leadingIcon!, applyThemeColor: false)); + } + return Utils.buildSvgWithAssets(icon: leadingIcon!, applyThemeColor: false, height: 40.h, width: 40.h); } Widget _buildTrailingIcon(BuildContext context, {bool isArrowTrailing = false}) { @@ -403,7 +409,9 @@ class TextInputWidget extends StatelessWidget { hintLocales: const [Locale('en', 'US')], enabled: isEnable, scrollPadding: EdgeInsets.zero, - keyboardType: isMultiline ? TextInputType.multiline : (isWalletAmountInput! ? const TextInputType.numberWithOptions(decimal: true) : keyboardType), + keyboardType: isMultiline + ? TextInputType.multiline + : (isWalletAmountInput! ? const TextInputType.numberWithOptions(decimal: true) : keyboardType), controller: controller, readOnly: isReadOnly, textAlignVertical: TextAlignVertical.top,