From 7ec72460e71e24c9c123cf22f5e916c96dc1f508 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Tue, 9 Jun 2026 10:44:07 +0300 Subject: [PATCH 1/9] added advances list in in wallet refund and other refactoring. --- assets/langs/ar-SA.json | 7 +- assets/langs/en-US.json | 5 +- lib/core/api/api_client.dart | 4 +- lib/core/api_consts.dart | 98 ++--- lib/core/app_state.dart | 36 +- .../blood_donation/blood_donation_repo.dart | 2 +- .../book_appointments_repo.dart | 4 +- .../contact_us/contact_us_view_model.dart | 2 +- .../emergency_services_repo.dart | 2 +- .../habib_wallet/habib_wallet_repo.dart | 2 +- .../habib_wallet/habib_wallet_view_model.dart | 49 ++- .../my_appointments/my_appointments_repo.dart | 2 +- .../my_invoices/my_invoices_repo.dart | 4 +- .../profile_settings_repo.dart | 2 +- lib/features/weather/weather_repo.dart | 2 +- lib/generated/locale_keys.g.dart | 11 +- .../withdraw_request_create_page.dart | 398 +++++++++++++----- .../home/lakum_wallet_details.dart | 2 +- .../home/widgets/lakum_wallet_card.dart | 2 +- .../home/widgets/large_service_card.dart | 4 +- 20 files changed, 426 insertions(+), 212 deletions(-) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index fbd8465f..2c312334 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1888,10 +1888,15 @@ "pharmacy": "الصيدلية", "visitsOrders": "الزيارات/الطلبات", "cannotOpenThisFile": "لا يمكن فتح هذا الملف", + "thisInvoiceIsNotEligibleForRefund": "هذه الفاتورة غير مؤهلة للاسترداد", "refundDetails": "تفاصيل الاسترداد", + "refund": "استرداد", "requestNo": "رقم الطلب.", "referenceNo": "رقم المرجع", "advanceNumber": "رقم الدفعة المقدمة", "amountOnly": "المبلغ", - "invoiceHistory": "سجل الفواتير" + "invoiceHistory": "سجل الفواتير", + "selectAdvance": "اختر الدفعة المقدمة", + "advanceNo": "رقم الدفعة", + "noAdvancesAvailable": "لا توجد دفعات متاحة للاسترداد" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index bfba9725..2fe76d05 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1886,7 +1886,10 @@ "refund": "Refund", "advanceNumber": "Advance Number", "amountOnly": "Amount", - "invoiceHistory": "Invoice History" + "invoiceHistory": "Invoice History", + "selectAdvance": "Select Advance", + "advanceNo": "Advance No", + "noAdvancesAvailable": "No advances available for refund" } diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index f8919251..b1fc0e5b 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -208,8 +208,8 @@ class ApiClientImp implements ApiClient { body['TokenID'] = "@dm!n"; } - // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 4774167; + body['TokenID'] = "@dm!n"; + body['PatientID'] = 4774167; // body['PatientID'] = 89754444; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 4a71fa9b..24872c32 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -290,30 +290,31 @@ class ApiConsts { static String googleCloudStorageENTranslationFileBaseURL = "https://storage.googleapis.com/hmg-patientapp-translations"; // ************ static values for Api **************** - static final double appVersionID = 21.2; + // static final double appVersionID = 21.2; - // static final double appVersionID = 50.7; + static final double appVersionID = 50.7; static final int appChannelId = 3; static final String appIpAddress = "10.20.10.20"; static final String appGeneralId = "Cs2020@2016\$2958"; static final String sessionID = 'TMRhVmkGhOsvamErw'; -} // PACKAGES and OFFERS -var EXA_CART_API_BASE_URL = 'https://mdlaboratories.com/offersdiscounts'; + static String exaCartApiBaseUrl = 'https://mdlaboratories.com/offersdiscounts'; + // var EXA_CART_API_BASE_URL = 'http://10.200.101.75:9000'; -var PACKAGES_CATEGORIES = '/api/categories'; -var PACKAGES_STORES = '/api/stores'; -var PACKAGES_TOKEN = '/api/token'; -var PACKAGES_PRODUCTS = '/api/products'; -var PACKAGES_CUSTOMER = '/api/customers'; -var PACKAGES_SHOPPING_CART = '/api/shopping_cart_items'; -var PACKAGES_ORDERS = '/api/orders'; -var PACKAGES_ORDER_HISTORY = '/api/orders/items'; -var PACKAGES_TAMARA_OPT = '/api/orders/paymentoptions/tamara'; + static String packagesCategories = '/api/categories'; + static String packagesStores = '/api/stores'; + static String packagesToken = '/api/token'; + static String packagesProducts = '/api/products'; + static String packagesCustomer = '/api/customers'; + static String packagesShoppingCart = '/api/shopping_cart_items'; + static String packagesOrders = '/api/orders'; + static String packagesOrderHistory = '/api/orders/items'; + static String packagesTamaraOpt = '/api/orders/paymentoptions/tamara'; + // var BASE_URL = 'http://10.50.100.198:2018/'; // var BASE_URL = 'https://uat.hmgwebservices.com/'; -var BASE_URL = 'https://hmgwebservices.com/'; + // var BASE_URL = 'http://10.201.204.103/'; // var BASE_URL = 'https://orash.cloudsolutions.com.sa/'; // var BASE_URL = 'https://vidauat.cloudsolutions.com.sa/'; @@ -328,10 +329,8 @@ var BASE_URL = 'https://hmgwebservices.com/'; // var PHARMACY_BASE_URL = 'https://uat.hmgwebservices.com/epharmacy/api/'; // // Pharmacy Production URLs -var BASE_PHARMACY_URL = 'https://mdlaboratories.com/exacartapi/api/'; -var PHARMACY_BASE_URL = 'https://mdlaboratories.com/exacartapi/api/'; - -var PHARMACY_REDIRECT_URL = 'https://bit.ly/AlhabibPharmacy'; + static String pharmacyBaseUrl = 'https://mdlaboratories.com/exacartapi/api/'; + static String pharmacyRedirectUrl = 'https://bit.ly/AlhabibPharmacy'; // Pharmacy VidaPlus URLs // var BASE_PHARMACY_URL = 'https://mdlaboratories.com/exacartapitest/api/'; @@ -346,28 +345,30 @@ var PHARMACY_REDIRECT_URL = 'https://bit.ly/AlhabibPharmacy'; // var RC_BASE_URL = 'https://ms.hmg.com/rc/'; -var PING_SERVICE = 'Services/Weather.svc/REST/CheckConnectivity'; + static String pingService = 'Services/Weather.svc/REST/CheckConnectivity'; -var GET_PROJECT_LIST = 'Services/Lists.svc/REST/GetProject'; + static String getProjectsList = 'Services/Lists.svc/REST/GetProject'; -///Geofencing -var GET_GEO_ZONES = 'Services/Patients.svc/REST/GeoF_GetAllPoints'; -var LOG_GEO_ZONES = 'Services/Patients.svc/REST/GeoF_InsertPatientFileInfo'; + ///Geofencing + static String getGeoZones = 'Services/Patients.svc/REST/GeoF_GetAllPoints'; + static String logGeoZones = 'Services/Patients.svc/REST/GeoF_InsertPatientFileInfo'; // Delivery Driver -var DRIVER_LOCATION = 'Services/Patients.svc/REST/PatientER_GetDriverLocation'; + static String driverLocation = 'Services/Patients.svc/REST/PatientER_GetDriverLocation'; //weather -var WEATHER_INDICATOR = 'Services/Weather.svc/REST/GetCityInfo'; + static String weatherIndicator = 'Services/Weather.svc/REST/GetCityInfo'; -var GET_PRIVILEGE = 'Services/Patients.svc/REST/Service_Privilege'; + static String getPrivilege = 'Services/Patients.svc/REST/Service_Privilege'; // Wifi Credentials -var WIFI_CREDENTIALS = "Services/Patients.svc/Hmg_SMS_Get_By_ProjectID_And_PatientID"; + static String wifiCredentials = "Services/Patients.svc/Hmg_SMS_Get_By_ProjectID_And_PatientID"; + + ///Doctor + static String getMyDoctor = 'Services/Doctors.svc/REST/GetPatientDoctorAppointmentResult'; + static String getDoctorProfiles = 'Services/Doctors.svc/REST/GetDocProfiles'; +} -///Doctor -var GET_MY_DOCTOR = 'Services/Doctors.svc/REST/GetPatientDoctorAppointmentResult'; -var GET_DOCTOR_PROFILE = 'Services/Doctors.svc/REST/GetDocProfiles'; var GET_DOCTOR_PRE_POST_IMAGES = 'Services/Doctors.svc/REST/GetDoctorPrePostImages'; var GET_DOCTOR_RATING_NOTES = 'Services/Doctors.svc/REST/dr_GetNotesDoctorRating'; var GET_DOCTOR_RATING_DETAILS = 'Services/Doctors.svc/REST/dr_GetDoctorRatingDetails'; @@ -1001,43 +1002,12 @@ const SAVE_SETTING = 'Services/Patients.svc/REST/UpdatePateintInfo'; const GET_PROFILE_IMAGE = 'Services/Patients.svc/REST/Patient_GetProfileImageData'; -const UPLOAD_PROFILE_IMAGE = 'Services/Patients.svc/REST/Patient_InsertProfileImageData'; +const uploadProfileImageData = 'Services/Patients.svc/REST/Patient_InsertProfileImageData'; -const DOWNLOAD_INVOICE_PDF = 'Services/Notifications.svc/REST/DownloadInvoiceReport'; +const downloadInvoicePdf = 'Services/Notifications.svc/REST/DownloadInvoiceReport'; -const DOWNLOAD_PHARMACY_INVOICE_PDF = 'Services/Notifications.svc/REST/DownloadInvoiceReport'; +const downloadPharmacyInvoicePdf = 'Services/Notifications.svc/REST/DownloadInvoiceReport'; class ApiKeyConstants { static final String googleMapsApiKey = 'AIzaSyB6TERnxIr0yJ3qG4ULBZbu0sAD4tGqtng'; } - -//flutter: URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_InsertUserActivity -// flutter: {"IdentificationNo":"2530976584","MobileNumber":"504278212","QuantityIntake":200,"VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417} -// flutter: response.body: -// flutter: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":true,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":[{"QuantityConsumed":200.00,"PercentageConsumed":9.41,"PercentageLeft":90.59,"QuantityLimit":2125.00}],"UserProgressForWeekData":null,"UserProgressHistoryData":[{"Quantity":200.000,"CreatedDate":"\/Date(1766911222217+0300)\/"}],"VerificationCode":null,"isSMSSent":false} - -// URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2o_UndoUserActivity -// flutter: {"Progress":1,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417} -// flutter: response.body: -// flutter: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":[{"QuantityConsumed":0.00,"PercentageConsumed":0.00,"PercentageLeft":100.00,"QuantityLimit":2125.00}],"UserProgressForWeekData":null,"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false} - -// Progress":2 means weekly data - -// flutter: URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_GetUserProgress -// flutter: {"Progress":2,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417} -// flutter: response.body: -// [log] {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":null,"UserProgressForWeekData":[{"DayNumber":1,"DayDate":null,"DayName":"Sunday","PercentageConsumed":0},{"DayNumber":7,"DayDate":null,"DayName":"Saturday","PercentageConsumed":0},{"DayNumber":6,"DayDate":null,"DayName":"Friday","PercentageConsumed":0},{"DayNumber":5,"DayDate":null,"DayName":"Thursday","PercentageConsumed":0},{"DayNumber":4,"DayDate":null,"DayName":"Wednesday","PercentageConsumed":0},{"DayNumber":3,"DayDate":null,"DayName":"Tuesday","PercentageConsumed":0},{"DayNumber":2,"DayDate":null,"DayName":"Monday","PercentageConsumed":0}],"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false} - -// Progress":1 means daily data - -//URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_GetUserProgress -// flutter: {"Progress":1,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417} -// flutter: response.body: -// flutter: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":null,"UserProgressForTodayData":[{"QuantityConsumed":0.00,"PercentageConsumed":0.00,"PercentageLeft":100.00,"QuantityLimit":2125.00}],"UserProgressForWeekData":null,"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false} - -// Progress":1 means monthly data - -// flutter: URL : https://hmgwebservices.com/Services/H2ORemainder.svc/REST/H2O_GetUserProgress -// flutter: {"Progress":3,"MobileNumber":"504278212","IdentificationNo":"2530976584","VersionID":20.0,"Channel":3,"LanguageID":2,"IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","isDentalAllowedBackend":false,"DeviceTypeID":1,"PatientID":4515697,"TokenID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNS0xMi0yOFQwODozNjo0My45MTY0MzRaIiwiUGF0aWVudElEIjoiNDUxNTY5NyIsIlBhdGllbnRNZXJnZWRJRHMiOiI0NTE1Njk3IiwiUGF0aWVudE91dFNBIjpmYWxzZX0sImV4cCI6MTc2Njk2NjQwMH0.l1rCcs2OAU5u2J-iZMiO7NX6shGzLJV0hlYtVh2JeqY","PatientTypeID":1,"PatientType":1,"Latitude":37.785834,"Longitude":-122.406417} -// flutter: response.body: -// [log] {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":null,"ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":false,"MealOrderStatus":0,"MealType":0,"MessageStatus":1,"NumberOfResultRecords":0,"PatientBlodType":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"IsHMGPatient":false,"LogInTokenID":null,"PhysicalActivityData":null,"RowExists":0,"UserDetailData":null,"UserDetailData_New":null,"UserProgressForMonthData":[{"MonthNumber":1,"MonthName":"January","PercentageConsumed":0},{"MonthNumber":2,"MonthName":"February","PercentageConsumed":0},{"MonthNumber":3,"MonthName":"March","PercentageConsumed":0},{"MonthNumber":4,"MonthName":"April","PercentageConsumed":0},{"MonthNumber":5,"MonthName":"May","PercentageConsumed":0},{"MonthNumber":6,"MonthName":"June","PercentageConsumed":0},{"MonthNumber":7,"MonthName":"July","PercentageConsumed":0},{"MonthNumber":8,"MonthName":"August","PercentageConsumed":0},{"MonthNumber":9,"MonthName":"September","PercentageConsumed":0},{"MonthNumber":10,"MonthName":"October","PercentageConsumed":0},{"MonthNumber":11,"MonthName":"November","PercentageConsumed":0},{"MonthNumber":12,"MonthName":"December","PercentageConsumed":0}],"UserProgressForTodayData":null,"UserProgressForWeekData":null,"UserProgressHistoryData":[],"VerificationCode":null,"isSMSSent":false} diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index 9a4826b0..81742148 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; @@ -9,8 +10,8 @@ import 'package:hmg_patient_app_new/core/common_models/privilege/VidaPlusProject import 'package:hmg_patient_app_new/features/authentication/models/resp_models/authenticated_user_resp_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/check_user_staus_nhic_response_model.dart'; import 'package:hmg_patient_app_new/features/authentication/models/resp_models/select_device_by_imei.dart'; -import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; import '../features/authentication/models/request_models/registration_payload_model.dart'; @@ -20,7 +21,7 @@ class AppState { static const String _profileImageKey = 'profile_image_data'; - AppState({required this.navigationService, required this.cacheService}){ + AppState({required this.navigationService, required this.cacheService}) { checkIfGMSAvailableOrNot(); _loadProfileImageFromCache(); } @@ -52,7 +53,8 @@ class AppState { bool isChildLoggedIn = false; bool isGMSAvailable = true; bool isAndroid = true; - bool isRatedVisible =false; + bool isRatedVisible = false; + void setAuthenticatedUser(AuthenticatedUser? authenticatedUser, {bool isFamily = false}) { if (isFamily) { _authenticatedChildUser = authenticatedUser; @@ -64,10 +66,10 @@ class AppState { FutureOr checkIfGMSAvailableOrNot() async { isGMSAvailable = (await GmsCheck().checkGmsAvailability()) ?? true; - if(Platform.isIOS) { - isGMSAvailable = true; - isAndroid = false; - return; + if (Platform.isIOS) { + isGMSAvailable = true; + isAndroid = false; + return; } } @@ -100,16 +102,16 @@ class AppState { set setProfileImageData(String? value) { // Clear old data first if (_profileImageData != null && _profileImageData != value) { - print('🔄 Replacing profile image: old data length=${_profileImageData!.length}, new data length=${value?.length ?? 0}'); + log('🔄 Replacing profile image: old data length=${_profileImageData!.length}, new data length=${value?.length ?? 0}'); } // Set new value directly WITHOUT caching _profileImageData = value; if (value != null && value.isNotEmpty) { - print('✅ Profile image set (length: ${value.length}) - NO CACHE'); + log('✅ Profile image set (length: ${value.length}) - NO CACHE'); } else { - print('🗑️ Profile image cleared - NO CACHE'); + log('🗑️ Profile image cleared - NO CACHE'); } } @@ -117,14 +119,14 @@ class AppState { void _loadProfileImageFromCache() { // DO NOT LOAD FROM CACHE - always fetch fresh from API _profileImageData = null; - print('📂 Profile image cache disabled - will fetch from API'); + log('📂 Profile image cache disabled - will fetch from API'); } /// Clear profile image data (e.g., on logout or user switch) void clearProfileImageCache() { - print('🧹 Clearing profile image data (was: ${_profileImageData != null ? "${_profileImageData!.length} chars" : "null"})'); + log('🧹 Clearing profile image data (was: ${_profileImageData != null ? "${_profileImageData!.length} chars" : "null"})'); _profileImageData = null; - print('✅ Profile image data cleared - NO CACHE'); + log('✅ Profile image data cleared - NO CACHE'); } SelectDeviceByImeiRespModelElement? _selectDeviceByImeiRespModelElement; @@ -159,7 +161,6 @@ class AppState { set setIsAuthenticated(v) => isAuthenticated = v; - String deviceTypeID = ""; set setDeviceTypeID(v) => deviceTypeID = v; @@ -215,7 +216,7 @@ class AppState { userLat = 0.0; } - setRatedVisible(bool value) { + setRatedVisible(bool value) { isRatedVisible = value; } @@ -270,7 +271,6 @@ class AppState { } } - dynamic getUserSwitchPayload({required String userType}) { return _userSwitchPayloads[userType]; } @@ -296,11 +296,11 @@ class AppState { void clearChildUsersKeepSuperUser() { // Keep the super user payload final superUserPayload = _userSwitchPayloads['superUser']; - + // Clear all payloads _userSwitchPayloads.clear(); _childSwitchCounter = 0; - + // Restore super user if it existed if (superUserPayload != null) { _userSwitchPayloads['superUser'] = superUserPayload; diff --git a/lib/features/blood_donation/blood_donation_repo.dart b/lib/features/blood_donation/blood_donation_repo.dart index c709b038..2cbcd916 100644 --- a/lib/features/blood_donation/blood_donation_repo.dart +++ b/lib/features/blood_donation/blood_donation_repo.dart @@ -116,7 +116,7 @@ class BloodDonationRepoImp implements BloodDonationRepo { GenericApiModel>? apiResponse; Failure? failure; await apiClient.post( - GET_PROJECT_LIST, + ApiConsts.getProjectsList, body: request, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; diff --git a/lib/features/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart index 9afa113e..8bbb596c 100644 --- a/lib/features/book_appointments/book_appointments_repo.dart +++ b/lib/features/book_appointments/book_appointments_repo.dart @@ -271,7 +271,7 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { GenericApiModel? apiResponse; Failure? failure; await apiClient.post( - GET_DOCTOR_PROFILE, + ApiConsts.getDoctorProfiles, body: mapDevice, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; @@ -527,7 +527,7 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { GenericApiModel>? apiResponse; Failure? failure; await apiClient.post( - GET_PROJECT_LIST, + ApiConsts.getProjectsList, body: request, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; diff --git a/lib/features/contact_us/contact_us_view_model.dart b/lib/features/contact_us/contact_us_view_model.dart index 648fab6b..1b460a03 100644 --- a/lib/features/contact_us/contact_us_view_model.dart +++ b/lib/features/contact_us/contact_us_view_model.dart @@ -299,7 +299,7 @@ class ContactUsViewModel extends ChangeNotifier { requestInsertCOCItem.projectID = 0; requestInsertCOCItem.tokenID = "C0c@@dm!n?T&A&A@Barcha202029582948"; requestInsertCOCItem.identificationNo = int.parse(appState.getAuthenticatedUser()!.patientIdentificationNo!); - if (BASE_URL.contains('uat')) { + if (ApiConsts.baseUrl.contains('uat')) { requestInsertCOCItem.forDemo = true; } else { requestInsertCOCItem.forDemo = false; diff --git a/lib/features/emergency_services/emergency_services_repo.dart b/lib/features/emergency_services/emergency_services_repo.dart index 582c4a58..5fca01a8 100644 --- a/lib/features/emergency_services/emergency_services_repo.dart +++ b/lib/features/emergency_services/emergency_services_repo.dart @@ -189,7 +189,7 @@ class EmergencyServicesRepoImp implements EmergencyServicesRepo { GenericApiModel>? apiResponse; Failure? failure; await apiClient.post( - GET_PROJECT_LIST, + ApiConsts.getProjectsList, body: request, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; diff --git a/lib/features/habib_wallet/habib_wallet_repo.dart b/lib/features/habib_wallet/habib_wallet_repo.dart index 57d5af20..ff255092 100644 --- a/lib/features/habib_wallet/habib_wallet_repo.dart +++ b/lib/features/habib_wallet/habib_wallet_repo.dart @@ -83,7 +83,7 @@ class HabibWalletRepoImp implements HabibWalletRepo { GenericApiModel>? apiResponse; Failure? failure; await apiClient.post( - GET_PROJECT_LIST, + ApiConsts.getProjectsList, body: request, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart index 00ba55cc..ce18bf09 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -39,6 +39,31 @@ class HabibWalletViewModel extends ChangeNotifier { List refundableAdvancesList = []; bool isSubmittingWithdrawal = false; + // ── Wallet Withdrawal Step Management ───────────────────────────────────── + int currentWithdrawStep = 0; + RefundableAdvancesItem? selectedAdvance; + + void nextWithdrawStep() { + if (currentWithdrawStep < 1) { + currentWithdrawStep++; + notifyListeners(); + } + } + + void previousWithdrawStep() { + if (currentWithdrawStep > 0) { + currentWithdrawStep--; + notifyListeners(); + } + } + + void setSelectedAdvance(RefundableAdvancesItem? advance) { + selectedAdvance = advance; + notifyListeners(); + } + + double get selectedAdvanceBalance => selectedAdvance?.balanceAmount ?? 0.0; + bool isSearchedFileNumberDataShown = false; int currentIndex = 0; @@ -261,15 +286,17 @@ class HabibWalletViewModel extends ChangeNotifier { isSubmittingWithdrawal = true; notifyListeners(); - final advances = refundableAdvancesList - .map((a) => SubmitRefundAdvanceItem( - advanceNo: (a.advanceNo ?? 0).toString(), - paidAmount: a.paidAmount ?? 0.0, - balanceAmount: a.balanceAmount ?? 0.0, - advanceDate: a.advanceDate ?? "", - // advanceDate: _formatAdvanceDate(a.advanceDate), - )) - .toList(); + // Only send the selected advance (not all advances) + final advances = selectedAdvance != null + ? [ + SubmitRefundAdvanceItem( + advanceNo: (selectedAdvance!.advanceNo ?? 0).toString(), + paidAmount: selectedAdvance!.paidAmount ?? 0.0, + balanceAmount: selectedAdvance!.balanceAmount ?? 0.0, + advanceDate: selectedAdvance!.advanceDate ?? "", + ) + ] + : []; final request = SubmitRefundRequestModel( projectId: projectId, @@ -293,7 +320,7 @@ class HabibWalletViewModel extends ChangeNotifier { // only bank transfer refundIban: iban, refundRemarks: remarks, - refundTotalAmount: totalAdvancesBalance, + refundTotalAmount: selectedAdvanceBalance, refundProceduresList: const [], refundAdvancesList: advances, ); @@ -362,6 +389,8 @@ class HabibWalletViewModel extends ChangeNotifier { isSubmittingWithdrawal = false; _ibanError = null; _withdrawDescriptionError = null; + currentWithdrawStep = 0; + selectedAdvance = null; if (!silent) notifyListeners(); } diff --git a/lib/features/my_appointments/my_appointments_repo.dart b/lib/features/my_appointments/my_appointments_repo.dart index 553491e1..f05edc6f 100644 --- a/lib/features/my_appointments/my_appointments_repo.dart +++ b/lib/features/my_appointments/my_appointments_repo.dart @@ -522,7 +522,7 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { GenericApiModel>? apiResponse; Failure? failure; await apiClient.post( - GET_MY_DOCTOR, + ApiConsts.getMyDoctor, body: mapDevice, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; diff --git a/lib/features/my_invoices/my_invoices_repo.dart b/lib/features/my_invoices/my_invoices_repo.dart index 23f5d06a..c0f60049 100644 --- a/lib/features/my_invoices/my_invoices_repo.dart +++ b/lib/features/my_invoices/my_invoices_repo.dart @@ -199,7 +199,7 @@ class MyInvoicesRepoImp implements MyInvoicesRepo { GenericApiModel? apiResponse; Failure? failure; await apiClient.post( - DOWNLOAD_INVOICE_PDF, + downloadInvoicePdf, body: mapDevice, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; @@ -238,7 +238,7 @@ class MyInvoicesRepoImp implements MyInvoicesRepo { GenericApiModel? apiResponse; Failure? failure; await apiClient.post( - DOWNLOAD_PHARMACY_INVOICE_PDF, + downloadPharmacyInvoicePdf, body: mapDevice, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; diff --git a/lib/features/profile_settings/profile_settings_repo.dart b/lib/features/profile_settings/profile_settings_repo.dart index 113a52c0..5c16e646 100644 --- a/lib/features/profile_settings/profile_settings_repo.dart +++ b/lib/features/profile_settings/profile_settings_repo.dart @@ -207,7 +207,7 @@ class ProfileSettingsRepoImp implements ProfileSettingsRepo { Failure? failure; await apiClient.post( - UPLOAD_PROFILE_IMAGE, + uploadProfileImageData, body: body, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; diff --git a/lib/features/weather/weather_repo.dart b/lib/features/weather/weather_repo.dart index 26bf0a32..4777713f 100644 --- a/lib/features/weather/weather_repo.dart +++ b/lib/features/weather/weather_repo.dart @@ -25,7 +25,7 @@ class WeatherRepoImp implements WeatherRepo { GenericApiModel>? apiResponse; Failure? failure; await apiClient.post( - WEATHER_INDICATOR, + ApiConsts.weatherIndicator, body: request, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 07afd569..1cf39e81 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -2,7 +2,7 @@ // ignore_for_file: constant_identifier_names -abstract class LocaleKeys { +abstract class LocaleKeys { static const english = 'english'; static const arabic = 'arabic'; static const login = 'login'; @@ -1851,7 +1851,7 @@ abstract class LocaleKeys { static const invalidIbanFormat = 'invalidIbanFormat'; static const refundHistory = 'refundHistory'; static const noRefundHistoryFound = 'noRefundHistoryFound'; - static const selectProcedures = 'selectProcedures'; + static const selectProcedure = 'selectProcedure'; static const noProceduresAvailableForRefund = 'noProceduresAvailableForRefund'; static const selectAll = 'selectAll'; static const selectRefundMethod = 'selectRefundMethod'; @@ -1866,6 +1866,7 @@ abstract class LocaleKeys { static const refundReason5 = 'refundReason5'; static const refundReason6 = 'refundReason6'; static const refundReason7 = 'refundReason7'; + static const selectProcedures = 'selectProcedures'; static const redeem = 'redeem'; static const points = 'points'; static const transactions = 'transactions'; @@ -1876,8 +1877,12 @@ abstract class LocaleKeys { static const refundDetails = 'refundDetails'; static const requestNo = 'requestNo'; static const referenceNo = 'referenceNo'; - static const refund = 'refund'; static const advanceNumber = 'advanceNumber'; static const amountOnly = 'amountOnly'; static const invoiceHistory = 'invoiceHistory'; + static const selectAdvance = 'selectAdvance'; + static const advanceNo = 'advanceNo'; + static const noAdvancesAvailable = 'noAdvancesAvailable'; + static const refund = 'refund'; + } diff --git a/lib/presentation/habib_wallet/withdraw_request_create_page.dart b/lib/presentation/habib_wallet/withdraw_request_create_page.dart index cc0c9731..a5909e3a 100644 --- a/lib/presentation/habib_wallet/withdraw_request_create_page.dart +++ b/lib/presentation/habib_wallet/withdraw_request_create_page.dart @@ -1,15 +1,17 @@ -import 'dart:io'; - import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_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'; @@ -17,10 +19,10 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; -import 'package:hmg_patient_app_new/widgets/image_picker.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:hmg_patient_app_new/widgets/stepper/stepper_widget.dart'; import 'package:provider/provider.dart'; class WithdrawRequestCreatePage extends StatefulWidget { @@ -45,6 +47,8 @@ class _WithdrawRequestCreatePageState extends State { _appState = getIt.get(); _habibWalletViewModel = context.read(); WidgetsBinding.instance.addPostFrameCallback((_) { + _habibWalletViewModel.currentWithdrawStep = 0; + _habibWalletViewModel.selectedAdvance = null; _habibWalletViewModel.getProjectsList(); }); } @@ -59,6 +63,170 @@ class _WithdrawRequestCreatePageState extends State { super.dispose(); } + // ── Stepper ─────────────────────────────────────────────────────────────── + + Widget _buildStepper(HabibWalletViewModel vm) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: LayoutBuilder( + builder: (context, constraints) { + final itemWidth = (constraints.maxWidth - 2 * 8.h) / 2; + return Row( + children: List.generate(2, (index) { + final isActive = index <= vm.currentWithdrawStep; + final isThumb = index == vm.currentWithdrawStep; + return StepperWidget( + itemWidth, + isActive ? AppColors.primaryRedColor : AppColors.greyColor, + isThumb, + 6.h, + ); + }), + ); + }, + ), + ); + } + + // ── Step 1 — Select Advance ────────────────────────────────────────────── + + Widget _buildStep1Content(HabibWalletViewModel vm) { + return Consumer( + builder: (context, walletVM, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.selectAdvance.tr(context: context).toText20(isBold: true), + SizedBox(height: 24.h), + if (vm.isAdvancesLoading) + ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => SizedBox(height: 16.h), + itemCount: 4, + itemBuilder: (_, index) => Container( + height: 80.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.h, + ), + ).toShimmer2(isShow: true, radius: 16.r), + ) + else if (vm.refundableAdvancesList.isEmpty) + Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 24.h), + child: LocaleKeys.noAdvancesAvailable.tr().toText14(color: AppColors.greyTextColor), + ), + ) + else + AnimationLimiter( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => Divider( + color: AppColors.bottomNAVBorder, + height: 1, + thickness: 1, + ).paddingSymmetrical(16.w, 0.w), + itemCount: vm.refundableAdvancesList.length, + itemBuilder: (context, index) { + final advance = vm.refundableAdvancesList[index]; + return _buildAdvanceCard(advance, index); + }, + ), + ), + ), + ], + ); + }, + ); + } + + Widget _buildAdvanceCard(RefundableAdvancesItem advance, int index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 400), + child: SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _habibWalletViewModel.setSelectedAdvance(advance), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 24.w, + height: 24.w, + child: Radio( + value: advance.advanceNo ?? 0, + groupValue: _habibWalletViewModel.selectedAdvance?.advanceNo ?? -1, + onChanged: (_) => _habibWalletViewModel.setSelectedAdvance(advance), + activeColor: AppColors.primaryRedColor, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "${LocaleKeys.advanceNo.tr()}: ${advance.advanceNo ?? 0}".toText16(isBold: true), + SizedBox(height: 4.h), + _buildAdvanceDate(advance.advanceDate), + ], + ), + ), + SizedBox(width: 8.w), + Utils.getPaymentAmountWithSymbol( + (advance.balanceAmount ?? 0.0).toStringAsFixed(2).toText20( + isBold: true, + isEnglishOnly: true, + color: AppColors.textColor, + ), + AppColors.textColor, + 10.f, + isSaudiCurrency: true, + ), + ], + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildAdvanceDate(String? dateString) { + if (dateString == null || dateString.isEmpty) { + return const SizedBox.shrink(); + } + + try { + final date = DateUtil.convertStringToDate(dateString); + final formattedDate = DateUtil.formatDateToDate(date, false); + return formattedDate.toText12(color: AppColors.greyTextColor); + } catch (e) { + return const SizedBox.shrink(); + } + } + + // ── Step 2 — Bank Details ──────────────────────────────────────────────── + bool get _isFormValid { final vm = _habibWalletViewModel; return vm.selectedHospital != null && @@ -72,9 +240,13 @@ class _WithdrawRequestCreatePageState extends State { Widget _buildBottomBar() { return Consumer( builder: (context, vm, _) { - final isValid = vm.selectedHospital != null && - !vm.isAdvancesLoading && - _bankDetailsController.text.trim().isNotEmpty && + final isStep2 = vm.currentWithdrawStep == 1; + + // Step 1 validation: advance selected + final isStep1Valid = vm.selectedAdvance != null; + + // Step 2 validation: bank details filled + final isStep2Valid = _bankDetailsController.text.trim().isNotEmpty && _descriptionController.text.trim().isNotEmpty; return Container( @@ -86,16 +258,47 @@ class _WithdrawRequestCreatePageState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ + if (isStep2 && vm.selectedAdvance != null) ...[ + Padding( + padding: EdgeInsets.fromLTRB(24.w, 16.h, 24.w, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.totalAmount.tr().toText18(color: AppColors.textColor, isBold: true), + Utils.getPaymentAmountWithSymbol( + vm.selectedAdvanceBalance.toStringAsFixed(2).toText24( + isBold: true, + isEnglishOnly: true, + color: AppColors.textColor, + ), + AppColors.textColorLight, + 14, + isSaudiCurrency: true, + ), + ], + ), + ), + ], CustomButton( - text: LocaleKeys.submitRequest.tr(context: context), - isDisabled: !isValid, + text: isStep2 + ? LocaleKeys.submitRequest.tr(context: context) + : LocaleKeys.next.tr(context: context), + isDisabled: isStep2 ? !isStep2Valid : !isStep1Valid, onPressed: () { - final isFormValid = vm.validateWithdrawForm( - iban: _bankDetailsController.text, - description: _descriptionController.text, - ); - if (isFormValid) { - _handleWalletWithdrawalSubmit(vm); + if (!isStep2) { + // Move to step 2 + if (vm.selectedAdvance != null) { + vm.nextWithdrawStep(); + } + } else { + // Submit + final isFormValid = vm.validateWithdrawForm( + iban: _bankDetailsController.text, + description: _descriptionController.text, + ); + if (isFormValid) { + _handleWalletWithdrawalSubmit(vm); + } } }, ).paddingSymmetrical(24.h, 24.h), @@ -108,6 +311,9 @@ class _WithdrawRequestCreatePageState extends State { Widget _buildProjectSelectionAndBalance(HabibWalletViewModel habibWalletViewModel) { final isArabic = _appState.isArabic(); + final showAmount = habibWalletViewModel.currentWithdrawStep == 1 && + habibWalletViewModel.selectedAdvance != null; + return Container( padding: EdgeInsets.all(16.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), @@ -137,45 +343,40 @@ class _WithdrawRequestCreatePageState extends State { ]), ], ), - Utils.buildSvgWithAssets( - icon: AppAssets.arrow_down, - width: 25.h, - height: 25.h, - ), + if (habibWalletViewModel.currentWithdrawStep == 0) + 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) ...[ + ).onPress( + habibWalletViewModel.currentWithdrawStep == 0 + ? () { + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.selectHospital.tr(context: context), + isDismissible: false, + child: SelectHospitalBottomSheet( + onHospitalSelected: (hospital) { + habibWalletViewModel.setSelectedHospital(hospital); + habibWalletViewModel.fetchAdvancesForHospital(); + }, + ), + callBackFunc: () {}, + ); + } + : () {}, // Empty callback for step 2 + ), + if (showAmount) ...[ 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, - ), + Utils.getPaymentAmountWithSymbol( + habibWalletViewModel.selectedAdvanceBalance.toStringAsFixed(2).toText44(isBold: true), + AppColors.blackColor, + 18.w, + isSaudiCurrency: true, + isExpanded: false, + ), ], ], ), @@ -254,45 +455,6 @@ class _WithdrawRequestCreatePageState extends State { ); } - Widget _buildImagePickerDropDown() { - return Container( - padding: EdgeInsets.all(16.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.selectImageIcon, - width: 40.h, - height: 40.h, - applyThemeColor: false, - ), - SizedBox(width: 8.h), - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - LocaleKeys.uploadImage.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), - LocaleKeys.selectImage.tr(context: context).toText14(color: AppColors.textColor, isBold: true), - ]), - ], - ), - Utils.buildSvgWithAssets( - icon: AppAssets.arrow_down, - width: 25.h, - height: 25.h, - ), - ], - ).onPress(() { - ImageOptions.showImageOptionsNew( - context, - false, - title: LocaleKeys.selectOptions.tr(context: context), - (String image, File file) {}, - ); - }), - ); - } - // ── Submit ──────────────────────────────────────────────────────────────── void _handleWalletWithdrawalSubmit(HabibWalletViewModel vm) { @@ -407,6 +569,13 @@ class _WithdrawRequestCreatePageState extends State { Expanded( child: CollapsingListView( title: LocaleKeys.requestRefund.tr(context: context), + leadingCallback: () { + if (habibWalletViewModel.currentWithdrawStep > 0) { + habibWalletViewModel.previousWithdrawStep(); + } else { + context.pop(); + } + }, requests: () => Navigator.of(context).push( CustomPageRoute( page: RefundHistoryScreen( @@ -415,17 +584,53 @@ class _WithdrawRequestCreatePageState extends State { ), ), child: SingleChildScrollView( - padding: EdgeInsets.only(bottom: 32.h, left: 24.w, right: 24.w), + padding: EdgeInsets.only(bottom: 32.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildProjectSelectionAndBalance(habibWalletViewModel), + // Hospital selection card - always visible + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: _buildProjectSelectionAndBalance(habibWalletViewModel), + ), + + // Stepper - show only after hospital selected if (habibWalletViewModel.selectedHospital != null) ...[ SizedBox(height: 16.h), - _buildBankInputDetails(), - // SizedBox(height: 16.h), - // _buildImagePickerDropDown(), + _buildStepper(habibWalletViewModel), + SizedBox(height: 16.h), ], + + // Step content - show only after hospital selected + if (habibWalletViewModel.selectedHospital != null) + AnimatedSwitcher( + duration: const Duration(milliseconds: 350), + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: const Offset(0.05, 0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + ), + child: habibWalletViewModel.currentWithdrawStep == 0 + ? KeyedSubtree( + key: const ValueKey('step1'), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: _buildStep1Content(habibWalletViewModel), + ), + ) + : KeyedSubtree( + key: const ValueKey('step2'), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: _buildBankInputDetails(), + ), + ), + ), ], ), ), @@ -440,6 +645,3 @@ class _WithdrawRequestCreatePageState extends State { } } -// I/flutter (13750): uri: https://uat.hmgwebservices.com/Services/COCWS.svc/REST/SubmitRefund -// [log] body: {"ProjectID":15,"Refund_RefundType":"2","Refund_Project":"مستشفى الريان","Refund_ProjectId":15,"Refund_PatientId":"4770714","PatientMobileNumber":"0504278212","Refund_PatientName":"FAIZ UR REHMAN REHMAN","Refund_NationalId":"2530976584","Refund_DoctorId":0,"Refund_DoctorName":"","Refund_RefundDestination":"2","Refund_IBAN":"SA0380000000608010167519","Refund_Remarks":"teskchjvs","Refund_TotalRefundAmount":1714.75,"Refund_AdvancesList":[{"AdvanceNo":"2026000444","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1775682000000+0300)/"},{"AdvanceNo":"2026000392","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1774645200000+0300)/"},{"AdvanceNo":"2026000066","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1770152400000+0300)/"},{"AdvanceNo":"2026000047","PaidAmount":30.0,"BalanceAmount":30.0,"AdvanceDate":"/Date(1769547600000+0300)/"},{"AdvanceNo":"2025000751","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1765746000000+0300)/"},{"AdvanceNo":"2025000414","PaidAmount":115.0,"BalanceAmount":115.0,"AdvanceDate":"/Date(1747083600000+0300)/"},{"AdvanceNo":"2025000210","PaidAmount":120.75,"BalanceAmount":120.75,"AdvanceDate":"/Date(1741726800000+0300)/"}],"PatientType":1,"PatientTypeID":1,"TokenID":"@dm!n","PatientID":4770714,"PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNi0wNS0xOFQwNzo1MzoyMS4wMjQ5MDM2WiIsIlBhdGllbnRJRCI6IjQ3NzIxNjMiLCJQYXRpZW50TWVyZ2VkSURzIjoiNDc3MjE2MyIsIlBhdGllbnRPdXRTQSI6ZmFsc2UsIlNoYXJlZElkcyI6IiJ9LCJleHAiOjE3NzkxMzgwMDB9.VHd_kCya33GfK-DywTHwSU1PMewbQB7UhQD1zXS7X5w","VersionID":"21.2","Channel":"3","IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","LanguageID":"1","Latitude":"0.0","Longitude":"0.0","DeviceTypeID":"2","IsNewFlutterApp":true} -// [log] response: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":"Internal server error occurred.","ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":true,"MealOrderStatus":0,"MealType":0,"MessageStatus":2,"NumberOfResultRecords":0,"PatientBlodType":null,"RequestId":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"ENSAT_GetPatientRequestListResponse":null,"ENSAT_Patient_External_Response":null,"ETQAN_CreateIncident_Response":null,"ETQAN_GetEmployeeOVRRequests_Response":null,"ETQAN_GetIncidentRequest_Response":null,"ETQAN_GetProjects_Response":null,"ITGRequest":null,"Itg_FormAttachmentsList":null,"ListCOCItems":null,"Message":"Refunds API returned status 500. Body: {\"message\":\"Internal server error occurred.\"}","Mohemm_ENSAT_GetItgToken_Response":null,"Mohemm_ITG_DepartmentSectionsList":null,"Mohemm_ITG_Pending_Task_ResponseItem":null,"Mohemm_ITG_ProjectDepartmentsList":null,"Mohemm_ITG_ResponseItem":null,"Mohemm_ITG_SectionTopicsList":null,"Mohemm_ITG_TicketDetailsList":null,"Mohemm_ITG_TicketTransactionsList":null,"Mohemm_ITG_TicketsByEmployeeList":null,"Mohemm_Itg_ProjectsList":null,"Mohemm_Itg_TicketTypesList":null,"ReferenceNumber":null,"Refund_InvoicesList":null,"Refund_PatientIdDisplay":null,"Refund_RefNo":null,"Refund_RefundTypeDisplay":null,"Refund_StatusItem":null,"Refund_TotalInvoices":0,"Refund_VerifyResult":null,"RequestType":null,"TotalCount":0,"statuseCode":500} diff --git a/lib/presentation/home/lakum_wallet_details.dart b/lib/presentation/home/lakum_wallet_details.dart index 79b71ec0..83f7009d 100644 --- a/lib/presentation/home/lakum_wallet_details.dart +++ b/lib/presentation/home/lakum_wallet_details.dart @@ -250,7 +250,7 @@ class LakumWalletDetails extends StatelessWidget { padding: EdgeInsets.fromLTRB(4, 0, 12, 0), fontSize: 14.f, onPressed: () { - Uri uri = Uri.parse(PHARMACY_REDIRECT_URL); + Uri uri = Uri.parse(ApiConsts.pharmacyRedirectUrl); launchUrl(uri, mode: LaunchMode.externalApplication); }, ); diff --git a/lib/presentation/home/widgets/lakum_wallet_card.dart b/lib/presentation/home/widgets/lakum_wallet_card.dart index 3645a2d3..f2ee7a26 100644 --- a/lib/presentation/home/widgets/lakum_wallet_card.dart +++ b/lib/presentation/home/widgets/lakum_wallet_card.dart @@ -137,7 +137,7 @@ class LakumWalletCard extends StatelessWidget { padding: EdgeInsets.fromLTRB(4, 0, 12, 0), fontSize: 14.f, onPressed: () { - Uri uri = Uri.parse(PHARMACY_REDIRECT_URL); + Uri uri = Uri.parse(ApiConsts.pharmacyRedirectUrl); launchUrl(uri, mode: LaunchMode.externalApplication); // Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage())); }, diff --git a/lib/presentation/home/widgets/large_service_card.dart b/lib/presentation/home/widgets/large_service_card.dart index 72b11ef3..7c4948c4 100644 --- a/lib/presentation/home/widgets/large_service_card.dart +++ b/lib/presentation/home/widgets/large_service_card.dart @@ -165,7 +165,7 @@ class LargeServiceCard extends StatelessWidget { } case "pharmacy": { - Uri uri = Uri.parse(PHARMACY_REDIRECT_URL); + Uri uri = Uri.parse(ApiConsts.pharmacyRedirectUrl); launchUrl(uri, mode: LaunchMode.externalApplication); } } @@ -312,7 +312,7 @@ class FadedLargeServiceCard extends StatelessWidget { } case "pharmacy": { - Uri uri = Uri.parse(PHARMACY_REDIRECT_URL); + Uri uri = Uri.parse(ApiConsts.pharmacyRedirectUrl); launchUrl(uri, mode: LaunchMode.externalApplication); } } -- 2.30.2 From 34a55396b05e017e8d688762bb834232b03e24c7 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Wed, 10 Jun 2026 16:26:18 +0300 Subject: [PATCH 2/9] Added invoice type checks --- lib/core/api/api_client.dart | 3 +- lib/core/api_consts.dart | 4 +- .../get_invoices_list_response_model.dart | 16 ++ .../refund_request_list_response_model.dart | 3 + .../refund_request_view_model.dart | 11 +- .../widgets/invoice_list_card.dart | 18 +- .../refund_request_create_page.dart | 236 ++++++++++-------- .../refund_request_detail_screen.dart | 44 +++- 8 files changed, 219 insertions(+), 116 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index b1fc0e5b..8b373737 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -209,7 +209,8 @@ class ApiClientImp implements ApiClient { } body['TokenID'] = "@dm!n"; - body['PatientID'] = 4774167; + // body['PatientID'] = 4770714; + body['PatientID'] = 4771727; // body['PatientID'] = 89754444; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 24872c32..c7081528 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -4,7 +4,7 @@ import 'package:hmg_patient_app_new/core/enums.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -292,7 +292,7 @@ class ApiConsts { // ************ static values for Api **************** // static final double appVersionID = 21.2; - static final double appVersionID = 50.7; + static final double appVersionID = 21.4; static final int appChannelId = 3; static final String appIpAddress = "10.20.10.20"; static final String appGeneralId = "Cs2020@2016\$2958"; 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 a3c9858c..1aa580e7 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 @@ -19,6 +19,10 @@ class GetInvoicesListResponseModel { String? doctorImageUrl; int? doctorRate; bool? isAllowRefund; + int? invoiceType; + String? invoiceTypeDescription; + bool? isPackageInvoice; + bool? isOutPatient; int? patientNumber; String? projectName; @@ -41,6 +45,10 @@ class GetInvoicesListResponseModel { this.doctorImageUrl, this.doctorRate, this.isAllowRefund, + this.invoiceType, + this.invoiceTypeDescription, + this.isPackageInvoice, + this.isOutPatient, this.patientNumber, this.projectName, }); @@ -68,6 +76,10 @@ class GetInvoicesListResponseModel { doctorImageUrl: json["DoctorImageURL"], doctorRate: json["DoctorRate"], isAllowRefund: json["IsAllowRefund"], + invoiceType: json["InvoiceType"], + invoiceTypeDescription: json["InvoiceTypeDescription"], + isPackageInvoice: json["IsPackageInvoice"], + isOutPatient: json['InvoiceType'] != 75, patientNumber: json["PatientNumber"], projectName: json["ProjectName"], ); @@ -91,6 +103,10 @@ class GetInvoicesListResponseModel { "DoctorImageURL": doctorImageUrl, "DoctorRate": doctorRate, "IsAllowRefund": isAllowRefund, + "invoiceType": invoiceType, + "invoiceDescription": invoiceTypeDescription, + "isOutPatient": isOutPatient, + "isPackageInvoice": isPackageInvoice, "PatientNumber": patientNumber, "ProjectName": projectName, }; 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 b17dd5aa..015a7556 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 @@ -142,6 +142,7 @@ class InvoiceRefundProcedure { final String? procedureId; final String? procedureName; final String? refundReason; + final bool? isApproved; InvoiceRefundProcedure({ this.isLocked, @@ -150,6 +151,7 @@ class InvoiceRefundProcedure { this.procedureId, this.procedureName, this.refundReason, + this.isApproved, }); factory InvoiceRefundProcedure.fromJson(Map json) { @@ -160,6 +162,7 @@ class InvoiceRefundProcedure { procedureId: json['ProcedureID'] as String?, procedureName: json['ProcedureName'] as String?, refundReason: json['RefundReason'] as String?, + isApproved: json['IsApproved'] as bool?, ); } } diff --git a/lib/features/refund_request/refund_request_view_model.dart b/lib/features/refund_request/refund_request_view_model.dart index 828e3d0a..d539d97f 100644 --- a/lib/features/refund_request/refund_request_view_model.dart +++ b/lib/features/refund_request/refund_request_view_model.dart @@ -109,6 +109,7 @@ class RefundRequestViewModel extends ChangeNotifier { currentStep = 0; procedures.clear(); needITGApproval = false; + isWholeInvoiceRefund = false; selectedPaymentMethod = null; selectedRefundReason = null; isProceduresLoading = false; @@ -208,15 +209,20 @@ class RefundRequestViewModel extends ChangeNotifier { List procedures = []; bool isProceduresLoading = false; bool needITGApproval = false; + bool isWholeInvoiceRefund = false; // For Inpatient/Package invoices - void setProcedures(List list, {bool needsITGApproval = false}) { + void setProcedures(List list, {bool needsITGApproval = false, bool isWholeInvoiceRefund = false}) { procedures = list; needITGApproval = needsITGApproval; + this.isWholeInvoiceRefund = isWholeInvoiceRefund; isProceduresLoading = false; notifyListeners(); } void toggleProcedureSelection(RefundableProcedure procedure) { + // Prevent selection toggle for whole invoice refunds + if (isWholeInvoiceRefund) return; + procedure.isSelected = !procedure.isSelected; notifyListeners(); } @@ -229,6 +235,9 @@ class RefundRequestViewModel extends ChangeNotifier { bool get isAllSelected => procedures.isNotEmpty && procedures.every((p) => p.isSelected); void toggleSelectAll() { + // Prevent toggle for whole invoice refunds + if (isWholeInvoiceRefund) return; + final selectAll = !isAllSelected; for (final p in procedures) { p.isSelected = selectAll; diff --git a/lib/presentation/my_invoices/widgets/invoice_list_card.dart b/lib/presentation/my_invoices/widgets/invoice_list_card.dart index dc5414cd..3b0ea71d 100644 --- a/lib/presentation/my_invoices/widgets/invoice_list_card.dart +++ b/lib/presentation/my_invoices/widgets/invoice_list_card.dart @@ -24,9 +24,9 @@ import 'package:provider/provider.dart'; class InvoiceListCard extends StatelessWidget { final GetInvoicesListResponseModel getInvoicesListResponseModel; - Function? onTap; + final Function? onTap; - InvoiceListCard({super.key, required this.getInvoicesListResponseModel, required this.onTap}); + const InvoiceListCard({super.key, required this.getInvoicesListResponseModel, required this.onTap}); void _handleRefundInvoiceTap(BuildContext context) { final invoiceNo = getInvoicesListResponseModel.invoiceNo; @@ -70,7 +70,6 @@ class InvoiceListCard extends StatelessWidget { @override Widget build(BuildContext context) { - AppState appState = getIt(); return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), child: Padding( @@ -90,11 +89,14 @@ class InvoiceListCard extends StatelessWidget { labelText: LocaleKeys.walkin.tr(), textColor: AppColors.textColor, ), - AppCustomChipWidget( - labelText: appState.isArabic() ? "العيادات الخارجيه" : "Out Patient", - backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.1), - textColor: AppColors.warningColorYellow, - ), + if (getInvoicesListResponseModel.invoiceTypeDescription != null && + getInvoicesListResponseModel.invoiceTypeDescription!.isNotEmpty) ...[ + AppCustomChipWidget( + labelText: getInvoicesListResponseModel.invoiceTypeDescription ?? "", + backgroundColor: AppColors.warningColorYellow.withValues(alpha: 0.1), + textColor: AppColors.warningColorYellow, + ), + ], ], ), SizedBox(height: 16.h), diff --git a/lib/presentation/refund_request/refund_request_create_page.dart b/lib/presentation/refund_request/refund_request_create_page.dart index 1d0f723f..a795cf16 100644 --- a/lib/presentation/refund_request/refund_request_create_page.dart +++ b/lib/presentation/refund_request/refund_request_create_page.dart @@ -74,6 +74,20 @@ class _RefundRequestCreatePageState extends State { super.dispose(); } + /// Determines if the invoice allows individual procedure selection + /// Returns true for Outpatient and ER invoices only + /// Returns false for Inpatient and Package invoices (whole invoice refund only) + bool get _canSelectIndividualProcedures { + // Package invoices: no individual procedure selection + if (widget.invoice.isPackageInvoice == true) return false; + + // Inpatient invoices: no individual procedure selection + if (widget.invoice.isOutPatient == false) return false; + + // Outpatient and ER invoices: allow individual procedure selection + return true; + } + void _loadInvoiceDetails() { final invoiceNo = widget.invoice.invoiceNo; final projectId = widget.invoice.projectId; @@ -90,9 +104,17 @@ class _RefundRequestCreatePageState extends State { patientPaidAmount: item.patientPaidAmount ?? 0.0, apiLineItemNo: item.lineItemNo, apiProcedureId: item.procedureId, + // For Inpatient/Package invoices: auto-select all procedures (can't deselect) + isSelected: !_canSelectIndividualProcedures, + // For Inpatient/Package invoices: set empty string as refund reason (mandatory field) + refundReason: !_canSelectIndividualProcedures ? "Inpatient/Package invoice" : null, )) .toList(); - _refundRequestViewModel.setProcedures(procedures, needsITGApproval: details.needITGApproval ?? false); + _refundRequestViewModel.setProcedures( + procedures, + needsITGApproval: details.needITGApproval ?? false, + isWholeInvoiceRefund: !_canSelectIndividualProcedures, + ); }, ); } @@ -123,12 +145,11 @@ class _RefundRequestCreatePageState extends State { ); } - // ── Step 1 — exactly mirrors RechargeWalletPage hospital row ───────────── + // ── Step 1 — Select Procedures (or whole invoice for Inpatient/Package) ── Widget _buildStep1Content(RefundRequestViewModel vm) { return Consumer( builder: (context, refundVM, _) { - final isArabic = _appState.isArabic(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -164,39 +185,41 @@ class _RefundRequestCreatePageState extends State { 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)), + // Select All row - only show for Outpatient/ER invoices + if (_canSelectIndividualProcedures) + 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), - ], + 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), + if (_canSelectIndividualProcedures) + 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), @@ -331,11 +354,12 @@ class _RefundRequestCreatePageState extends State { ); } - // ── Step 2 — procedure checkboxes ───────────────────────────────────────── + // ── Procedure Card ──────────────────────────────────────────────────────── Widget _buildProcedureCard(RefundableProcedure procedure, int index, int length) { final isArabic = _appState.isArabic(); final name = isArabic && procedure.procedureNameN != null ? procedure.procedureNameN! : procedure.procedureName; + final canSelect = _canSelectIndividualProcedures; return AnimationConfiguration.staggeredList( position: index, @@ -349,81 +373,88 @@ class _RefundRequestCreatePageState extends State { child: Material( color: Colors.transparent, child: InkWell( - onTap: () => _refundRequestViewModel.toggleProcedureSelection(procedure), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: 24.w, - height: 24.w, - child: Transform.scale( - scale: 24.w / 22, - child: Checkbox( - value: procedure.isSelected, - onChanged: (_) => _refundRequestViewModel.toggleProcedureSelection(procedure), - activeColor: AppColors.primaryRedColor, - side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: VisualDensity.compact, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4.r), + // Disable tap for Inpatient/Package invoices + onTap: canSelect ? () => _refundRequestViewModel.toggleProcedureSelection(procedure) : null, + child: Opacity( + // Reduce opacity for disabled items + opacity: canSelect ? 1.0 : 0.7, + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 24.w, + height: 24.w, + child: Transform.scale( + scale: 24.w / 22, + child: Checkbox( + value: procedure.isSelected, + // Disable checkbox for Inpatient/Package invoices + onChanged: canSelect ? (_) => _refundRequestViewModel.toggleProcedureSelection(procedure) : null, + activeColor: AppColors.primaryRedColor, + side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4.r), + ), ), ), ), - ), - SizedBox(width: 8.w), - Expanded(child: name.toText16(isBold: true)), - SizedBox(width: 8.w), - Utils.getPaymentAmountWithSymbol( - procedure.patientPaidAmount.toStringAsFixed(2).toText20( - isBold: true, - isEnglishOnly: true, - color: AppColors.textColor, - ), - AppColors.textColor, - 10.f, - isSaudiCurrency: true, - ), - ], - ), - if (procedure.isSelected) ...[ - InkWell( - onTap: () => showRefundReasonBottomSheet( - context, - selectedReason: procedure.refundReason, - onDone: (reason) => _refundRequestViewModel.setRefundReasonForProcedure(procedure, reason), - ), - child: Padding( - padding: EdgeInsets.only(top: 16.h), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.refundReason.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), - (procedure.refundReason ?? LocaleKeys.selectRefundReason.tr(context: context)) - .toText14(color: AppColors.textColor, isBold: true), - ], + SizedBox(width: 8.w), + Expanded(child: name.toText16(isBold: true)), + SizedBox(width: 8.w), + Utils.getPaymentAmountWithSymbol( + procedure.patientPaidAmount.toStringAsFixed(2).toText20( + isBold: true, + isEnglishOnly: true, + color: AppColors.textColor, ), - ), - Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), - ], + AppColors.textColor, + 10.f, + isSaudiCurrency: true, ), - ), + ], ), + // Only show refund reason selector for Outpatient/ER invoices + if (canSelect && procedure.isSelected) ...[ + InkWell( + onTap: () => showRefundReasonBottomSheet( + context, + selectedReason: procedure.refundReason, + onDone: (reason) => _refundRequestViewModel.setRefundReasonForProcedure(procedure, reason), + ), + child: Padding( + padding: EdgeInsets.only(top: 16.h), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.refundReason.tr(context: context).toText12(color: AppColors.greyTextColor, isBold: true), + (procedure.refundReason ?? LocaleKeys.selectRefundReason.tr(context: context)) + .toText14(color: AppColors.textColor, isBold: true), + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), + ], + ), + ), + ), + ], + if (index != length - 1) ...[ + Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingOnly(top: 16.h), + ] ], - if (index != length - 1) ...[ - Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingOnly(top: 16.h), - ] - ], + ), ), ), ), @@ -492,7 +523,10 @@ class _RefundRequestCreatePageState extends State { ? (refundRequestViewModel.selectedPaymentMethod == null || (refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices && (_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty))) - : (refundRequestViewModel.selectedProcedures.isEmpty || !refundRequestViewModel.allSelectedProceduresHaveReasons), + : (refundRequestViewModel.selectedProcedures.isEmpty || + // For Outpatient/ER: require refund reasons + // For Inpatient/Package: skip refund reason validation + (_canSelectIndividualProcedures && !refundRequestViewModel.allSelectedProceduresHaveReasons)), ).paddingSymmetrical(24.h, 24.h), ], ), diff --git a/lib/presentation/refund_request/refund_request_detail_screen.dart b/lib/presentation/refund_request/refund_request_detail_screen.dart index a28aa4dd..85f399b4 100644 --- a/lib/presentation/refund_request/refund_request_detail_screen.dart +++ b/lib/presentation/refund_request/refund_request_detail_screen.dart @@ -186,8 +186,28 @@ class RefundRequestDetailScreen extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.procedure.tr(context: context).toText12(color: AppColors.textColor, isBold: true), - LocaleKeys.amountOnly.tr(context: context).toText12(color: AppColors.textColor, isBold: true), + Expanded( + flex: 3, + child: LocaleKeys.procedure.tr(context: context).toText12(color: AppColors.textColor, isBold: true), + ), + SizedBox(width: 8.w), + SizedBox( + width: 70.w, + child: LocaleKeys.approved.tr(context: context).toText12( + color: AppColors.textColor, + isBold: true, + textAlignment: TextAlign.center, + ), + ), + SizedBox(width: 8.w), + SizedBox( + width: 60.w, + child: LocaleKeys.amountOnly.tr(context: context).toText12( + color: AppColors.textColor, + isBold: true, + textAlignment: TextAlign.end, + ), + ), ], ), SizedBox(height: 16.h), @@ -221,6 +241,8 @@ class RefundRequestDetailScreen extends StatelessWidget { } Widget _buildProcedureRow(BuildContext context, InvoiceRefundProcedure procedure) { + final isApproved = procedure.isApproved ?? false; + return Padding( padding: EdgeInsets.symmetric(vertical: 6.h), child: Row( @@ -232,7 +254,23 @@ class RefundRequestDetailScreen extends StatelessWidget { child: (procedure.procedureName ?? '-').toText12(color: AppColors.textColor), ), SizedBox(width: 8.w), - (procedure.patientPaidAmount?.toString() ?? '-').toText12(color: AppColors.textColor, isBold: true), + SizedBox( + width: 70.w, + child: (isApproved ? LocaleKeys.yes : LocaleKeys.no).tr(context: context).toText12( + color: AppColors.textColor, + isBold: true, + textAlignment: TextAlign.center, + ), + ), + SizedBox(width: 8.w), + SizedBox( + width: 60.w, + child: (procedure.patientPaidAmount?.toStringAsFixed(2) ?? '-').toText12( + color: AppColors.textColor, + isBold: true, + textAlignment: TextAlign.end, + ), + ), ], ), ); -- 2.30.2 From 3d4609a1a9122b8fabcc1536dfb2c26153f8c707 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Fri, 12 Jun 2026 12:54:08 +0300 Subject: [PATCH 3/9] completed refund CR --- assets/images/svg/open_file_icon.svg | 4 + assets/langs/ar-SA.json | 10 +- assets/langs/en-US.json | 10 +- lib/core/app_assets.dart | 1 + .../habib_wallet/habib_wallet_repo.dart | 38 ++++ .../habib_wallet/habib_wallet_view_model.dart | 57 ++++- .../submit_refund_request_model.dart | 5 + .../refund_request_view_model.dart | 47 +++- lib/generated/locale_keys.g.dart | 9 +- .../contact_us/feedback_page.dart | 3 +- .../widget/e_referral_other_details.dart | 1 + .../habib_wallet/habib_wallet_page.dart | 39 +++- .../withdraw_request_create_page.dart | 145 +++++++++--- .../refund_request/refund_history_screen.dart | 2 +- .../refund_request_create_page.dart | 210 +++++++++++++++++- .../refund_request_detail_screen.dart | 4 + lib/widgets/attachment_options.dart | 13 +- lib/widgets/image_picker.dart | 152 +++++++++++-- 18 files changed, 677 insertions(+), 73 deletions(-) create mode 100644 assets/images/svg/open_file_icon.svg diff --git a/assets/images/svg/open_file_icon.svg b/assets/images/svg/open_file_icon.svg new file mode 100644 index 00000000..91d64fe5 --- /dev/null +++ b/assets/images/svg/open_file_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 2c312334..9cfa8c1a 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1870,6 +1870,7 @@ "selectRefundMethod": "اختر طريقة الاسترداد", "hmgWallet": "محفظة HMG", "bankTransfer": "تحويل بنكي", + "cash": "نقدي", "selectRefundReason": "اختر سبب الاسترداد", "refundReason": "سبب الاسترداد", "done": "تم", @@ -1898,5 +1899,12 @@ "invoiceHistory": "سجل الفواتير", "selectAdvance": "اختر الدفعة المقدمة", "advanceNo": "رقم الدفعة", - "noAdvancesAvailable": "لا توجد دفعات متاحة للاسترداد" + "noAdvancesAvailable": "لا توجد دفعات متاحة للاسترداد", + "maximumFilesReached": "الحد الأقصى 4 ملفات مسموح به", + "fileSizeTooLarge": "يجب ألا يتجاوز حجم الملف 2 ميغابايت", + "image": "صورة", + "refundCannotBeProcessedPatientAdmitted": "لا يمكن معالجة الاسترداد أثناء إقامة المريض", + "cashRefundLimitExceeded": "يمكنك فقط التقدم بطلب استرداد نقدي إذا كان المبلغ أقل من 5000. لمزيد من المعلومات يرجى زيارتنا.", + "cashRefundInstructions": "بمجرد الموافقة على الطلب، يرجى زيارة صراف مستشفى الحبيب الطبي أو الحبيب للرعاية الأولية لاستلام النقد. عند نافذة الصراف، سوف تتلقى رمز التحقق على رقم هاتفك المسجل. قدم الرمز للصراف للتحقق منه. سيتم صرف النقد فقط بعد التحقق الناجح.", + "cashRefundApprovalInstructions": "بمجرد الموافقة على الطلب،\nيرجى زيارة صراف مستشفى الحبيب الطبي أو الحبيب للرعاية الأولية لاستلام النقد.\nعند نافذة الصراف، سوف تتلقى رمز التحقق على رقم هاتفك المسجل. قدم الرمز للصراف للتحقق منه. سيتم صرف النقد فقط بعد التحقق الناجح." } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 2fe76d05..f367cfe5 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1862,6 +1862,7 @@ "selectRefundMethod": "Select Refund Method", "hmgWallet": "HMG Wallet", "bankTransfer": "Bank Transfer", + "cash": "Cash", "selectRefundReason": "Select Refund Reason", "refundReason": "Refund Reason", "done": "Done", @@ -1889,7 +1890,14 @@ "invoiceHistory": "Invoice History", "selectAdvance": "Select Advance", "advanceNo": "Advance No", - "noAdvancesAvailable": "No advances available for refund" + "noAdvancesAvailable": "No advances available for refund", + "maximumFilesReached": "Maximum 4 files allowed", + "fileSizeTooLarge": "File size must not exceed 2 MB", + "image": "Image", + "refundCannotBeProcessedPatientAdmitted": "Refund cannot be processed while the patient is admitted", + "cashRefundLimitExceeded": "You can only apply for cash refund if the amount is less than 5000. For more information please visit us.", + "cashRefundInstructions": "Once the request is approved, Please visit HMG Hospital or HMG Primary Care cashier to receive cash. At the cashier counter, you will receive an OTP on your registered mobile number. Provide the OTP to the cashier for validation. Cash will be released only after successful validation.", + "cashRefundApprovalInstructions": "Once the request is approved,\nPlease visit HMG Hospital or HMG Primary Care cashier to receive cash.\nAt the cashier counter, you will receive an OTP on your registered mobile number. Provide the OTP to the cashier for validation. Cash will be released only after successful validation." } diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 37f9c4fd..31c2473c 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -377,6 +377,7 @@ class AppAssets { static const String invoiceListIcon = '$svgBasePath/invoice_list_icon.svg'; static const String open_camera = '$svgBasePath/open_camera.svg'; static const String gallery = '$svgBasePath/gallery.svg'; + static const String openFileIcon = '$svgBasePath/open_file_icon.svg'; static const String files = '$svgBasePath/files.svg'; static const String online_checkin_icon = '$svgBasePath/online_checkin_icon.svg'; static const String hmg_pharmacy_logo = '$svgBasePath/hmg_pharmacy_logo.svg'; diff --git a/lib/features/habib_wallet/habib_wallet_repo.dart b/lib/features/habib_wallet/habib_wallet_repo.dart index ff255092..b5f8d30c 100644 --- a/lib/features/habib_wallet/habib_wallet_repo.dart +++ b/lib/features/habib_wallet/habib_wallet_repo.dart @@ -22,6 +22,8 @@ abstract class HabibWalletRepo { Future>> getLakumAccountInformation({required String identificationNumber}); Future>> getLakumAccountInquiryInformation({required String lakumAccountNumber}); + + Future>> checkIsPatientAdmittedOrAdmissionRequest({required bool isActiveAppointment}); } class HabibWalletRepoImp implements HabibWalletRepo { @@ -306,4 +308,40 @@ class HabibWalletRepoImp implements HabibWalletRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> checkIsPatientAdmittedOrAdmissionRequest({required bool isActiveAppointment}) async { + Map requestBody = { + "IsActiveAppointment": isActiveAppointment, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + CHECK_IF_PATIENT_ADMITTED, + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart index ce18bf09..44c0763c 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -12,6 +12,7 @@ import 'package:hmg_patient_app_new/features/refund_request/models/req_models/su 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/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/services/error_handler_service.dart'; @@ -42,6 +43,8 @@ class HabibWalletViewModel extends ChangeNotifier { // ── Wallet Withdrawal Step Management ───────────────────────────────────── int currentWithdrawStep = 0; RefundableAdvancesItem? selectedAdvance; + RefundTabType? selectedWithdrawMethod; // Bank Transfer or Cash + static const double cashRefundMaxAmount = 5000.0; // Maximum amount for cash refund void nextWithdrawStep() { if (currentWithdrawStep < 1) { @@ -62,8 +65,16 @@ class HabibWalletViewModel extends ChangeNotifier { notifyListeners(); } + void setSelectedWithdrawMethod(RefundTabType? method) { + selectedWithdrawMethod = method; + notifyListeners(); + } + double get selectedAdvanceBalance => selectedAdvance?.balanceAmount ?? 0.0; + // Check if selected advance balance exceeds cash limit + bool get isCashWithdrawAmountExceeded => selectedAdvanceBalance > cashRefundMaxAmount; + bool isSearchedFileNumberDataShown = false; int currentIndex = 0; @@ -280,6 +291,7 @@ class HabibWalletViewModel extends ChangeNotifier { required String patientNationalId, required int projectId, required String projectName, + required String refundDestination, // '2' for Bank Transfer, '3' for Cash Function(SubmitRefundResponseModel)? onSuccess, Function(String)? onError, }) async { @@ -316,8 +328,8 @@ class HabibWalletViewModel extends ChangeNotifier { refundDoctorName: '', refundClinicId: 0, refundClinicName: '', - refundDestination: '2', - // only bank transfer + refundDestination: refundDestination, + // Use provided refund destination refundIban: iban, refundRemarks: remarks, refundTotalAmount: selectedAdvanceBalance, @@ -604,4 +616,45 @@ class HabibWalletViewModel extends ChangeNotifier { }, ); } + + // ── Check Is Patient Admitted Or Admission Request ──────────────────────── + bool isCheckingPatientAdmitted = false; + + Future checkIsPatientAdmittedOrAdmissionRequest({ + required bool isActiveAppointment, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + isCheckingPatientAdmitted = true; + notifyListeners(); + + final result = await habibWalletRepo.checkIsPatientAdmittedOrAdmissionRequest( + isActiveAppointment: isActiveAppointment, + ); + + result.fold( + (failure) { + isCheckingPatientAdmitted = false; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (apiResponse) { + isCheckingPatientAdmitted = false; + notifyListeners(); + if (apiResponse.messageStatus == 1) { + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } else { + if (onError != null) { + onError(apiResponse.errorMessage ?? LocaleKeys.failed.tr()); + } + } + }, + ); + } } 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 93eaadca..ae8ac20e 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 @@ -79,6 +79,9 @@ class SubmitRefundRequestModel { /// Populated only for wallet withdrawal refunds (refundType == '2'). final List refundAdvancesList; + /// List of base64 encoded images for refund attachments (max 4 files, each max 2MB) + final List refundAttachmentsBase64List; + const SubmitRefundRequestModel({ required this.projectId, required this.refundType, @@ -101,6 +104,7 @@ class SubmitRefundRequestModel { required this.isRefundFastTrack, required this.refundProceduresList, this.refundAdvancesList = const [], + this.refundAttachmentsBase64List = const [], }); /// refundType '1' = invoice refund → sends Refund_ProceduresList @@ -122,6 +126,7 @@ class SubmitRefundRequestModel { "Refund_Remarks": refundRemarks, "Refund_TotalRefundAmount": refundTotalAmount, "Refund_IsFastTrack": isRefundFastTrack, + "Refund_AttachmentsBase64List": refundAttachmentsBase64List, }; if (refundType == '2') { diff --git a/lib/features/refund_request/refund_request_view_model.dart b/lib/features/refund_request/refund_request_view_model.dart index d539d97f..1b77c007 100644 --- a/lib/features/refund_request/refund_request_view_model.dart +++ b/lib/features/refund_request/refund_request_view_model.dart @@ -11,7 +11,7 @@ import 'package:hmg_patient_app_new/features/refund_request/refund_request_repo. 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 } +enum RefundTabType { invoices, hmgWallet, cash } extension RefundTabTypeExtension on RefundTabType { String toDisplayString() { @@ -20,6 +20,8 @@ extension RefundTabTypeExtension on RefundTabType { return LocaleKeys.hmgWallet.tr(); case RefundTabType.invoices: return LocaleKeys.bankTransfer.tr(); + case RefundTabType.cash: + return LocaleKeys.cash.tr(); } } } @@ -117,6 +119,8 @@ class RefundRequestViewModel extends ChangeNotifier { invoiceDetails = null; _ibanError = null; _descriptionError = null; + refundAttachmentList.clear(); + _attachmentError = null; // Use Future.microtask to avoid calling notifyListeners while tree is locked (e.g. during dispose) Future.microtask(() { if (hasListeners) notifyListeners(); @@ -305,6 +309,47 @@ class RefundRequestViewModel extends ChangeNotifier { return isValid; } + // ── Attachments ─────────────────────────────────────────────────────────── + List refundAttachmentList = []; // Stores base64 strings + String? _attachmentError; + static const int maxAttachments = 4; + static const int maxFileSizeBytes = 2 * 1024 * 1024; // 2MB + static const double cashRefundMaxAmount = 5000.0; // Maximum amount for cash refund + + String? get attachmentError => _attachmentError; + + // Check if total refundable amount exceeds cash limit + bool get isCashRefundAmountExceeded => totalRefundableAmount > cashRefundMaxAmount; + + void addRefundAttachment(String base64String) { + if (refundAttachmentList.length >= maxAttachments) { + _attachmentError = LocaleKeys.maximumFilesReached.tr(); + notifyListeners(); + return; + } + refundAttachmentList.add(base64String); + _attachmentError = null; + notifyListeners(); + } + + void removeRefundAttachment(int index) { + if (index >= 0 && index < refundAttachmentList.length) { + refundAttachmentList.removeAt(index); + _attachmentError = null; + notifyListeners(); + } + } + + void clearAttachmentError() { + _attachmentError = null; + notifyListeners(); + } + + void setAttachmentError(String error) { + _attachmentError = error; + notifyListeners(); + } + // ── Payment Method ──────────────────────────────────────────────────────── RefundTabType? selectedPaymentMethod; diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 1cf39e81..f3341345 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1875,6 +1875,7 @@ abstract class LocaleKeys { static const cannotOpenThisFile = 'cannotOpenThisFile'; static const thisInvoiceIsNotEligibleForRefund = 'thisInvoiceIsNotEligibleForRefund'; static const refundDetails = 'refundDetails'; + static const refund = 'refund'; static const requestNo = 'requestNo'; static const referenceNo = 'referenceNo'; static const advanceNumber = 'advanceNumber'; @@ -1883,6 +1884,12 @@ abstract class LocaleKeys { static const selectAdvance = 'selectAdvance'; static const advanceNo = 'advanceNo'; static const noAdvancesAvailable = 'noAdvancesAvailable'; - static const refund = 'refund'; + static const maximumFilesReached = 'maximumFilesReached'; + static const fileSizeTooLarge = 'fileSizeTooLarge'; + static const image = 'image'; + static const refundCannotBeProcessedPatientAdmitted = 'refundCannotBeProcessedPatientAdmitted'; + static const cashRefundLimitExceeded = 'cashRefundLimitExceeded'; + static const cashRefundInstructions = 'cashRefundInstructions'; + static const cashRefundApprovalInstructions = 'cashRefundApprovalInstructions'; } diff --git a/lib/presentation/contact_us/feedback_page.dart b/lib/presentation/contact_us/feedback_page.dart index 00576e9d..743acedf 100644 --- a/lib/presentation/contact_us/feedback_page.dart +++ b/lib/presentation/contact_us/feedback_page.dart @@ -354,8 +354,7 @@ class FeedbackPage extends StatelessWidget { context, true, (String image, file) { - print(image); - print(file); + // Navigator.pop(context); contactUsViewModel.addFeedbackAttachment(image); }, diff --git a/lib/presentation/e_referral/widget/e_referral_other_details.dart b/lib/presentation/e_referral/widget/e_referral_other_details.dart index 043dda21..64dc3730 100644 --- a/lib/presentation/e_referral/widget/e_referral_other_details.dart +++ b/lib/presentation/e_referral/widget/e_referral_other_details.dart @@ -217,6 +217,7 @@ class _OtherDetailsStepState extends State { (String image, File file) { _addInsuranceDocument(image, file, formManager); }, + allowMultiple: true, // Enable multiple selection ); }, ), diff --git a/lib/presentation/habib_wallet/habib_wallet_page.dart b/lib/presentation/habib_wallet/habib_wallet_page.dart index 4f056dad..5463d94e 100644 --- a/lib/presentation/habib_wallet/habib_wallet_page.dart +++ b/lib/presentation/habib_wallet/habib_wallet_page.dart @@ -150,7 +150,42 @@ class _HabibWalletState extends State { borderColor: Colors.transparent, padding: EdgeInsets.fromLTRB(4, 0, 12, 0), fontSize: 14.f, - onPressed: () => Navigator.of(context).push(CustomPageRoute(page: WithdrawRequestCreatePage())), + onPressed: () async { + // Show loader + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context)); + + // Check if patient is admitted + await habibWalletVM.checkIsPatientAdmittedOrAdmissionRequest( + isActiveAppointment: false, + onSuccess: (data) { + LoaderBottomSheet.hideLoader(); + + // Check if patient is admitted + bool isAdmitted = data['isAdmitted'] ?? false; + + if (isAdmitted) { + // Show error message if patient is admitted + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget( + loadingText: LocaleKeys.refundCannotBeProcessedPatientAdmitted.tr(context: context), + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } else { + // Navigate to withdraw page if not admitted + Navigator.of(context).push(CustomPageRoute(page: WithdrawRequestCreatePage())); + } + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + // Navigate to withdraw page even on error (messageStatus == 2) + Navigator.of(context).push(CustomPageRoute(page: WithdrawRequestCreatePage())); + }, + ); + }, ), ), ], @@ -307,5 +342,3 @@ class _HabibWalletState extends State { ); } } - - diff --git a/lib/presentation/habib_wallet/withdraw_request_create_page.dart b/lib/presentation/habib_wallet/withdraw_request_create_page.dart index a5909e3a..6b8bb6f9 100644 --- a/lib/presentation/habib_wallet/withdraw_request_create_page.dart +++ b/lib/presentation/habib_wallet/withdraw_request_create_page.dart @@ -12,6 +12,7 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_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_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'; @@ -21,6 +22,7 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/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'; @@ -225,14 +227,74 @@ class _WithdrawRequestCreatePageState extends State { } } - // ── Step 2 — Bank Details ──────────────────────────────────────────────── + // ── Step 2 — Refund Method Selection ───────────────────────────────────── - bool get _isFormValid { - final vm = _habibWalletViewModel; - return vm.selectedHospital != null && - !vm.isAdvancesLoading && - _bankDetailsController.text.trim().isNotEmpty && - _descriptionController.text.trim().isNotEmpty; + Widget _buildRefundMethodSelection(HabibWalletViewModel vm) { + final paymentOptions = [ + RefundTabType.invoices, // Bank Transfer + RefundTabType.cash, // Cash + ]; + + 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.selectedWithdrawMethod, + title: type.toDisplayString(), + onChanged: (value) { + if (value != null) vm.setSelectedWithdrawMethod(value); + }, + ), + if (index != paymentOptions.length - 1) + Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w), + ], + ); + }, + ), + ), + if (vm.selectedWithdrawMethod == RefundTabType.cash && vm.isCashWithdrawAmountExceeded) ...[ + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.warningColorYellow.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12.r), + border: Border.all(color: AppColors.warningColorYellow, width: 1), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.info_outline, color: AppColors.warningColorYellow, size: 20.h), + SizedBox(width: 12.w), + Expanded( + child: LocaleKeys.cashRefundLimitExceeded.tr(context: context).toText14( + color: AppColors.textColor, + ), + ), + ], + ), + ), + ], + ], + ); } // ── Bottom bar ──────────────────────────────────────────────────────────── @@ -245,9 +307,18 @@ class _WithdrawRequestCreatePageState extends State { // Step 1 validation: advance selected final isStep1Valid = vm.selectedAdvance != null; - // Step 2 validation: bank details filled - final isStep2Valid = _bankDetailsController.text.trim().isNotEmpty && - _descriptionController.text.trim().isNotEmpty; + // Step 2 validation: + // - Refund method must be selected + // - If Bank Transfer selected, IBAN and description required + // - If Cash selected, amount must be <= 5000 SAR + final isBankTransfer = vm.selectedWithdrawMethod == RefundTabType.invoices; + final isCash = vm.selectedWithdrawMethod == RefundTabType.cash; + final isStep2Valid = vm.selectedWithdrawMethod != null && + (isCash + ? !vm.isCashWithdrawAmountExceeded // Cash: check amount limit + : (isBankTransfer && + _bankDetailsController.text.trim().isNotEmpty && + _descriptionController.text.trim().isNotEmpty)); // Bank: check fields return Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( @@ -280,9 +351,7 @@ class _WithdrawRequestCreatePageState extends State { ), ], CustomButton( - text: isStep2 - ? LocaleKeys.submitRequest.tr(context: context) - : LocaleKeys.next.tr(context: context), + text: isStep2 ? LocaleKeys.submitRequest.tr(context: context) : LocaleKeys.next.tr(context: context), isDisabled: isStep2 ? !isStep2Valid : !isStep1Valid, onPressed: () { if (!isStep2) { @@ -291,14 +360,16 @@ class _WithdrawRequestCreatePageState extends State { vm.nextWithdrawStep(); } } else { - // Submit - final isFormValid = vm.validateWithdrawForm( - iban: _bankDetailsController.text, - description: _descriptionController.text, - ); - if (isFormValid) { - _handleWalletWithdrawalSubmit(vm); + // Submit - only validate bank details if Bank Transfer is selected + final isBankTransfer = vm.selectedWithdrawMethod == RefundTabType.invoices; + if (isBankTransfer) { + final isFormValid = vm.validateWithdrawForm( + iban: _bankDetailsController.text, + description: _descriptionController.text, + ); + if (!isFormValid) return; } + _handleWalletWithdrawalSubmit(vm); } }, ).paddingSymmetrical(24.h, 24.h), @@ -311,8 +382,7 @@ class _WithdrawRequestCreatePageState extends State { Widget _buildProjectSelectionAndBalance(HabibWalletViewModel habibWalletViewModel) { final isArabic = _appState.isArabic(); - final showAmount = habibWalletViewModel.currentWithdrawStep == 1 && - habibWalletViewModel.selectedAdvance != null; + final showAmount = habibWalletViewModel.currentWithdrawStep == 1 && habibWalletViewModel.selectedAdvance != null; return Container( padding: EdgeInsets.all(16.h), @@ -472,6 +542,11 @@ class _WithdrawRequestCreatePageState extends State { LoaderBottomSheet.showLoader(); final user = _appState.getAuthenticatedUser(); final hospital = vm.selectedHospital; + + // Determine refund destination: '2' = Bank Transfer, '3' = Cash + final isCash = vm.selectedWithdrawMethod == RefundTabType.cash; + final refundDestination = isCash ? '3' : '2'; + await vm.submitWalletWithdrawalRequest( iban: _bankDetailsController.text.trim(), remarks: _descriptionController.text.trim(), @@ -481,9 +556,10 @@ class _WithdrawRequestCreatePageState extends State { patientNationalId: user?.patientIdentificationNo?.toString() ?? '', projectId: hospital?.mainProjectID ?? 0, projectName: hospital?.name ?? '', + refundDestination: refundDestination, onSuccess: (response) { LoaderBottomSheet.hideLoader(); - _showWithdrawalSuccessBottomSheet(response.refNo ?? ''); + _showWithdrawalSuccessBottomSheet(response.refNo ?? '', refundDestination); }, onError: (err) { LoaderBottomSheet.hideLoader(); @@ -502,7 +578,10 @@ class _WithdrawRequestCreatePageState extends State { ); } - void _showWithdrawalSuccessBottomSheet(String refNo) { + void _showWithdrawalSuccessBottomSheet(String refNo, String refundDestination) { + // refundDestination: '2' = Bank Transfer, '3' = Cash + final isCashRefund = refundDestination == '3'; + showCommonBottomSheetWithoutHeight( context, child: Padding( @@ -521,7 +600,11 @@ class _WithdrawRequestCreatePageState extends State { SizedBox(width: 6.w), refNo.toText18(isBold: true, color: AppColors.textColor, isEnglishOnly: true), ], - ).paddingSymmetrical(24.w, 0), + ).paddingSymmetrical(16.w, 0), + ], + if (isCashRefund) ...[ + SizedBox(height: 16.h), + LocaleKeys.cashRefundApprovalInstructions.tr(context: context).toText14(color: AppColors.textColor).paddingSymmetrical(16.w, 0), ], SizedBox(height: 24.h), Row( @@ -627,7 +710,16 @@ class _WithdrawRequestCreatePageState extends State { key: const ValueKey('step2'), child: Padding( padding: EdgeInsets.symmetric(horizontal: 24.w), - child: _buildBankInputDetails(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildRefundMethodSelection(habibWalletViewModel), + if (habibWalletViewModel.selectedWithdrawMethod == RefundTabType.invoices) ...[ + SizedBox(height: 16.h), + _buildBankInputDetails(), + ], + ], + ), ), ), ), @@ -644,4 +736,3 @@ class _WithdrawRequestCreatePageState extends State { ); } } - diff --git a/lib/presentation/refund_request/refund_history_screen.dart b/lib/presentation/refund_request/refund_history_screen.dart index 630c5dc0..f689ed4a 100644 --- a/lib/presentation/refund_request/refund_history_screen.dart +++ b/lib/presentation/refund_request/refund_history_screen.dart @@ -12,7 +12,7 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; -import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_detail_screen.dart'; +import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_detail_screen.dart' show RefundRequestDetailScreen; 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'; diff --git a/lib/presentation/refund_request/refund_request_create_page.dart b/lib/presentation/refund_request/refund_request_create_page.dart index a795cf16..afef99ae 100644 --- a/lib/presentation/refund_request/refund_request_create_page.dart +++ b/lib/presentation/refund_request/refund_request_create_page.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -20,6 +22,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/image_picker.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/radio_list_tile_widget.dart'; @@ -240,6 +243,7 @@ class _RefundRequestCreatePageState extends State { final paymentOptions = [ RefundTabType.hmgWallet, RefundTabType.invoices, + RefundTabType.cash, ]; return Column( @@ -277,14 +281,191 @@ class _RefundRequestCreatePageState extends State { }, ), ), + if (vm.selectedPaymentMethod == RefundTabType.cash && vm.isCashRefundAmountExceeded) ...[ + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.warningColorYellow.withOpacity(0.1), + borderRadius: BorderRadius.circular(12.r), + border: Border.all(color: AppColors.warningColorYellow, width: 1), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.info_outline, color: AppColors.warningColorYellow, size: 20.h), + SizedBox(width: 12.w), + Expanded( + child: LocaleKeys.cashRefundLimitExceeded.tr(context: context).toText14( + color: AppColors.textColor, + ), + ), + ], + ), + ), + ], if (vm.selectedPaymentMethod == RefundTabType.invoices) ...[ SizedBox(height: 16.h), _buildBankInputDetails(vm), + SizedBox(height: 16.h), + _buildImageUploadSection(vm), ], ], ); } + Widget _buildImageUploadSection(RefundRequestViewModel vm) { + return Consumer( + builder: (context, refundVM, _) { + final hasError = refundVM.attachmentError != null; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Upload button + InkWell( + onTap: () { + if (refundVM.refundAttachmentList.length >= 4) { + refundVM.setAttachmentError(LocaleKeys.maximumFilesReached.tr(context: context)); + return; + } + ImageOptions.showImageOptionsNew( + context, + true, + (String base64Image, File file) async { + // Validate file size (2MB max) + final fileSize = await file.length(); + if (fileSize > RefundRequestViewModel.maxFileSizeBytes) { + refundVM.setAttachmentError(LocaleKeys.fileSizeTooLarge.tr(context: context)); + return; + } + + // Check max files limit + if (refundVM.refundAttachmentList.length >= RefundRequestViewModel.maxAttachments) { + refundVM.setAttachmentError(LocaleKeys.maximumFilesReached.tr(context: context)); + return; + } + + // Add attachment + refundVM.addRefundAttachment(base64Image); + }, + title: LocaleKeys.selectAttachment.tr(context: context), + allowMultiple: true, // Enable multiple selection + ); + }, + child: Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + border: hasError ? Border.all(color: AppColors.primaryRedBorderColor, width: 1) : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + height: 40.h, + width: 40.h, + padding: EdgeInsets.all(8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + borderRadius: 12.r, + color: AppColors.greyColor, + ), + child: Utils.buildSvgWithAssets(icon: AppAssets.selectImageIcon, applyThemeColor: false), + ), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.uploadImage.tr(context: context).toText12( + color: AppColors.inputLabelTextColor, + isBold: true, + ), + LocaleKeys.selectImage.tr(context: context).toText14( + color: AppColors.inputLabelTextColor, + isBold: true, + ), + ], + ), + ], + ), + if (hasError) ...[ + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + refundVM.attachmentError!, + style: TextStyle( + color: AppColors.primaryRedColor, + fontSize: 12.f, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), + ], + ), + ), + ), + // Selected images list + if (refundVM.refundAttachmentList.isNotEmpty) ...[ + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: ListView.separated( + padding: EdgeInsets.all(16.h), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: refundVM.refundAttachmentList.length, + separatorBuilder: (context, index) => SizedBox(height: 12.h), + itemBuilder: (BuildContext context, int index) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Icons.attach_file, + color: Color(0xff2B353E), + size: 20.h, + ), + SizedBox(width: 8.w), + "${LocaleKeys.image.tr(context: context)} ${index + 1}".toText14(), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.cancel_circle_icon, width: 20.h, height: 20.h).onPress(() { + refundVM.removeRefundAttachment(index); + }), + ], + ); + }, + ), + ), + ], + ], + ); + }, + ); + } + // ── Bank Transfer Input ─────────────────────────────────────────────────── Widget _buildBankInputDetails(RefundRequestViewModel vm) { @@ -522,7 +703,8 @@ class _RefundRequestCreatePageState extends State { isDisabled: isStep2 ? (refundRequestViewModel.selectedPaymentMethod == null || (refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices && - (_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty))) + (_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty)) || + (refundRequestViewModel.selectedPaymentMethod == RefundTabType.cash && refundRequestViewModel.isCashRefundAmountExceeded)) : (refundRequestViewModel.selectedProcedures.isEmpty || // For Outpatient/ER: require refund reasons // For Inpatient/Package: skip refund reason validation @@ -557,12 +739,24 @@ class _RefundRequestCreatePageState extends State { LoaderBottomSheet.showLoader(); final isBankTransfer = vm.selectedPaymentMethod == RefundTabType.invoices; + final isCash = vm.selectedPaymentMethod == RefundTabType.cash; 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(); + + // Determine refund destination: 1 = HMG Wallet, 2 = Bank Transfer, 3 = Cash + String refundDestination; + if (isCash) { + refundDestination = '3'; + } else if (isBankTransfer) { + refundDestination = '2'; + } else { + refundDestination = '1'; // HMG Wallet + } + final request = SubmitRefundRequestModel( projectId: invoice.projectId ?? 0, refundType: '1', @@ -579,10 +773,11 @@ class _RefundRequestCreatePageState extends State { refundDoctorName: invoice.doctorName ?? '', refundClinicId: invoice.clinicId ?? 0, refundClinicName: invoice.clinicName ?? '', - refundDestination: isBankTransfer ? '2' : '1', + refundDestination: refundDestination, refundIban: isBankTransfer ? _ibanController.text.trim() : '', refundRemarks: isBankTransfer ? _descriptionController.text.trim() : '', refundTotalAmount: vm.totalRefundableAmount, + refundAttachmentsBase64List: vm.refundAttachmentList, refundProceduresList: vm.selectedProcedures .map((p) => SubmitRefundProcedureItem( lineItemNo: p.apiLineItemNo ?? 0, @@ -598,7 +793,7 @@ class _RefundRequestCreatePageState extends State { request: request, onSuccess: (response) { LoaderBottomSheet.hideLoader(); - _showSuccessBottomSheet(response.refNo ?? ''); + _showSuccessBottomSheet(response.refNo ?? '', refundDestination); }, onError: (err) { LoaderBottomSheet.hideLoader(); @@ -617,7 +812,10 @@ class _RefundRequestCreatePageState extends State { ); } - void _showSuccessBottomSheet(String refNo) { + void _showSuccessBottomSheet(String refNo, String refundDestination) { + // refundDestination: '1' = HMG Wallet, '2' = Bank Transfer, '3' = Cash + final isCashRefund = refundDestination == '3'; + showCommonBottomSheetWithoutHeight( context, child: Padding( @@ -638,6 +836,10 @@ class _RefundRequestCreatePageState extends State { ], ).paddingSymmetrical(24.w, 0), ], + if (isCashRefund) ...[ + SizedBox(height: 16.h), + LocaleKeys.cashRefundApprovalInstructions.tr(context: context).toText14(color: AppColors.textColor), + ], SizedBox(height: 24.h), Row( children: [ diff --git a/lib/presentation/refund_request/refund_request_detail_screen.dart b/lib/presentation/refund_request/refund_request_detail_screen.dart index 85f399b4..856f1394 100644 --- a/lib/presentation/refund_request/refund_request_detail_screen.dart +++ b/lib/presentation/refund_request/refund_request_detail_screen.dart @@ -10,7 +10,9 @@ import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/r 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/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; class RefundRequestDetailScreen extends StatelessWidget { final RefundInvoiceModel invoice; @@ -240,6 +242,8 @@ class RefundRequestDetailScreen extends StatelessWidget { ); } + + Widget _buildProcedureRow(BuildContext context, InvoiceRefundProcedure procedure) { final isApproved = procedure.isApproved ?? false; diff --git a/lib/widgets/attachment_options.dart b/lib/widgets/attachment_options.dart index bbc0bc6d..9fe70ddd 100644 --- a/lib/widgets/attachment_options.dart +++ b/lib/widgets/attachment_options.dart @@ -1,4 +1,5 @@ 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'; @@ -8,7 +9,6 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; -import 'package:flutter/material.dart'; class AttachmentOptions extends StatelessWidget { final Future Function() onCameraTap; @@ -16,13 +16,8 @@ class AttachmentOptions extends StatelessWidget { final VoidCallback onFilesTap; final bool showFilesOption; - const AttachmentOptions({ - Key? key, - required this.onCameraTap, - required this.onGalleryTap, - required this.onFilesTap, - this.showFilesOption = true - }) : super(key: key); + const AttachmentOptions({Key? key, required this.onCameraTap, required this.onGalleryTap, required this.onFilesTap, this.showFilesOption = true}) + : super(key: key); @override Widget build(BuildContext context) { @@ -52,7 +47,7 @@ class AttachmentOptions extends StatelessWidget { if (showFilesOption) checkInOptionCard( appState, - AppAssets.file_icon, + AppAssets.openFileIcon, LocaleKeys.openFiles.tr(context: context), LocaleKeys.selectDocumentFiles.tr(context: context), ).onPress(() async { diff --git a/lib/widgets/image_picker.dart b/lib/widgets/image_picker.dart index e849cc96..e92ef282 100644 --- a/lib/widgets/image_picker.dart +++ b/lib/widgets/image_picker.dart @@ -22,6 +22,7 @@ class ImageOptions { bool showFilesOption, Function(String, File) image, { String? title, + bool allowMultiple = false, // Allow multiple file/image selection Future Function()? checkCameraPermission, Future Function()? checkGalleryPermission, }) { @@ -51,7 +52,11 @@ class ImageOptions { String fileName = _image.path; var bytes = File(fileName).readAsBytesSync(); String base64Encode = base64.encode(bytes); - image(base64Encode, _image); + + // Convert to data URI format + String mimeType = _getMimeType(fileName); + String dataUri = 'data:$mimeType;base64,$base64Encode'; + image(dataUri, _image); } }, onGalleryTap: () async { @@ -66,21 +71,46 @@ class ImageOptions { Navigator.pop(context); // Close bottom sheet if (Platform.isAndroid) { - galleryImageAndroid(image); + galleryImageAndroid(image, allowMultiple: allowMultiple); } else { - XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery, imageQuality: 20); - if (pickedFile == null) return; + if (allowMultiple) { + // iOS/macOS: Pick multiple images + List pickedFiles = await picker.pickMultiImage(imageQuality: 20); + if (pickedFiles.isEmpty) return; - File _image = File(pickedFile.path); - String fileName = _image.path; - var bytes = File(fileName).readAsBytesSync(); - String base64Encode = base64.encode(bytes); - image(base64Encode, _image); + for (var pickedFile in pickedFiles) { + File _image = File(pickedFile.path); + var bytes = await _image.readAsBytes(); + String base64Encode = base64.encode(bytes); + + // Convert to data URI format + String mimeType = _getMimeType(pickedFile.path); + String dataUri = 'data:$mimeType;base64,$base64Encode'; + image(dataUri, _image); + } + } else { + // Single image selection + XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery, imageQuality: 20); + if (pickedFile == null) return; + + File _image = File(pickedFile.path); + String fileName = _image.path; + var bytes = File(fileName).readAsBytesSync(); + String base64Encode = base64.encode(bytes); + + // Convert to data URI format + String mimeType = _getMimeType(fileName); + String dataUri = 'data:$mimeType;base64,$base64Encode'; + image(dataUri, _image); + } } }, onFilesTap: () async { + Navigator.pop(context); // Close bottom sheet + FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.custom, + allowMultiple: allowMultiple, // Use the parameter allowedExtensions: [ 'jpg', 'jpeg', @@ -95,8 +125,22 @@ class ImageOptions { 'zip', ], ); - List files = result!.paths.map((path) => File(path!)).toList(); - image(result.files.first.path.toString(), files.first); + + if (result == null || result.files.isEmpty) return; + + // Process selected file(s) + for (var fileData in result.files) { + if (fileData.path != null) { + File selectedFile = File(fileData.path!); + var bytes = await selectedFile.readAsBytes(); + String base64Encode = base64.encode(bytes); + + // Convert to data URI format + String mimeType = _getMimeType(fileData.path!); + String dataUri = 'data:$mimeType;base64,$base64Encode'; + image(dataUri, selectedFile); + } + } }, ), callBackFunc: () {}, @@ -217,23 +261,46 @@ class ImageOptions { // } } -void galleryImageAndroid(Function(String, File) image) async { +void galleryImageAndroid(Function(String, File) image, {bool allowMultiple = false}) async { // Enable Android Photo Picker if available if (_pickerPlatform is ImagePickerAndroid) { (_pickerPlatform as ImagePickerAndroid).useAndroidPhotoPicker = true; } try { - final XFile? pickedFile = - await _pickerPlatform.getImageFromSource(source: ImageSource.gallery, options: const ImagePickerOptions(imageQuality: 20)); + if (allowMultiple) { + // Pick multiple images + final List pickedFiles = await picker.pickMultiImage(imageQuality: 20); - if (pickedFile == null) return; + if (pickedFiles.isEmpty) return; - File _image = File(pickedFile.path); - String fileName = _image.path; - var bytes = File(fileName).readAsBytesSync(); - String base64Encode = base64.encode(bytes); - image(base64Encode, _image); + for (var pickedFile in pickedFiles) { + File _image = File(pickedFile.path); + var bytes = await _image.readAsBytes(); + String base64Encode = base64.encode(bytes); + + // Convert to data URI format + String mimeType = _getMimeType(pickedFile.path); + String dataUri = 'data:$mimeType;base64,$base64Encode'; + image(dataUri, _image); + } + } else { + // Pick single image + final XFile? pickedFile = + await _pickerPlatform.getImageFromSource(source: ImageSource.gallery, options: const ImagePickerOptions(imageQuality: 20)); + + if (pickedFile == null) return; + + File _image = File(pickedFile.path); + String fileName = _image.path; + var bytes = File(fileName).readAsBytesSync(); + String base64Encode = base64.encode(bytes); + + // Convert to data URI format + String mimeType = _getMimeType(fileName); + String dataUri = 'data:$mimeType;base64,$base64Encode'; + image(dataUri, _image); + } } catch (e) { print('Error picking image: $e'); } @@ -256,12 +323,55 @@ void cameraImageAndroid(Function(String, File) image) async { String fileName = _image.path; var bytes = File(fileName).readAsBytesSync(); String base64Encode = base64.encode(bytes); - image(base64Encode, _image); + + // Convert to data URI format + String mimeType = _getMimeType(fileName); + String dataUri = 'data:$mimeType;base64,$base64Encode'; + image(dataUri, _image); } catch (e) { print('Error taking picture: $e'); } } +/// Helper function to determine MIME type from file extension +String _getMimeType(String filePath) { + final extension = filePath.split('.').last.toLowerCase(); + + switch (extension) { + case 'jpg': + case 'jpeg': + return 'image/jpeg'; + case 'png': + return 'image/png'; + case 'gif': + return 'image/gif'; + case 'webp': + return 'image/webp'; + case 'pdf': + return 'application/pdf'; + case 'doc': + return 'application/msword'; + case 'docx': + return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + case 'xls': + return 'application/vnd.ms-excel'; + case 'xlsx': + return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + case 'ppt': + return 'application/vnd.ms-powerpoint'; + case 'pptx': + return 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; + case 'txt': + return 'text/plain'; + case 'zip': + return 'application/zip'; + case 'rar': + return 'application/x-rar-compressed'; + default: + return 'application/octet-stream'; + } +} + class _BottomSheet extends StatelessWidget { final List children; -- 2.30.2 From e7f3095308cb31a5fd71757482f125bb4fea9407 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Sun, 14 Jun 2026 14:44:04 +0300 Subject: [PATCH 4/9] refund module feedback changes completed --- assets/langs/ar-SA.json | 1 + assets/langs/en-US.json | 1 + .../habib_wallet/habib_wallet_view_model.dart | 41 ++++ .../refund_request_list_response_model.dart | 3 + lib/generated/locale_keys.g.dart | 1 + .../withdraw_request_create_page.dart | 193 +++++++++++++++++- .../refund_request_create_page.dart | 31 ++- .../refund_request_detail_screen.dart | 70 ++++--- 8 files changed, 315 insertions(+), 26 deletions(-) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 9cfa8c1a..04b5d9c3 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1873,6 +1873,7 @@ "cash": "نقدي", "selectRefundReason": "اختر سبب الاسترداد", "refundReason": "سبب الاسترداد", + "rejectionReason": "سبب الرفض", "done": "تم", "refundReason1": "تم معالجة مريض الشركة كمدفوع ذاتي وإعادة فوترته", "refundReason2": "المريض لا يريد إجراء سريرياً", diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index f367cfe5..69c74a1b 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1865,6 +1865,7 @@ "cash": "Cash", "selectRefundReason": "Select Refund Reason", "refundReason": "Refund Reason", + "rejectionReason": "Rejection Reason", "done": "Done", "refundReason1": "Company patient treated as self-paying and re-invoiced", "refundReason2": "Patient does not want a clinical procedure", diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart index 44c0763c..05a3f83e 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -212,6 +212,43 @@ class HabibWalletViewModel extends ChangeNotifier { return isValid; } + // ==================== WITHDRAW ATTACHMENTS ==================================== + List withdrawAttachmentList = []; // Stores base64 strings + String? _withdrawAttachmentError; + static const int maxAttachments = 4; + static const int maxFileSizeBytes = 2 * 1024 * 1024; // 2MB + + String? get withdrawAttachmentError => _withdrawAttachmentError; + + void addWithdrawAttachment(String base64String) { + if (withdrawAttachmentList.length >= maxAttachments) { + _withdrawAttachmentError = LocaleKeys.maximumFilesReached.tr(); + notifyListeners(); + return; + } + withdrawAttachmentList.add(base64String); + _withdrawAttachmentError = null; + notifyListeners(); + } + + void removeWithdrawAttachment(int index) { + if (index >= 0 && index < withdrawAttachmentList.length) { + withdrawAttachmentList.removeAt(index); + _withdrawAttachmentError = null; + notifyListeners(); + } + } + + void clearWithdrawAttachmentError() { + _withdrawAttachmentError = null; + notifyListeners(); + } + + void setWithdrawAttachmentError(String error) { + _withdrawAttachmentError = error; + notifyListeners(); + } + // Validate recharge form - shows only first error bool validateRechargeForm(String amount) { // Clear previous errors @@ -335,6 +372,7 @@ class HabibWalletViewModel extends ChangeNotifier { refundTotalAmount: selectedAdvanceBalance, refundProceduresList: const [], refundAdvancesList: advances, + refundAttachmentsBase64List: withdrawAttachmentList, ); final result = await refundRequestRepo.submitRefund(request: request); @@ -401,8 +439,11 @@ class HabibWalletViewModel extends ChangeNotifier { isSubmittingWithdrawal = false; _ibanError = null; _withdrawDescriptionError = null; + _withdrawAttachmentError = null; + withdrawAttachmentList.clear(); currentWithdrawStep = 0; selectedAdvance = null; + selectedWithdrawMethod = null; if (!silent) notifyListeners(); } 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 015a7556..aeb75f1c 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 @@ -142,6 +142,7 @@ class InvoiceRefundProcedure { final String? procedureId; final String? procedureName; final String? refundReason; + final String? rejectionReason; final bool? isApproved; InvoiceRefundProcedure({ @@ -151,6 +152,7 @@ class InvoiceRefundProcedure { this.procedureId, this.procedureName, this.refundReason, + this.rejectionReason, this.isApproved, }); @@ -162,6 +164,7 @@ class InvoiceRefundProcedure { procedureId: json['ProcedureID'] as String?, procedureName: json['ProcedureName'] as String?, refundReason: json['RefundReason'] as String?, + rejectionReason: json['RejectionReason'] as String?, isApproved: json['IsApproved'] as bool?, ); } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index f3341345..751ccb40 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1859,6 +1859,7 @@ abstract class LocaleKeys { static const bankTransfer = 'bankTransfer'; static const selectRefundReason = 'selectRefundReason'; static const refundReason = 'refundReason'; + static const rejectionReason = 'rejectionReason'; static const refundReason1 = 'refundReason1'; static const refundReason2 = 'refundReason2'; static const refundReason3 = 'refundReason3'; diff --git a/lib/presentation/habib_wallet/withdraw_request_create_page.dart b/lib/presentation/habib_wallet/withdraw_request_create_page.dart index 6b8bb6f9..8161cbd8 100644 --- a/lib/presentation/habib_wallet/withdraw_request_create_page.dart +++ b/lib/presentation/habib_wallet/withdraw_request_create_page.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -20,6 +22,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/image_picker.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/radio_list_tile_widget.dart'; @@ -293,6 +296,33 @@ class _WithdrawRequestCreatePageState extends State { ), ), ], + if (vm.selectedWithdrawMethod == RefundTabType.cash) ...[ + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.alertSquare, + height: 24.h, + width: 24.h, + iconColor: AppColors.textColor, + ), + SizedBox(width: 12.w), + Expanded( + child: LocaleKeys.cashRefundApprovalInstructions.tr(context: context).toText12( + color: AppColors.greyInfoTextColor, + ), + ), + ], + ), + ), + ], ], ); } @@ -329,7 +359,7 @@ class _WithdrawRequestCreatePageState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (isStep2 && vm.selectedAdvance != null) ...[ + if (vm.selectedAdvance != null) ...[ Padding( padding: EdgeInsets.fromLTRB(24.w, 16.h, 24.w, 0), child: Row( @@ -351,7 +381,8 @@ class _WithdrawRequestCreatePageState extends State { ), ], CustomButton( - text: isStep2 ? LocaleKeys.submitRequest.tr(context: context) : LocaleKeys.next.tr(context: context), + icon: isStep2 ? AppAssets.refundIcon : null, + text: isStep2 ? LocaleKeys.submit.tr(context: context) : LocaleKeys.next.tr(context: context), isDisabled: isStep2 ? !isStep2Valid : !isStep1Valid, onPressed: () { if (!isStep2) { @@ -525,6 +556,160 @@ class _WithdrawRequestCreatePageState extends State { ); } + // ── Image Upload Section ────────────────────────────────────────────────── + + Widget _buildImageUploadSection(HabibWalletViewModel vm) { + return Consumer( + builder: (context, viewModel, _) { + final hasError = viewModel.withdrawAttachmentError != null; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Upload button + InkWell( + onTap: () { + if (viewModel.withdrawAttachmentList.length >= 4) { + viewModel.setWithdrawAttachmentError(LocaleKeys.maximumFilesReached.tr(context: context)); + return; + } + ImageOptions.showImageOptionsNew( + context, + true, + (String base64Image, File file) async { + // Validate file size (2MB max) + final fileSize = await file.length(); + if (fileSize > HabibWalletViewModel.maxFileSizeBytes) { + viewModel.setWithdrawAttachmentError(LocaleKeys.fileSizeTooLarge.tr(context: context)); + return; + } + + // Check max files limit + if (viewModel.withdrawAttachmentList.length >= HabibWalletViewModel.maxAttachments) { + viewModel.setWithdrawAttachmentError(LocaleKeys.maximumFilesReached.tr(context: context)); + return; + } + + // Add attachment + viewModel.addWithdrawAttachment(base64Image); + }, + title: LocaleKeys.selectAttachment.tr(context: context), + allowMultiple: true, // Enable multiple selection + ); + }, + child: Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + border: hasError ? Border.all(color: AppColors.primaryRedBorderColor, width: 1) : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + height: 40.h, + width: 40.h, + padding: EdgeInsets.all(8.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + borderRadius: 12.r, + color: AppColors.greyColor, + ), + child: Utils.buildSvgWithAssets(icon: AppAssets.selectImageIcon, applyThemeColor: false), + ), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.uploadImage.tr(context: context).toText12( + color: AppColors.inputLabelTextColor, + isBold: true, + ), + LocaleKeys.selectImage.tr(context: context).toText14( + color: AppColors.inputLabelTextColor, + isBold: true, + ), + ], + ), + ], + ), + if (hasError) ...[ + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + viewModel.withdrawAttachmentError!, + style: TextStyle( + color: AppColors.primaryRedColor, + fontSize: 12.f, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ], + ), + ), + Utils.buildSvgWithAssets(icon: AppAssets.arrow_down, width: 25.h, height: 25.h), + ], + ), + ), + ), + // Selected images list + if (viewModel.withdrawAttachmentList.isNotEmpty) ...[ + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, + ), + child: ListView.separated( + padding: EdgeInsets.all(16.h), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: viewModel.withdrawAttachmentList.length, + separatorBuilder: (context, index) => SizedBox(height: 12.h), + itemBuilder: (BuildContext context, int index) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Icons.attach_file, + color: Color(0xff2B353E), + size: 20.h, + ), + SizedBox(width: 8.w), + "${LocaleKeys.image.tr(context: context)} ${index + 1}".toText14(), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.cancel_circle_icon, width: 20.h, height: 20.h).onPress(() { + viewModel.removeWithdrawAttachment(index); + }), + ], + ); + }, + ), + ), + ], + ], + ); + }, + ); + } + // ── Submit ──────────────────────────────────────────────────────────────── void _handleWalletWithdrawalSubmit(HabibWalletViewModel vm) { @@ -718,6 +903,10 @@ class _WithdrawRequestCreatePageState extends State { SizedBox(height: 16.h), _buildBankInputDetails(), ], + if (habibWalletViewModel.selectedWithdrawMethod != null) ...[ + SizedBox(height: 16.h), + _buildImageUploadSection(habibWalletViewModel), + ], ], ), ), diff --git a/lib/presentation/refund_request/refund_request_create_page.dart b/lib/presentation/refund_request/refund_request_create_page.dart index afef99ae..32b96d6b 100644 --- a/lib/presentation/refund_request/refund_request_create_page.dart +++ b/lib/presentation/refund_request/refund_request_create_page.dart @@ -286,7 +286,7 @@ class _RefundRequestCreatePageState extends State { Container( padding: EdgeInsets.all(16.h), decoration: BoxDecoration( - color: AppColors.warningColorYellow.withOpacity(0.1), + color: AppColors.warningColorYellow.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12.r), border: Border.all(color: AppColors.warningColorYellow, width: 1), ), @@ -304,9 +304,38 @@ class _RefundRequestCreatePageState extends State { ), ), ], + if (vm.selectedPaymentMethod == RefundTabType.cash) ...[ + SizedBox(height: 16.h), + Container( + padding: EdgeInsets.all(16.h), + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.circular(24.r), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.alertSquare, + height: 24.h, + width: 24.h, + iconColor: AppColors.textColor, + ), + SizedBox(width: 12.w), + Expanded( + child: LocaleKeys.cashRefundApprovalInstructions.tr(context: context).toText12( + color: AppColors.greyInfoTextColor, + ), + ), + ], + ), + ), + ], if (vm.selectedPaymentMethod == RefundTabType.invoices) ...[ SizedBox(height: 16.h), _buildBankInputDetails(vm), + ], + if (vm.selectedPaymentMethod != null) ...[ SizedBox(height: 16.h), _buildImageUploadSection(vm), ], diff --git a/lib/presentation/refund_request/refund_request_detail_screen.dart b/lib/presentation/refund_request/refund_request_detail_screen.dart index 856f1394..f67a065a 100644 --- a/lib/presentation/refund_request/refund_request_detail_screen.dart +++ b/lib/presentation/refund_request/refund_request_detail_screen.dart @@ -10,9 +10,7 @@ import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/r 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/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; -import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; class RefundRequestDetailScreen extends StatelessWidget { final RefundInvoiceModel invoice; @@ -242,39 +240,65 @@ class RefundRequestDetailScreen extends StatelessWidget { ); } - - Widget _buildProcedureRow(BuildContext context, InvoiceRefundProcedure procedure) { final isApproved = procedure.isApproved ?? false; + final hasRejectionReason = procedure.rejectionReason != null && procedure.rejectionReason!.isNotEmpty; + return Padding( padding: EdgeInsets.symmetric(vertical: 6.h), - child: Row( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - flex: 3, - child: (procedure.procedureName ?? '-').toText12(color: AppColors.textColor), - ), - SizedBox(width: 8.w), - SizedBox( - width: 70.w, - child: (isApproved ? LocaleKeys.yes : LocaleKeys.no).tr(context: context).toText12( + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 3, + child: (procedure.procedureName ?? '-').toText12(color: AppColors.textColor), + ), + SizedBox(width: 8.w), + SizedBox( + width: 70.w, + child: (isApproved ? LocaleKeys.yes : LocaleKeys.no).tr(context: context).toText12( + color: AppColors.textColor, + isBold: true, + textAlignment: TextAlign.center, + ), + ), + SizedBox(width: 8.w), + SizedBox( + width: 60.w, + child: (procedure.patientPaidAmount?.toStringAsFixed(2) ?? '-').toText12( color: AppColors.textColor, isBold: true, - textAlignment: TextAlign.center, + textAlignment: TextAlign.end, ), + ), + ], ), - SizedBox(width: 8.w), - SizedBox( - width: 60.w, - child: (procedure.patientPaidAmount?.toStringAsFixed(2) ?? '-').toText12( - color: AppColors.textColor, - isBold: true, - textAlignment: TextAlign.end, + if (hasRejectionReason) ...[ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 10.h), + "${LocaleKeys.rejectionReason.tr(context: context)}:".toText10( + color: AppColors.greyInfoTextColor, + isBold: true, + ), + Row( + children: [ + Expanded( + child: procedure.rejectionReason!.toText10( + color: AppColors.greyInfoTextColor, + ), + ), + ], + ), + ], ), - ), + ], ], ), ); -- 2.30.2 From f2061de166a35fcd1e737100e68c1acbfcdda547 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Sun, 14 Jun 2026 16:18:58 +0300 Subject: [PATCH 5/9] added text changes --- assets/langs/ar-SA.json | 3 +- assets/langs/en-US.json | 3 +- lib/generated/locale_keys.g.dart | 1 + .../withdraw_request_create_page.dart | 53 ++++++++------- .../refund_request_create_page.dart | 66 ++++++++++--------- 5 files changed, 67 insertions(+), 59 deletions(-) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 04b5d9c3..09080c4f 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1907,5 +1907,6 @@ "refundCannotBeProcessedPatientAdmitted": "لا يمكن معالجة الاسترداد أثناء إقامة المريض", "cashRefundLimitExceeded": "يمكنك فقط التقدم بطلب استرداد نقدي إذا كان المبلغ أقل من 5000. لمزيد من المعلومات يرجى زيارتنا.", "cashRefundInstructions": "بمجرد الموافقة على الطلب، يرجى زيارة صراف مستشفى الحبيب الطبي أو الحبيب للرعاية الأولية لاستلام النقد. عند نافذة الصراف، سوف تتلقى رمز التحقق على رقم هاتفك المسجل. قدم الرمز للصراف للتحقق منه. سيتم صرف النقد فقط بعد التحقق الناجح.", - "cashRefundApprovalInstructions": "بمجرد الموافقة على الطلب،\nيرجى زيارة صراف مستشفى الحبيب الطبي أو الحبيب للرعاية الأولية لاستلام النقد.\nعند نافذة الصراف، سوف تتلقى رمز التحقق على رقم هاتفك المسجل. قدم الرمز للصراف للتحقق منه. سيتم صرف النقد فقط بعد التحقق الناجح." + "cashRefundApprovalInstructions": "بمجرد الموافقة على الطلب،\nيرجى زيارة صراف مستشفى الحبيب الطبي أو الحبيب للرعاية الأولية لاستلام النقد.\nعند نافذة الصراف، سوف تتلقى رمز التحقق على رقم هاتفك المسجل. قدم الرمز للصراف للتحقق منه. سيتم صرف النقد فقط بعد التحقق الناجح.", + "packageRefundValidationMessage": "طلب استرداد الباقة الخاص بك يتطلب التحقق بسبب استخدام الباقة والفواتير الفرعية ذات الصلة. سنقوم بمراجعة ومعالجة الطلب ضمن اتفاقية مستوى الخدمة المعتمدة بعد تأكيد الأهلية." } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 69c74a1b..7c745297 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1898,7 +1898,8 @@ "refundCannotBeProcessedPatientAdmitted": "Refund cannot be processed while the patient is admitted", "cashRefundLimitExceeded": "You can only apply for cash refund if the amount is less than 5000. For more information please visit us.", "cashRefundInstructions": "Once the request is approved, Please visit HMG Hospital or HMG Primary Care cashier to receive cash. At the cashier counter, you will receive an OTP on your registered mobile number. Provide the OTP to the cashier for validation. Cash will be released only after successful validation.", - "cashRefundApprovalInstructions": "Once the request is approved,\nPlease visit HMG Hospital or HMG Primary Care cashier to receive cash.\nAt the cashier counter, you will receive an OTP on your registered mobile number. Provide the OTP to the cashier for validation. Cash will be released only after successful validation." + "cashRefundApprovalInstructions": "Once the request is approved,\nPlease visit HMG Hospital or HMG Primary Care cashier to receive cash.\nAt the cashier counter, you will receive an OTP on your registered mobile number. Provide the OTP to the cashier for validation. Cash will be released only after successful validation.", + "packageRefundValidationMessage": "Your package refund request required validation because of package use and related child invoices. We will review the process the request within the approved SLA after confirming eligibility." } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 751ccb40..59535414 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1892,5 +1892,6 @@ abstract class LocaleKeys { static const cashRefundLimitExceeded = 'cashRefundLimitExceeded'; static const cashRefundInstructions = 'cashRefundInstructions'; static const cashRefundApprovalInstructions = 'cashRefundApprovalInstructions'; + static const packageRefundValidationMessage = 'packageRefundValidationMessage'; } diff --git a/lib/presentation/habib_wallet/withdraw_request_create_page.dart b/lib/presentation/habib_wallet/withdraw_request_create_page.dart index 8161cbd8..04abda32 100644 --- a/lib/presentation/habib_wallet/withdraw_request_create_page.dart +++ b/lib/presentation/habib_wallet/withdraw_request_create_page.dart @@ -256,6 +256,9 @@ class _WithdrawRequestCreatePageState extends State { physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { final type = paymentOptions[index]; + final isCashOption = type == RefundTabType.cash; + final isCashSelected = vm.selectedWithdrawMethod == RefundTabType.cash; + return Column( children: [ RadioListTileWidget( @@ -266,6 +269,29 @@ class _WithdrawRequestCreatePageState extends State { if (value != null) vm.setSelectedWithdrawMethod(value); }, ), + // Show cash instructions inside cash tile when selected + if (isCashOption && isCashSelected) ...[ + Padding( + padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 16.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.alertSquare, + height: 24.h, + width: 24.h, + iconColor: AppColors.textColor, + ), + SizedBox(width: 12.w), + Expanded( + child: LocaleKeys.cashRefundApprovalInstructions.tr(context: context).toText12( + color: AppColors.greyInfoTextColor, + ), + ), + ], + ), + ), + ], if (index != paymentOptions.length - 1) Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w), ], @@ -296,33 +322,6 @@ class _WithdrawRequestCreatePageState extends State { ), ), ], - if (vm.selectedWithdrawMethod == RefundTabType.cash) ...[ - SizedBox(height: 16.h), - Container( - padding: EdgeInsets.all(16.h), - decoration: BoxDecoration( - color: AppColors.whiteColor, - borderRadius: BorderRadius.circular(24.r), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.alertSquare, - height: 24.h, - width: 24.h, - iconColor: AppColors.textColor, - ), - SizedBox(width: 12.w), - Expanded( - child: LocaleKeys.cashRefundApprovalInstructions.tr(context: context).toText12( - color: AppColors.greyInfoTextColor, - ), - ), - ], - ), - ), - ], ], ); } diff --git a/lib/presentation/refund_request/refund_request_create_page.dart b/lib/presentation/refund_request/refund_request_create_page.dart index 32b96d6b..5c27a908 100644 --- a/lib/presentation/refund_request/refund_request_create_page.dart +++ b/lib/presentation/refund_request/refund_request_create_page.dart @@ -264,6 +264,9 @@ class _RefundRequestCreatePageState extends State { physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { final type = paymentOptions[index]; + final isCashOption = type == RefundTabType.cash; + final isCashSelected = vm.selectedPaymentMethod == RefundTabType.cash; + return Column( children: [ RadioListTileWidget( @@ -274,6 +277,29 @@ class _RefundRequestCreatePageState extends State { if (value != null) vm.setPaymentMethod(value); }, ), + // Show cash instructions inside cash tile when selected + if (isCashOption && isCashSelected) ...[ + Padding( + padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 16.h), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.buildSvgWithAssets( + icon: AppAssets.alertSquare, + height: 24.h, + width: 24.h, + iconColor: AppColors.textColor, + ), + SizedBox(width: 12.w), + Expanded( + child: LocaleKeys.cashRefundApprovalInstructions.tr(context: context).toText12( + color: AppColors.greyInfoTextColor, + ), + ), + ], + ), + ), + ], if (index != paymentOptions.length - 1) Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w), ], @@ -304,33 +330,6 @@ class _RefundRequestCreatePageState extends State { ), ), ], - if (vm.selectedPaymentMethod == RefundTabType.cash) ...[ - SizedBox(height: 16.h), - Container( - padding: EdgeInsets.all(16.h), - decoration: BoxDecoration( - color: AppColors.whiteColor, - borderRadius: BorderRadius.circular(24.r), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.alertSquare, - height: 24.h, - width: 24.h, - iconColor: AppColors.textColor, - ), - SizedBox(width: 12.w), - Expanded( - child: LocaleKeys.cashRefundApprovalInstructions.tr(context: context).toText12( - color: AppColors.greyInfoTextColor, - ), - ), - ], - ), - ), - ], if (vm.selectedPaymentMethod == RefundTabType.invoices) ...[ SizedBox(height: 16.h), _buildBankInputDetails(vm), @@ -844,6 +843,7 @@ class _RefundRequestCreatePageState extends State { void _showSuccessBottomSheet(String refNo, String refundDestination) { // refundDestination: '1' = HMG Wallet, '2' = Bank Transfer, '3' = Cash final isCashRefund = refundDestination == '3'; + final isPackageInvoice = widget.invoice.isPackageInvoice == true; showCommonBottomSheetWithoutHeight( context, @@ -865,9 +865,15 @@ class _RefundRequestCreatePageState extends State { ], ).paddingSymmetrical(24.w, 0), ], - if (isCashRefund) ...[ - SizedBox(height: 16.h), - LocaleKeys.cashRefundApprovalInstructions.tr(context: context).toText14(color: AppColors.textColor), + if (isCashRefund || isPackageInvoice) ...[ + if (isCashRefund) ...[ + SizedBox(height: 16.h), + LocaleKeys.cashRefundApprovalInstructions.tr(context: context).toText14(color: AppColors.textColor), + ], + if (isPackageInvoice) ...[ + SizedBox(height: 16.h), + LocaleKeys.packageRefundValidationMessage.tr(context: context).toText14(color: AppColors.textColor), + ], ], SizedBox(height: 24.h), Row( -- 2.30.2 From 00869beb14c4274ab0c00c9c008abe9faa1d1179 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Wed, 17 Jun 2026 10:36:20 +0300 Subject: [PATCH 6/9] merge conflicts resolved --- lib/generated/locale_keys.g.dart | 1 - pubspec.lock | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 02a260d5..7bed764b 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -1920,7 +1920,6 @@ abstract class LocaleKeys { static const refundReason6 = 'refundReason6'; static const refundReason7 = 'refundReason7'; static const selectProcedures = 'selectProcedures'; - static const selectProcedures = 'selectProcedures'; static const beforeUsingSymptomsChecker = 'beforeUsingSymptomsChecker'; static const symptomsCheckerNotDiagnosis = 'symptomsCheckerNotDiagnosis'; static const symptomsCheckerNotEmergency = 'symptomsCheckerNotEmergency'; diff --git a/pubspec.lock b/pubspec.lock index 4f2c35ac..78176753 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -499,6 +499,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_amazonpaymentservices: + dependency: "direct main" + description: + name: flutter_amazonpaymentservices + sha256: "23f45fd3b1e12a0933da3704d66cebd19c4c41e28a50d4fd458d3e3e6311f332" + url: "https://pub.dev" + source: hosted + version: "0.0.13" flutter_cache_manager: dependency: transitive description: @@ -1641,6 +1649,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + screen_brightness: + dependency: "direct main" + description: + name: screen_brightness + sha256: "7d4ac84ae26b37c01d6f5db7123a72db7933e1f2a2a8c369a51e08f81b3178d8" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + screen_brightness_android: + dependency: transitive + description: + name: screen_brightness_android + sha256: "8c69d3ac475e4d625e7fa682a3a51a69ff59abe5b4a9e57f6ec7d830a6c69bd6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + screen_brightness_ios: + dependency: transitive + description: + name: screen_brightness_ios + sha256: f08f70ca1ac3e30719764b5cfb8b3fe1e28163065018a41b3e6f243ab146c2f1 + url: "https://pub.dev" + source: hosted + version: "1.0.1" + screen_brightness_macos: + dependency: transitive + description: + name: screen_brightness_macos + sha256: "70c2efa4534e22b927e82693488f127dd4a0f008469fccf4f0eefe9061bbdd6a" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + screen_brightness_platform_interface: + dependency: transitive + description: + name: screen_brightness_platform_interface + sha256: "9f3ebf7f22d5487e7676fe9ddaf3fc55b6ff8057707cf6dc0121c7dfda346a16" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + screen_brightness_windows: + dependency: transitive + description: + name: screen_brightness_windows + sha256: c8e12a91cf6dd912a48bd41fcf749282a51afa17f536c3460d8d05702fb89ffa + url: "https://pub.dev" + source: hosted + version: "1.0.1" scrollable_positioned_list: dependency: "direct main" description: -- 2.30.2 From 10296efd12ce98ffadcb21737121ca7a28d20121 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Sun, 21 Jun 2026 15:48:54 +0300 Subject: [PATCH 7/9] added threshold API --- assets/langs/ar-SA.json | 6 +-- assets/langs/en-US.json | 5 +- lib/core/api/api_client.dart | 4 +- lib/core/api_consts.dart | 1 + .../habib_wallet/habib_wallet_view_model.dart | 45 +++++++++++++++++ .../threshold_amount_response_model.dart | 50 +++++++++++++++++++ .../refund_request/refund_request_repo.dart | 45 +++++++++++++++++ .../refund_request_view_model.dart | 45 +++++++++++++++++ .../withdraw_request_create_page.dart | 6 +++ .../refund_request_create_page.dart | 8 +++ 10 files changed, 205 insertions(+), 10 deletions(-) create mode 100644 lib/features/refund_request/models/resp_models/threshold_amount_response_model.dart diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 2ac175e9..526b9ffe 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1968,11 +1968,7 @@ "cashRefundLimitExceeded": "يمكنك فقط التقدم بطلب استرداد نقدي إذا كان المبلغ أقل من 5000. لمزيد من المعلومات يرجى زيارتنا.", "cashRefundInstructions": "بمجرد الموافقة على الطلب، يرجى زيارة صراف مستشفى الحبيب الطبي أو الحبيب للرعاية الأولية لاستلام النقد. عند نافذة الصراف، سوف تتلقى رمز التحقق على رقم هاتفك المسجل. قدم الرمز للصراف للتحقق منه. سيتم صرف النقد فقط بعد التحقق الناجح.", "cashRefundApprovalInstructions": "بمجرد الموافقة على الطلب،\nيرجى زيارة صراف مستشفى الحبيب الطبي أو الحبيب للرعاية الأولية لاستلام النقد.\nعند نافذة الصراف، سوف تتلقى رمز التحقق على رقم هاتفك المسجل. قدم الرمز للصراف للتحقق منه. سيتم صرف النقد فقط بعد التحقق الناجح.", - "packageRefundValidationMessage": "طلب استرداد الباقة الخاص بك يتطلب التحقق بسبب استخدام الباقة والفواتير الفرعية ذات الصلة. سنقوم بمراجعة ومعالجة الطلب ضمن اتفاقية مستوى الخدمة المعتمدة بعد تأكيد الأهلية." - "invoiceHistory": "سجل الفواتير", - "refund": "استرداد", - "invoiceHistory": "سجل الفواتير", - "refund": "استرداد", + "packageRefundValidationMessage": "طلب استرداد الباقة الخاص بك يتطلب التحقق بسبب استخدام الباقة والفواتير الفرعية ذات الصلة. سنقوم بمراجعة ومعالجة الطلب ضمن اتفاقية مستوى الخدمة المعتمدة بعد تأكيد الأهلية.", "parkingQR": "رمز الاستجابة السريعة لوقوف السيارات", "parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 2dcfe244..d74da82a 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1958,11 +1958,10 @@ "cashRefundLimitExceeded": "You can only apply for cash refund if the amount is less than 5000. For more information please visit us.", "cashRefundInstructions": "Once the request is approved, Please visit HMG Hospital or HMG Primary Care cashier to receive cash. At the cashier counter, you will receive an OTP on your registered mobile number. Provide the OTP to the cashier for validation. Cash will be released only after successful validation.", "cashRefundApprovalInstructions": "Once the request is approved,\nPlease visit HMG Hospital or HMG Primary Care cashier to receive cash.\nAt the cashier counter, you will receive an OTP on your registered mobile number. Provide the OTP to the cashier for validation. Cash will be released only after successful validation.", - "packageRefundValidationMessage": "Your package refund request required validation because of package use and related child invoices. We will review the process the request within the approved SLA after confirming eligibility." - - "invoiceHistory": "Invoice History", + "packageRefundValidationMessage": "Your package refund request required validation because of package use and related child invoices. We will review the process the request within the approved SLA after confirming eligibility.", "parkingQR": "Parking QR Code", "parkingQRDesc": "Scan the below QR Code at the exit parking gate:" } + diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index ed52b416..e0c3c41c 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -212,8 +212,8 @@ class ApiClientImp implements ApiClient { body['TokenID'] = "@dm!n"; // body['PatientID'] = 6424682; - // body['PatientID'] = 4770714; - body['PatientID'] = 4771727; + body['PatientID'] = 4770714; + // body['PatientID'] = 4771727; // body['PatientID'] = 89754444; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index e89841d2..9397a795 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -288,6 +288,7 @@ class ApiConsts { static String getRefundInvoices = "Services/COCWS.svc/REST/GetRefundInvoices"; static String submitRefund = "Services/COCWS.svc/REST/SubmitRefund"; static String isRefundableInvoice = "Services/Outps.svc/REST/isRefundableInvoice"; + static String getThresholdAmount = "Services/OUTPs.svc/REST/HIS_getThresholdAmount"; static String getAiAnalysisURL = "https://api.labpredict.com"; static String getAiRadiologyAnalysis = "$getAiAnalysisURL/radiology"; diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart index 2586d862..9afc65d8 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -11,6 +11,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ 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/models/resp_models/threshold_amount_response_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/refund_request_repo.dart'; import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; @@ -712,4 +713,48 @@ class HabibWalletViewModel extends ChangeNotifier { }, ); } + + // ── Threshold Amount ────────────────────────────────────────────────────── + double thresholdAmount = 5000.0; + bool isThresholdAmountLoading = false; + + Future getThresholdAmount({ + required int projectId, + Function(double)? onSuccess, + Function(String)? onError, + }) async { + isThresholdAmountLoading = true; + notifyListeners(); + + final result = await refundRequestRepo.getThresholdAmount(projectId: projectId); + + result.fold( + (failure) { + isThresholdAmountLoading = false; + thresholdAmount = 5000.0; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } + }, + (apiResponse) { + isThresholdAmountLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null && apiResponse.data!.thresholdAmountList != null) { + final thresholdItem = apiResponse.data!.thresholdAmountList!.firstWhere( + (item) => item.projectID == projectId, + orElse: () => ThresholdAmountItem(thresholdAvailableAmountByProject: 5000.0), + ); + thresholdAmount = thresholdItem.thresholdAvailableAmountByProject ?? 5000.0; + notifyListeners(); + if (onSuccess != null) onSuccess(thresholdAmount); + } else { + thresholdAmount = 5000.0; + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? "Failed to get threshold amount"); + } + } + }, + ); + } } diff --git a/lib/features/refund_request/models/resp_models/threshold_amount_response_model.dart b/lib/features/refund_request/models/resp_models/threshold_amount_response_model.dart new file mode 100644 index 00000000..698e6b01 --- /dev/null +++ b/lib/features/refund_request/models/resp_models/threshold_amount_response_model.dart @@ -0,0 +1,50 @@ +class ThresholdAmountResponseModel { + final List? thresholdAmountList; + + ThresholdAmountResponseModel({this.thresholdAmountList}); + + factory ThresholdAmountResponseModel.fromJson(Map json) { + return ThresholdAmountResponseModel( + thresholdAmountList: json['ThresholdAmountList'] != null + ? (json['ThresholdAmountList'] as List) + .map((item) => ThresholdAmountItem.fromJson(item as Map)) + .toList() + : null, + ); + } + + Map toJson() { + return { + 'ThresholdAmountList': thresholdAmountList?.map((item) => item.toJson()).toList(), + }; + } +} + +class ThresholdAmountItem { + final int? projectID; + final double? thresholdAvailableAmountByProject; + + ThresholdAmountItem({ + this.projectID, + this.thresholdAvailableAmountByProject, + }); + + factory ThresholdAmountItem.fromJson(Map json) { + return ThresholdAmountItem( + projectID: json['ProjectID'] as int?, + thresholdAvailableAmountByProject: json['ThresholdAvailableAmountByProject'] != null + ? (json['ThresholdAvailableAmountByProject'] is int + ? (json['ThresholdAvailableAmountByProject'] as int).toDouble() + : json['ThresholdAvailableAmountByProject'] as double) + : null, + ); + } + + Map toJson() { + return { + 'ProjectID': projectID, + 'ThresholdAvailableAmountByProject': thresholdAvailableAmountByProject, + }; + } +} + diff --git a/lib/features/refund_request/refund_request_repo.dart b/lib/features/refund_request/refund_request_repo.dart index eaf7d9e0..cd07a25e 100644 --- a/lib/features/refund_request/refund_request_repo.dart +++ b/lib/features/refund_request/refund_request_repo.dart @@ -7,6 +7,7 @@ import 'package:hmg_patient_app_new/features/refund_request/models/req_models/su import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/threshold_amount_response_model.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; abstract class RefundRequestRepo { @@ -33,6 +34,8 @@ abstract class RefundRequestRepo { required int invoiceNo, required int projectId, }); + + Future>> getThresholdAmount({required int projectId}); } class RefundRequestRepoImp implements RefundRequestRepo { @@ -357,4 +360,46 @@ class RefundRequestRepoImp implements RefundRequestRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> getThresholdAmount({required int projectId}) async { + final Map body = { + "ProjectID": projectId, + }; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + ApiConsts.getThresholdAmount, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final ThresholdAmountResponseModel model = ThresholdAmountResponseModel.fromJson(response); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: model, + ); + } catch (e) { + loggerService.logError("getThresholdAmount 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("getThresholdAmount 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 1b77c007..e19ee464 100644 --- a/lib/features/refund_request/refund_request_view_model.dart +++ b/lib/features/refund_request/refund_request_view_model.dart @@ -7,6 +7,7 @@ import 'package:hmg_patient_app_new/features/refund_request/models/req_models/su import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart'; import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_model.dart'; +import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/threshold_amount_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'; @@ -624,4 +625,48 @@ class RefundRequestViewModel extends ChangeNotifier { }, ); } + + // ── Threshold Amount ────────────────────────────────────────────────────── + double thresholdAmount = 5000.0; + bool isThresholdAmountLoading = false; + + Future getThresholdAmount({ + required int projectId, + Function(double)? onSuccess, + Function(String)? onError, + }) async { + isThresholdAmountLoading = true; + notifyListeners(); + + final result = await refundRequestRepo.getThresholdAmount(projectId: projectId); + + result.fold( + (failure) { + isThresholdAmountLoading = false; + thresholdAmount = 5000.0; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } + }, + (apiResponse) { + isThresholdAmountLoading = false; + if (apiResponse.messageStatus == 1 && apiResponse.data != null && apiResponse.data!.thresholdAmountList != null) { + final thresholdItem = apiResponse.data!.thresholdAmountList!.firstWhere( + (item) => item.projectID == projectId, + orElse: () => ThresholdAmountItem(thresholdAvailableAmountByProject: 5000.0), + ); + thresholdAmount = thresholdItem.thresholdAvailableAmountByProject ?? 5000.0; + notifyListeners(); + if (onSuccess != null) onSuccess(thresholdAmount); + } else { + thresholdAmount = 5000.0; + notifyListeners(); + if (onError != null) { + onError(apiResponse.errorMessage ?? "Failed to get threshold amount"); + } + } + }, + ); + } } diff --git a/lib/presentation/habib_wallet/withdraw_request_create_page.dart b/lib/presentation/habib_wallet/withdraw_request_create_page.dart index 04abda32..808fdda3 100644 --- a/lib/presentation/habib_wallet/withdraw_request_create_page.dart +++ b/lib/presentation/habib_wallet/withdraw_request_create_page.dart @@ -461,6 +461,12 @@ class _WithdrawRequestCreatePageState extends State { onHospitalSelected: (hospital) { habibWalletViewModel.setSelectedHospital(hospital); habibWalletViewModel.fetchAdvancesForHospital(); + if (hospital.iD != null) { + final projectId = hospital.iD is int ? hospital.iD as int : int.tryParse(hospital.iD.toString()); + if (projectId != null) { + habibWalletViewModel.getThresholdAmount(projectId: projectId); + } + } }, ), callBackFunc: () {}, diff --git a/lib/presentation/refund_request/refund_request_create_page.dart b/lib/presentation/refund_request/refund_request_create_page.dart index 5c27a908..5f9ddb53 100644 --- a/lib/presentation/refund_request/refund_request_create_page.dart +++ b/lib/presentation/refund_request/refund_request_create_page.dart @@ -64,6 +64,7 @@ class _RefundRequestCreatePageState extends State { _ibanController.addListener(() => _refundRequestViewModel.triggerRebuild()); _descriptionController.addListener(() => _refundRequestViewModel.triggerRebuild()); _loadInvoiceDetails(); + _loadThresholdAmount(); }); } @@ -122,6 +123,13 @@ class _RefundRequestCreatePageState extends State { ); } + void _loadThresholdAmount() { + final projectId = widget.invoice.projectId; + if (projectId == null) return; + + _refundRequestViewModel.getThresholdAmount(projectId: projectId); + } + // ── Stepper ─────────────────────────────────────────────────────────────── Widget _buildStepper(RefundRequestViewModel vm) { -- 2.30.2 From bcbdbb295d68f8f978d3e80d4b803a827fe0ba17 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Mon, 22 Jun 2026 14:34:14 +0300 Subject: [PATCH 8/9] completed the demo --- lib/core/api/api_client.dart | 6 +-- lib/core/api_consts.dart | 3 +- .../habib_wallet/habib_wallet_repo.dart | 21 +++++++-- .../habib_wallet/habib_wallet_view_model.dart | 13 ++++-- .../widgets/invoice_list_card.dart | 44 +++++++++---------- .../refund_request_create_page.dart | 2 + 6 files changed, 55 insertions(+), 34 deletions(-) diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 9e3e5621..7f6b178b 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -210,9 +210,9 @@ class ApiClientImp implements ApiClient { body['TokenID'] = "@dm!n"; } - body['TokenID'] = "@dm!n"; + // body['TokenID'] = "@dm!n"; // body['PatientID'] = 6424682; - body['PatientID'] = 4770714; + // body['PatientID'] = 4772785; // body['PatientID'] = 4771727; // body['PatientID'] = 3648831; // body['PatientID'] = 89754444; @@ -263,7 +263,7 @@ class ApiClientImp implements ApiClient { body: requestBody, headers: headers, ); - // log("response: ${response.body.toString()}"); + 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 9397a795..1542ce82 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -289,6 +289,7 @@ class ApiConsts { static String submitRefund = "Services/COCWS.svc/REST/SubmitRefund"; static String isRefundableInvoice = "Services/Outps.svc/REST/isRefundableInvoice"; static String getThresholdAmount = "Services/OUTPs.svc/REST/HIS_getThresholdAmount"; + static String checkIfPatientAdmitted = 'Services/Inps.svc/REST/checkIsPatientAdmittedOrAdmissionRequest'; static String getAiAnalysisURL = "https://api.labpredict.com"; static String getAiRadiologyAnalysis = "$getAiAnalysisURL/radiology"; @@ -922,8 +923,6 @@ var PAYFORT_PROD_URL = 'https://paymentservices.payfort.com/FortAPI/paymentApi'; // Check If InPatient API var CHECK_IF_INPATIENT = 'Services/Patients.svc/REST/GetInPatientAdmissionInfo'; -var CHECK_IF_PATIENT_ADMITTED = 'Services/Inps.svc/REST/checkIsPatientAdmittedOrAdmissionRequest'; - // Get General Instructions API var GET_GENERAL_INSTRUCTIONS = 'Services/INPs.svc/REST/getGeneralInstructions'; diff --git a/lib/features/habib_wallet/habib_wallet_repo.dart b/lib/features/habib_wallet/habib_wallet_repo.dart index b5f8d30c..c4a356bd 100644 --- a/lib/features/habib_wallet/habib_wallet_repo.dart +++ b/lib/features/habib_wallet/habib_wallet_repo.dart @@ -13,7 +13,12 @@ abstract class HabibWalletRepo { Future>>> getProjectList(); Future>> HISCreateAdvancePayment( - {required String paymentMethodName, required num paidAmount, required String paymentReference, required String patientID, required int projectID, required String depositorName}); + {required String paymentMethodName, + required num paidAmount, + required String paymentReference, + required String patientID, + required int projectID, + required String depositorName}); Future>> addAdvanceNumberRequest({required String advanceNumber, required String paymentReference}); @@ -52,7 +57,10 @@ class HabibWalletRepoImp implements HabibWalletRepo { // throw Exception("lab list is empty"); // } - final List balanceAmountList = list.map((item) => PatientAdvanceBalanceResponseModel.fromJson(item as Map)).toList().cast(); + final List balanceAmountList = list + .map((item) => PatientAdvanceBalanceResponseModel.fromJson(item as Map)) + .toList() + .cast(); for (var element in balanceAmountList) { element.totalAmount = response['TotalAdvanceBalanceAmount']; @@ -117,7 +125,12 @@ class HabibWalletRepoImp implements HabibWalletRepo { @override Future> HISCreateAdvancePayment( - {required String paymentMethodName, required num paidAmount, required String paymentReference, required String patientID, required int projectID, required String depositorName}) async { + {required String paymentMethodName, + required num paidAmount, + required String paymentReference, + required String patientID, + required int projectID, + required String depositorName}) async { Map request = { "CustName": depositorName, "CustID": patientID, @@ -319,7 +332,7 @@ class HabibWalletRepoImp implements HabibWalletRepo { GenericApiModel? apiResponse; Failure? failure; await apiClient.post( - CHECK_IF_PATIENT_ADMITTED, + ApiConsts.checkIfPatientAdmitted, body: requestBody, onFailure: (error, statusCode, {messageStatus, failureType}) { failure = failureType; diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart index 9afc65d8..23805aa3 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -557,7 +557,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), @@ -579,7 +584,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( @@ -631,7 +637,8 @@ class HabibWalletViewModel extends ChangeNotifier { lakumAccountInfo = LakumInquiryInformationResponseModel(); notifyListeners(); - final result = await habibWalletRepo.getLakumAccountInformation(identificationNumber: getIt.get().getAuthenticatedUser()!.patientIdentificationNo ?? ""); + final result = await habibWalletRepo.getLakumAccountInformation( + identificationNumber: getIt.get().getAuthenticatedUser()!.patientIdentificationNo ?? ""); result.fold( // (failure) async => await errorHandlerService.handleError(failure: failure), diff --git a/lib/presentation/my_invoices/widgets/invoice_list_card.dart b/lib/presentation/my_invoices/widgets/invoice_list_card.dart index 20e3b40b..d1a8f718 100644 --- a/lib/presentation/my_invoices/widgets/invoice_list_card.dart +++ b/lib/presentation/my_invoices/widgets/invoice_list_card.dart @@ -215,28 +215,28 @@ class InvoiceListCard extends StatelessWidget { iconSize: 14.h, ), ), - // if (getInvoicesListResponseModel.isAllowRefund == true) ...[ - // SizedBox(width: 8.w), - // Expanded( - // child: CustomButton( - // text: LocaleKeys.requestRefund.tr(context: context), - // onPressed: () { - // _handleRefundInvoiceTap(context); - // }, - // icon: AppAssets.refundIcon, - // iconColor: AppColors.caloriesCalculatorColor, - // backgroundColor: AppColors.blueColor.withValues(alpha: 0.14), - // borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), - // textColor: AppColors.caloriesCalculatorColor, - // fontSize: 14.f, - // fontWeight: FontWeight.w600, - // borderRadius: 12.r, - // padding: EdgeInsets.symmetric(horizontal: 10.w), - // height: 40.h, - // iconSize: 14.h, - // ), - // ), - // ] + if (getInvoicesListResponseModel.isAllowRefund == true) ...[ + SizedBox(width: 8.w), + Expanded( + child: CustomButton( + text: LocaleKeys.requestRefund.tr(context: context), + onPressed: () { + _handleRefundInvoiceTap(context); + }, + icon: AppAssets.refundIcon, + iconColor: AppColors.caloriesCalculatorColor, + backgroundColor: AppColors.blueColor.withValues(alpha: 0.14), + borderColor: AppColors.primaryRedColor.withValues(alpha: 0.01), + textColor: AppColors.caloriesCalculatorColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 40.h, + iconSize: 14.h, + ), + ), + ] ], ), ], diff --git a/lib/presentation/refund_request/refund_request_create_page.dart b/lib/presentation/refund_request/refund_request_create_page.dart index 5f9ddb53..b645307e 100644 --- a/lib/presentation/refund_request/refund_request_create_page.dart +++ b/lib/presentation/refund_request/refund_request_create_page.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; @@ -114,6 +115,7 @@ class _RefundRequestCreatePageState extends State { refundReason: !_canSelectIndividualProcedures ? "Inpatient/Package invoice" : null, )) .toList(); + _refundRequestViewModel.setProcedures( procedures, needsITGApproval: details.needITGApproval ?? false, -- 2.30.2 From 9a0f11b245db4978750bc58921ab69e122597028 Mon Sep 17 00:00:00 2001 From: faizatflutter Date: Tue, 23 Jun 2026 14:49:54 +0300 Subject: [PATCH 9/9] refund CR completed and demo done with QA --- assets/langs/ar-SA.json | 3 +- assets/langs/en-US.json | 3 +- .../habib_wallet/habib_wallet_view_model.dart | 4 +-- .../refund_request_list_response_model.dart | 3 ++ lib/generated/locale_keys.g.dart | 6 ++-- .../withdraw_request_create_page.dart | 2 +- .../refund_request_detail_screen.dart | 34 ++++++++++++++++++- 7 files changed, 46 insertions(+), 9 deletions(-) diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 526b9ffe..512a74f7 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1927,7 +1927,8 @@ "cash": "نقدي", "selectRefundReason": "اختر سبب الاسترداد", "refundReason": "سبب الاسترداد", - "rejectionReason": "سبب الرفض", + "requestRejectionReason": "سبب رفض الطلب", + "procedureRejectionReason": "سبب رفض الإجراء", "done": "تم", "refundReason1": "تم معالجة مريض الشركة كمدفوع ذاتي وإعادة فوترته", "refundReason2": "المريض لا يريد إجراء سريرياً", diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index d74da82a..71894964 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -1919,7 +1919,8 @@ "cash": "Cash", "selectRefundReason": "Select Refund Reason", "refundReason": "Refund Reason", - "rejectionReason": "Rejection Reason", + "requestRejectionReason": "Request Rejection Reason", + "procedureRejectionReason": "Procedure Rejection Reason", "done": "Done", "refundReason1": "Company patient treated as self-paying and re-invoiced", "refundReason2": "Patient does not want a clinical procedure", diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart index 23805aa3..514bac83 100644 --- a/lib/features/habib_wallet/habib_wallet_view_model.dart +++ b/lib/features/habib_wallet/habib_wallet_view_model.dart @@ -351,8 +351,8 @@ class HabibWalletViewModel extends ChangeNotifier { final request = SubmitRefundRequestModel( projectId: projectId, - isRefundFastTrack: true, - // in case of wallet refund, skip ITG approval + isRefundFastTrack: false, + // in case of wallet refund, skip ITG approval [need ITG Approval] refundType: '2', // wallet withdrawal refundProject: projectName, 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 aeb75f1c..bf8b2ec1 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 @@ -271,6 +271,7 @@ class RefundInvoiceModel { final String? verificationInitiatedAt; final bool? verified; final String? errorMessage; + final String? rejectReason; final WalletRequestPayload? walletRequestPayload; final InvoiceRequestPayload? invoiceRequestPayload; @@ -289,6 +290,7 @@ class RefundInvoiceModel { this.verificationInitiatedAt, this.verified, this.errorMessage, + this.rejectReason, this.walletRequestPayload, this.invoiceRequestPayload, }); @@ -309,6 +311,7 @@ class RefundInvoiceModel { verificationInitiatedAt: json['VerificationInitiatedAt'] as String?, verified: json['Verified'] as bool?, errorMessage: json['ErrorMessage'] as String?, + rejectReason: json['RejectReason'] as String?, walletRequestPayload: json['WalletRequestPayload'] != null ? WalletRequestPayload.fromJson(json['WalletRequestPayload'] as Map) : null, invoiceRequestPayload: diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 7bed764b..ac2c1ea0 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -2,7 +2,7 @@ // ignore_for_file: constant_identifier_names -abstract class LocaleKeys { +abstract class LocaleKeys { static const english = 'english'; static const arabic = 'arabic'; static const login = 'login'; @@ -1911,7 +1911,8 @@ abstract class LocaleKeys { static const bankTransfer = 'bankTransfer'; static const selectRefundReason = 'selectRefundReason'; static const refundReason = 'refundReason'; - static const rejectionReason = 'rejectionReason'; + static const requestRejectionReason = 'requestRejectionReason'; + static const procedureRejectionReason = 'procedureRejectionReason'; static const refundReason1 = 'refundReason1'; static const refundReason2 = 'refundReason2'; static const refundReason3 = 'refundReason3'; @@ -1952,5 +1953,4 @@ abstract class LocaleKeys { static const packageRefundValidationMessage = 'packageRefundValidationMessage'; static const parkingQR = 'parkingQR'; static const parkingQRDesc = 'parkingQRDesc'; - } diff --git a/lib/presentation/habib_wallet/withdraw_request_create_page.dart b/lib/presentation/habib_wallet/withdraw_request_create_page.dart index 808fdda3..b8ace1a9 100644 --- a/lib/presentation/habib_wallet/withdraw_request_create_page.dart +++ b/lib/presentation/habib_wallet/withdraw_request_create_page.dart @@ -809,7 +809,7 @@ class _WithdrawRequestCreatePageState extends State { Navigator.of(context).push( CustomPageRoute( page: RefundHistoryScreen( - refundType: 1, // 2 for wallet, 1 for invoice + refundType: 2, // 2 for wallet, 1 for invoice ), ), ); diff --git a/lib/presentation/refund_request/refund_request_detail_screen.dart b/lib/presentation/refund_request/refund_request_detail_screen.dart index f67a065a..a163456d 100644 --- a/lib/presentation/refund_request/refund_request_detail_screen.dart +++ b/lib/presentation/refund_request/refund_request_detail_screen.dart @@ -215,6 +215,22 @@ class RefundRequestDetailScreen extends StatelessWidget { SizedBox(height: 16.h), if (invoicePayload.proceduresList != null && invoicePayload.proceduresList!.isNotEmpty) ...invoicePayload.proceduresList!.map((p) => _buildProcedureRow(context, p)), + if (invoice?.rejectReason != null && invoice!.rejectReason!.isNotEmpty) ...[ + SizedBox(height: 16.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "${LocaleKeys.requestRejectionReason.tr(context: context)}:".toText12( + color: AppColors.greyInfoTextColor, + isBold: true, + ), + SizedBox(height: 4.h), + invoice.rejectReason!.toText12( + color: AppColors.greyInfoTextColor, + ), + ], + ), + ], ], if (isWalletType && walletPayload != null) ...[ Row( @@ -233,6 +249,22 @@ class RefundRequestDetailScreen extends StatelessWidget { SizedBox(height: 16.h), if (walletPayload.advancesList != null && walletPayload.advancesList!.isNotEmpty) ...walletPayload.advancesList!.map((a) => _buildAdvanceRow(context, a)), + if (invoice?.rejectReason != null && invoice!.rejectReason!.isNotEmpty) ...[ + SizedBox(height: 16.h), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "${LocaleKeys.requestRejectionReason.tr(context: context)}:".toText12( + color: AppColors.greyInfoTextColor, + isBold: true, + ), + SizedBox(height: 4.h), + invoice!.rejectReason!.toText12( + color: AppColors.greyInfoTextColor, + ), + ], + ), + ], ], ], ), @@ -283,7 +315,7 @@ class RefundRequestDetailScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 10.h), - "${LocaleKeys.rejectionReason.tr(context: context)}:".toText10( + "${LocaleKeys.procedureRejectionReason.tr(context: context)}:".toText10( color: AppColors.greyInfoTextColor, isBold: true, ), -- 2.30.2