Refund CR Complete with Demop (faiz_dev) #335

Open
FaizHashmiCS22 wants to merge 12 commits from faiz_dev into master

@ -0,0 +1,4 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="10" fill="#EFEFF0"/>
<path d="M15.0826 10.25C16.112 10.2496 16.8237 10.2493 17.4626 10.4888C18.8581 11.0121 19.4704 12.2513 19.9425 13.2064C20.0838 13.4892 20.3409 14.0028 20.4128 14.1157C20.4348 14.1526 20.5044 14.2282 20.6064 14.2354C20.7396 14.2492 20.92 14.2501 21.2361 14.2501L24.7905 14.2501H24.7905C25.8095 14.2501 26.6312 14.2501 27.2905 14.3171C27.9711 14.3864 28.5613 14.5332 29.0834 14.8821C29.4929 15.1557 29.8444 15.5072 30.118 15.9167C30.4669 16.4388 30.6137 17.029 30.683 17.7096C30.75 18.3689 30.75 19.2733 30.75 20.2923C30.75 22.0121 30.75 23.3603 30.6408 24.4337C30.5295 25.5284 30.2983 26.4203 29.781 27.1946C29.3614 27.8225 28.8224 28.3615 28.1945 28.781C27.4202 29.2984 26.5283 29.5295 25.4336 29.6409C24.3602 29.7501 23.012 29.7501 21.2923 29.7501H19.9426C17.6342 29.7501 15.8252 29.7501 14.4137 29.5603C12.969 29.3661 11.8289 28.9608 10.9341 28.0659C10.0393 27.1711 9.63399 26.0311 9.43975 24.5864C9.24998 23.1749 9.24999 21.3659 9.25 19.0575V19.0575V15.9096V15.9096C9.24999 15.0308 9.24998 14.3221 9.30044 13.7498C9.35242 13.16 9.46238 12.644 9.7254 12.1727C10.0643 11.5654 10.5653 11.0644 11.1726 10.7255C11.6439 10.4625 12.16 10.3525 12.7497 10.3005C13.322 10.2501 14.2039 10.25 15.0826 10.25Z" fill="#8F9AA3"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -1924,8 +1924,11 @@
"selectRefundMethod": "اختر طريقة الاسترداد",
"hmgWallet": "محفظة HMG",
"bankTransfer": "تحويل بنكي",
"cash": "نقدي",
"selectRefundReason": "اختر سبب الاسترداد",
"refundReason": "سبب الاسترداد",
"requestRejectionReason": "سبب رفض الطلب",
"procedureRejectionReason": "سبب رفض الإجراء",
"done": "تم",
"refundReason1": "تم معالجة مريض الشركة كمدفوع ذاتي وإعادة فوترته",
"refundReason2": "المريض لا يريد إجراء سريرياً",
@ -1948,15 +1951,25 @@
"earned": "حصل",
"getParkingQR": "الحصول على رمز الاستجابة السريعة لوقوف السيارات",
"cannotOpenThisFile": "لا يمكن فتح هذا الملف",
"thisInvoiceIsNotEligibleForRefund": "هذه الفاتورة غير مؤهلة للاسترداد",
"refundDetails": "تفاصيل الاسترداد",
"refund": "استرداد",
"requestNo": "رقم الطلب.",
"referenceNo": "رقم المرجع",
"advanceNumber": "رقم الدفعة المقدمة",
"amountOnly": "المبلغ",
"invoiceHistory": "سجل الفواتير",
"refund": "استرداد",
"invoiceHistory": "سجل الفواتير",
"refund": "استرداد",
"selectAdvance": "اختر الدفعة المقدمة",
"advanceNo": "رقم الدفعة",
"noAdvancesAvailable": "لا توجد دفعات متاحة للاسترداد",
"maximumFilesReached": "الحد الأقصى 4 ملفات مسموح به",
"fileSizeTooLarge": "يجب ألا يتجاوز حجم الملف 2 ميغابايت",
"image": "صورة",
"refundCannotBeProcessedPatientAdmitted": "لا يمكن معالجة الاسترداد أثناء إقامة المريض",
"cashRefundLimitExceeded": "يمكنك فقط التقدم بطلب استرداد نقدي إذا كان المبلغ أقل من 5000. لمزيد من المعلومات يرجى زيارتنا.",
"cashRefundInstructions": "بمجرد الموافقة على الطلب، يرجى زيارة صراف مستشفى الحبيب الطبي أو الحبيب للرعاية الأولية لاستلام النقد. عند نافذة الصراف، سوف تتلقى رمز التحقق على رقم هاتفك المسجل. قدم الرمز للصراف للتحقق منه. سيتم صرف النقد فقط بعد التحقق الناجح.",
"cashRefundApprovalInstructions": "بمجرد الموافقة على الطلب،\nيرجى زيارة صراف مستشفى الحبيب الطبي أو الحبيب للرعاية الأولية لاستلام النقد.\nعند نافذة الصراف، سوف تتلقى رمز التحقق على رقم هاتفك المسجل. قدم الرمز للصراف للتحقق منه. سيتم صرف النقد فقط بعد التحقق الناجح.",
"packageRefundValidationMessage": "طلب استرداد الباقة الخاص بك يتطلب التحقق بسبب استخدام الباقة والفواتير الفرعية ذات الصلة. سنقوم بمراجعة ومعالجة الطلب ضمن اتفاقية مستوى الخدمة المعتمدة بعد تأكيد الأهلية.",
"parkingQR": "رمز الاستجابة السريعة لوقوف السيارات",
"parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:"
}

@ -1916,8 +1916,11 @@
"selectRefundMethod": "Select Refund Method",
"hmgWallet": "HMG Wallet",
"bankTransfer": "Bank Transfer",
"cash": "Cash",
"selectRefundReason": "Select Refund Reason",
"refundReason": "Refund 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",
@ -1946,8 +1949,20 @@
"advanceNumber": "Advance Number",
"amountOnly": "Amount",
"invoiceHistory": "Invoice History",
"selectAdvance": "Select Advance",
"advanceNo": "Advance No",
"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.",
"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:"
}

@ -208,10 +208,13 @@ class ApiClientImp implements ApiClient {
if (url == 'https://uat.hmgwebservices.com/Services/NHIC.svc/REST/GetPatientInfo') {
url = "https://hmgwebservices.com/Services/NHIC.svc/REST/GetPatientInfo";
body['TokenID'] = "@dm!n";
}
}
// body['TokenID'] = "@dm!n";
// body['PatientID'] = 4772993;
// body['PatientID'] = 6424682;
// body['PatientID'] = 4772785;
// body['PatientID'] = 4771727;
// body['PatientID'] = 3648831;
// body['PatientID'] = 89754444;
// body['PatientTypeID'] = 1;
// body['PatientOutSA'] = 0;

@ -288,6 +288,8 @@ 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 checkIfPatientAdmitted = 'Services/Inps.svc/REST/checkIsPatientAdmittedOrAdmissionRequest';
static String getAiAnalysisURL = "https://api.labpredict.com";
static String getAiRadiologyAnalysis = "$getAiAnalysisURL/radiology";
@ -303,28 +305,28 @@ class ApiConsts {
// ************ static values for Api ****************
static final double appVersionID = 21.3;
// 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/';
@ -339,10 +341,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/';
@ -357,28 +357,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';
@ -921,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';
@ -1012,11 +1012,11 @@ 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';
const GET_APPOINTMENT_PARKING_QR = 'Services/outps.svc/rest/pms_getParkingTicketByAppointmentNo';

@ -378,6 +378,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';

@ -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();
}
@ -63,7 +64,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;
@ -75,10 +77,10 @@ class AppState {
FutureOr<void> checkIfGMSAvailableOrNot() async {
isGMSAvailable = (await GmsCheck().checkGmsAvailability()) ?? true;
if(Platform.isIOS) {
isGMSAvailable = true;
isAndroid = false;
return;
if (Platform.isIOS) {
isGMSAvailable = true;
isAndroid = false;
return;
}
}
@ -111,16 +113,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');
}
}
@ -128,14 +130,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;
@ -170,7 +172,6 @@ class AppState {
set setIsAuthenticated(v) => isAuthenticated = v;
String deviceTypeID = "";
set setDeviceTypeID(v) => deviceTypeID = v;
@ -226,7 +227,7 @@ class AppState {
userLat = 0.0;
}
setRatedVisible(bool value) {
setRatedVisible(bool value) {
isRatedVisible = value;
}
@ -281,7 +282,6 @@ class AppState {
}
}
dynamic getUserSwitchPayload({required String userType}) {
return _userSwitchPayloads[userType];
}
@ -307,11 +307,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;

@ -116,7 +116,7 @@ class BloodDonationRepoImp implements BloodDonationRepo {
GenericApiModel<List<HospitalsModel>>? apiResponse;
Failure? failure;
await apiClient.post(
GET_PROJECT_LIST,
ApiConsts.getProjectsList,
body: request,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;

@ -271,7 +271,7 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo {
GenericApiModel<DoctorsProfileResponseModel>? 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<List<HospitalsModel>>? apiResponse;
Failure? failure;
await apiClient.post(
GET_PROJECT_LIST,
ApiConsts.getProjectsList,
body: request,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;

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

@ -189,7 +189,7 @@ class EmergencyServicesRepoImp implements EmergencyServicesRepo {
GenericApiModel<List<HospitalsModel>>? apiResponse;
Failure? failure;
await apiClient.post(
GET_PROJECT_LIST,
ApiConsts.getProjectsList,
body: request,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;

@ -13,7 +13,12 @@ abstract class HabibWalletRepo {
Future<Either<Failure, GenericApiModel<List<HospitalsModel>>>> getProjectList();
Future<Either<Failure, GenericApiModel<dynamic>>> 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<Either<Failure, GenericApiModel<dynamic>>> addAdvanceNumberRequest({required String advanceNumber, required String paymentReference});
@ -22,6 +27,8 @@ abstract class HabibWalletRepo {
Future<Either<Failure, GenericApiModel<dynamic>>> getLakumAccountInformation({required String identificationNumber});
Future<Either<Failure, GenericApiModel<dynamic>>> getLakumAccountInquiryInformation({required String lakumAccountNumber});
Future<Either<Failure, GenericApiModel<dynamic>>> checkIsPatientAdmittedOrAdmissionRequest({required bool isActiveAppointment});
}
class HabibWalletRepoImp implements HabibWalletRepo {
@ -50,7 +57,10 @@ class HabibWalletRepoImp implements HabibWalletRepo {
// throw Exception("lab list is empty");
// }
final List<PatientAdvanceBalanceResponseModel> balanceAmountList = list.map((item) => PatientAdvanceBalanceResponseModel.fromJson(item as Map<String, dynamic>)).toList().cast<PatientAdvanceBalanceResponseModel>();
final List<PatientAdvanceBalanceResponseModel> balanceAmountList = list
.map((item) => PatientAdvanceBalanceResponseModel.fromJson(item as Map<String, dynamic>))
.toList()
.cast<PatientAdvanceBalanceResponseModel>();
for (var element in balanceAmountList) {
element.totalAmount = response['TotalAdvanceBalanceAmount'];
@ -83,7 +93,7 @@ class HabibWalletRepoImp implements HabibWalletRepo {
GenericApiModel<List<HospitalsModel>>? apiResponse;
Failure? failure;
await apiClient.post(
GET_PROJECT_LIST,
ApiConsts.getProjectsList,
body: request,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
@ -115,7 +125,12 @@ class HabibWalletRepoImp implements HabibWalletRepo {
@override
Future<Either<Failure, GenericApiModel>> 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<String, dynamic> request = {
"CustName": depositorName,
"CustID": patientID,
@ -306,4 +321,40 @@ class HabibWalletRepoImp implements HabibWalletRepo {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<dynamic>>> checkIsPatientAdmittedOrAdmissionRequest({required bool isActiveAppointment}) async {
Map<String, dynamic> requestBody = {
"IsActiveAppointment": isActiveAppointment,
};
try {
GenericApiModel<dynamic>? apiResponse;
Failure? failure;
await apiClient.post(
ApiConsts.checkIfPatientAdmitted,
body: requestBody,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
apiResponse = GenericApiModel<dynamic>(
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()));
}
}
}

@ -11,7 +11,9 @@ 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';
import 'package:hmg_patient_app_new/services/error_handler_service.dart';
@ -40,6 +42,41 @@ class HabibWalletViewModel extends ChangeNotifier {
List<RefundableAdvancesItem> refundableAdvancesList = [];
bool isSubmittingWithdrawal = false;
// 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) {
currentWithdrawStep++;
notifyListeners();
}
}
void previousWithdrawStep() {
if (currentWithdrawStep > 0) {
currentWithdrawStep--;
notifyListeners();
}
}
void setSelectedAdvance(RefundableAdvancesItem? advance) {
selectedAdvance = advance;
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;
@ -177,6 +214,43 @@ class HabibWalletViewModel extends ChangeNotifier {
return isValid;
}
// ==================== WITHDRAW ATTACHMENTS ====================================
List<String> 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
@ -256,26 +330,29 @@ 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 {
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 ?? "",
)
]
: <SubmitRefundAdvanceItem>[];
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,
@ -290,13 +367,14 @@ class HabibWalletViewModel extends ChangeNotifier {
refundDoctorName: '',
refundClinicId: 0,
refundClinicName: '',
refundDestination: '2',
// only bank transfer
refundDestination: refundDestination,
// Use provided refund destination
refundIban: iban,
refundRemarks: remarks,
refundTotalAmount: totalAdvancesBalance,
refundTotalAmount: selectedAdvanceBalance,
refundProceduresList: const [],
refundAdvancesList: advances,
refundAttachmentsBase64List: withdrawAttachmentList,
);
final result = await refundRequestRepo.submitRefund(request: request);
@ -365,6 +443,11 @@ class HabibWalletViewModel extends ChangeNotifier {
isSubmittingWithdrawal = false;
_ibanError = null;
_withdrawDescriptionError = null;
_withdrawAttachmentError = null;
withdrawAttachmentList.clear();
currentWithdrawStep = 0;
selectedAdvance = null;
selectedWithdrawMethod = null;
if (!silent) notifyListeners();
}
@ -474,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),
@ -496,7 +584,8 @@ class HabibWalletViewModel extends ChangeNotifier {
);
}
Future<void> addAdvanceNumberRequest({required String advanceNumber, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async {
Future<void> addAdvanceNumberRequest(
{required String advanceNumber, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await habibWalletRepo.addAdvanceNumberRequest(advanceNumber: advanceNumber, paymentReference: paymentReference);
result.fold(
@ -548,7 +637,8 @@ class HabibWalletViewModel extends ChangeNotifier {
lakumAccountInfo = LakumInquiryInformationResponseModel();
notifyListeners();
final result = await habibWalletRepo.getLakumAccountInformation(identificationNumber: getIt.get<AppState>().getAuthenticatedUser()!.patientIdentificationNo ?? "");
final result = await habibWalletRepo.getLakumAccountInformation(
identificationNumber: getIt.get<AppState>().getAuthenticatedUser()!.patientIdentificationNo ?? "");
result.fold(
// (failure) async => await errorHandlerService.handleError(failure: failure),
@ -589,4 +679,89 @@ class HabibWalletViewModel extends ChangeNotifier {
},
);
}
// Check Is Patient Admitted Or Admission Request
bool isCheckingPatientAdmitted = false;
Future<void> 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());
}
}
},
);
}
// Threshold Amount
double thresholdAmount = 5000.0;
bool isThresholdAmountLoading = false;
Future<void> 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");
}
}
},
);
}
}

@ -525,7 +525,7 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo {
GenericApiModel<List<PatientAppointmentHistoryResponseModel>>? apiResponse;
Failure? failure;
await apiClient.post(
GET_MY_DOCTOR,
ApiConsts.getMyDoctor,
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;

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

@ -199,7 +199,7 @@ class MyInvoicesRepoImp implements MyInvoicesRepo {
GenericApiModel<dynamic>? 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<dynamic>? apiResponse;
Failure? failure;
await apiClient.post(
DOWNLOAD_PHARMACY_INVOICE_PDF,
downloadPharmacyInvoicePdf,
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;

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

@ -79,6 +79,9 @@ class SubmitRefundRequestModel {
/// Populated only for wallet withdrawal refunds (refundType == '2').
final List<SubmitRefundAdvanceItem> refundAdvancesList;
/// List of base64 encoded images for refund attachments (max 4 files, each max 2MB)
final List<String> 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') {

@ -142,6 +142,8 @@ class InvoiceRefundProcedure {
final String? procedureId;
final String? procedureName;
final String? refundReason;
final String? rejectionReason;
final bool? isApproved;
InvoiceRefundProcedure({
this.isLocked,
@ -150,6 +152,8 @@ class InvoiceRefundProcedure {
this.procedureId,
this.procedureName,
this.refundReason,
this.rejectionReason,
this.isApproved,
});
factory InvoiceRefundProcedure.fromJson(Map<String, dynamic> json) {
@ -160,6 +164,8 @@ 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?,
);
}
}
@ -265,6 +271,7 @@ class RefundInvoiceModel {
final String? verificationInitiatedAt;
final bool? verified;
final String? errorMessage;
final String? rejectReason;
final WalletRequestPayload? walletRequestPayload;
final InvoiceRequestPayload? invoiceRequestPayload;
@ -283,6 +290,7 @@ class RefundInvoiceModel {
this.verificationInitiatedAt,
this.verified,
this.errorMessage,
this.rejectReason,
this.walletRequestPayload,
this.invoiceRequestPayload,
});
@ -303,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<String, dynamic>) : null,
invoiceRequestPayload:

@ -0,0 +1,50 @@
class ThresholdAmountResponseModel {
final List<ThresholdAmountItem>? thresholdAmountList;
ThresholdAmountResponseModel({this.thresholdAmountList});
factory ThresholdAmountResponseModel.fromJson(Map<String, dynamic> json) {
return ThresholdAmountResponseModel(
thresholdAmountList: json['ThresholdAmountList'] != null
? (json['ThresholdAmountList'] as List)
.map((item) => ThresholdAmountItem.fromJson(item as Map<String, dynamic>))
.toList()
: null,
);
}
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> toJson() {
return {
'ProjectID': projectID,
'ThresholdAvailableAmountByProject': thresholdAvailableAmountByProject,
};
}
}

@ -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<Either<Failure, GenericApiModel<ThresholdAmountResponseModel>>> getThresholdAmount({required int projectId});
}
class RefundRequestRepoImp implements RefundRequestRepo {
@ -357,4 +360,46 @@ class RefundRequestRepoImp implements RefundRequestRepo {
return Left(UnknownFailure(e.toString()));
}
}
@override
Future<Either<Failure, GenericApiModel<ThresholdAmountResponseModel>>> getThresholdAmount({required int projectId}) async {
final Map<String, dynamic> body = {
"ProjectID": projectId,
};
try {
GenericApiModel<ThresholdAmountResponseModel>? 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<ThresholdAmountResponseModel>(
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()));
}
}
}

@ -7,11 +7,12 @@ 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';
enum RefundTabType { invoices, hmgWallet }
enum RefundTabType { invoices, hmgWallet, cash }
extension RefundTabTypeExtension on RefundTabType {
String toDisplayString() {
@ -20,6 +21,8 @@ extension RefundTabTypeExtension on RefundTabType {
return LocaleKeys.hmgWallet.tr();
case RefundTabType.invoices:
return LocaleKeys.bankTransfer.tr();
case RefundTabType.cash:
return LocaleKeys.cash.tr();
}
}
}
@ -109,6 +112,7 @@ class RefundRequestViewModel extends ChangeNotifier {
currentStep = 0;
procedures.clear();
needITGApproval = false;
isWholeInvoiceRefund = false;
selectedPaymentMethod = null;
selectedRefundReason = null;
isProceduresLoading = false;
@ -116,6 +120,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();
@ -208,15 +214,20 @@ class RefundRequestViewModel extends ChangeNotifier {
List<RefundableProcedure> procedures = [];
bool isProceduresLoading = false;
bool needITGApproval = false;
bool isWholeInvoiceRefund = false; // For Inpatient/Package invoices
void setProcedures(List<RefundableProcedure> list, {bool needsITGApproval = false}) {
void setProcedures(List<RefundableProcedure> 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 +240,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;
@ -296,6 +310,47 @@ class RefundRequestViewModel extends ChangeNotifier {
return isValid;
}
// Attachments
List<String> 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;
@ -570,4 +625,48 @@ class RefundRequestViewModel extends ChangeNotifier {
},
);
}
// Threshold Amount
double thresholdAmount = 5000.0;
bool isThresholdAmountLoading = false;
Future<void> 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");
}
}
},
);
}
}

@ -25,7 +25,7 @@ class WeatherRepoImp implements WeatherRepo {
GenericApiModel<List<GetCityInfoList>>? apiResponse;
Failure? failure;
await apiClient.post(
WEATHER_INDICATOR,
ApiConsts.weatherIndicator,
body: request,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;

@ -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,6 +1911,8 @@ abstract class LocaleKeys {
static const bankTransfer = 'bankTransfer';
static const selectRefundReason = 'selectRefundReason';
static const refundReason = 'refundReason';
static const requestRejectionReason = 'requestRejectionReason';
static const procedureRejectionReason = 'procedureRejectionReason';
static const refundReason1 = 'refundReason1';
static const refundReason2 = 'refundReason2';
static const refundReason3 = 'refundReason3';
@ -1932,13 +1934,23 @@ abstract class LocaleKeys {
static const getParkingQR = 'getParkingQR';
static const cannotOpenThisFile = 'cannotOpenThisFile';
static const refundDetails = 'refundDetails';
static const refund = 'refund';
static const requestNo = 'requestNo';
static const referenceNo = 'referenceNo';
static const advanceNumber = 'advanceNumber';
static const amountOnly = 'amountOnly';
static const invoiceHistory = 'invoiceHistory';
static const refund = 'refund';
static const selectAdvance = 'selectAdvance';
static const advanceNo = 'advanceNo';
static const noAdvancesAvailable = 'noAdvancesAvailable';
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';
static const packageRefundValidationMessage = 'packageRefundValidationMessage';
static const parkingQR = 'parkingQR';
static const parkingQRDesc = 'parkingQRDesc';
}

@ -355,8 +355,7 @@ class FeedbackPage extends StatelessWidget {
context,
true,
(String image, file) {
print(image);
print(file);
// Navigator.pop(context);
contactUsViewModel.addFeedbackAttachment(image);
},

@ -217,6 +217,7 @@ class _OtherDetailsStepState extends State<OtherDetailsStep> {
(String image, File file) {
_addInsuranceDocument(image, file, formManager);
},
allowMultiple: true, // Enable multiple selection
);
},
),

@ -136,23 +136,58 @@ class _HabibWalletState extends State<HabibWalletPage> {
},
),
),
// SizedBox(width: 8.w),
// Flexible(
// child: CustomButton(
// height: 40.h,
// icon: AppAssets.refundIcon,
// iconSize: 24.w,
// backgroundColor: AppColors.successColor,
// textColor: Colors.white,
// text: LocaleKeys.withdraw.tr(context: context),
// borderWidth: 0.w,
// fontWeight: FontWeight.w600,
// borderColor: Colors.transparent,
// padding: EdgeInsets.fromLTRB(4, 0, 12, 0),
// fontSize: 14.f,
// onPressed: () => Navigator.of(context).push(CustomPageRoute(page: WithdrawRequestCreatePage())),
// ),
// ),
SizedBox(width: 8.w),
Flexible(
child: CustomButton(
height: 40.h,
icon: AppAssets.refundIcon,
iconSize: 24.w,
backgroundColor: AppColors.successColor,
textColor: Colors.white,
text: LocaleKeys.withdraw.tr(context: context),
borderWidth: 0.w,
fontWeight: FontWeight.w600,
borderColor: Colors.transparent,
padding: EdgeInsets.fromLTRB(4, 0, 12, 0),
fontSize: 14.f,
onPressed: () 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()));
},
);
},
),
),
],
),
SizedBox(height: 24.h),
@ -307,5 +342,3 @@ class _HabibWalletState extends State<HabibWalletPage> {
);
}
}

@ -2,14 +2,19 @@ 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/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';
@ -20,7 +25,9 @@ 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';
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 +52,8 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
_appState = getIt.get<AppState>();
_habibWalletViewModel = context.read<HabibWalletViewModel>();
WidgetsBinding.instance.addPostFrameCallback((_) {
_habibWalletViewModel.currentWithdrawStep = 0;
_habibWalletViewModel.selectedAdvance = null;
_habibWalletViewModel.getProjectsList();
});
}
@ -59,12 +68,262 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
super.dispose();
}
bool get _isFormValid {
final vm = _habibWalletViewModel;
return vm.selectedHospital != null &&
!vm.isAdvancesLoading &&
_bankDetailsController.text.trim().isNotEmpty &&
_descriptionController.text.trim().isNotEmpty;
// 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<HabibWalletViewModel>(
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<int>(
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 Refund Method Selection
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];
final isCashOption = type == RefundTabType.cash;
final isCashSelected = vm.selectedWithdrawMethod == RefundTabType.cash;
return Column(
children: [
RadioListTileWidget<RefundTabType>(
value: type,
groupValue: vm.selectedWithdrawMethod,
title: type.toDisplayString(),
onChanged: (value) {
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),
],
);
},
),
),
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
@ -72,10 +331,23 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
Widget _buildBottomBar() {
return Consumer<HabibWalletViewModel>(
builder: (context, vm, _) {
final isValid = vm.selectedHospital != null &&
!vm.isAdvancesLoading &&
_bankDetailsController.text.trim().isNotEmpty &&
_descriptionController.text.trim().isNotEmpty;
final isStep2 = vm.currentWithdrawStep == 1;
// Step 1 validation: advance selected
final isStep1Valid = vm.selectedAdvance != null;
// 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(
@ -86,15 +358,47 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (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,
icon: isStep2 ? AppAssets.refundIcon : null,
text: isStep2 ? LocaleKeys.submit.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) {
if (!isStep2) {
// Move to step 2
if (vm.selectedAdvance != null) {
vm.nextWithdrawStep();
}
} else {
// 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);
}
},
@ -108,6 +412,8 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
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 +443,46 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
]),
],
),
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();
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: () {},
);
}
: () {}, // 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,42 +561,157 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
);
}
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,
// Image Upload Section
Widget _buildImageUploadSection(HabibWalletViewModel vm) {
return Consumer<HabibWalletViewModel>(
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);
}),
],
);
},
),
),
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) {},
],
);
}),
},
);
}
@ -310,6 +732,11 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
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(),
@ -319,9 +746,10 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
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();
@ -340,7 +768,10 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
);
}
void _showWithdrawalSuccessBottomSheet(String refNo) {
void _showWithdrawalSuccessBottomSheet(String refNo, String refundDestination) {
// refundDestination: '2' = Bank Transfer, '3' = Cash
final isCashRefund = refundDestination == '3';
showCommonBottomSheetWithoutHeight(
context,
child: Padding(
@ -359,7 +790,11 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
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(
@ -374,7 +809,7 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
Navigator.of(context).push(
CustomPageRoute(
page: RefundHistoryScreen(
refundType: 1, // 2 for wallet, 1 for invoice
refundType: 2, // 2 for wallet, 1 for invoice
),
),
);
@ -407,6 +842,13 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
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 +857,66 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
),
),
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<Offset>(
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: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildRefundMethodSelection(habibWalletViewModel),
if (habibWalletViewModel.selectedWithdrawMethod == RefundTabType.invoices) ...[
SizedBox(height: 16.h),
_buildBankInputDetails(),
],
if (habibWalletViewModel.selectedWithdrawMethod != null) ...[
SizedBox(height: 16.h),
_buildImageUploadSection(habibWalletViewModel),
],
],
),
),
),
),
],
),
),
@ -439,7 +930,3 @@ class _WithdrawRequestCreatePageState extends State<WithdrawRequestCreatePage> {
);
}
}
// 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}

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

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

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

@ -89,11 +89,14 @@ class InvoiceListCard extends StatelessWidget {
labelText: LocaleKeys.walkin.tr(),
textColor: AppColors.textColor,
),
AppCustomChipWidget(
labelText: LocaleKeys.outPatient.tr(context: context),
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),
@ -212,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,
),
),
]
],
),
],

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

@ -1,3 +1,6 @@
import 'dart:developer';
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 +23,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';
@ -61,6 +65,7 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
_ibanController.addListener(() => _refundRequestViewModel.triggerRebuild());
_descriptionController.addListener(() => _refundRequestViewModel.triggerRebuild());
_loadInvoiceDetails();
_loadThresholdAmount();
});
}
@ -74,6 +79,20 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
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,13 +109,29 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
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,
);
},
);
}
void _loadThresholdAmount() {
final projectId = widget.invoice.projectId;
if (projectId == null) return;
_refundRequestViewModel.getThresholdAmount(projectId: projectId);
}
// Stepper
Widget _buildStepper(RefundRequestViewModel vm) {
@ -123,12 +158,11 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
);
}
// Step 1 exactly mirrors RechargeWalletPage hospital row
// Step 1 Select Procedures (or whole invoice for Inpatient/Package)
Widget _buildStep1Content(RefundRequestViewModel vm) {
return Consumer<RefundRequestViewModel>(
builder: (context, refundVM, _) {
final isArabic = _appState.isArabic();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -164,39 +198,41 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
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),
@ -217,6 +253,7 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
final paymentOptions = [
RefundTabType.hmgWallet,
RefundTabType.invoices,
RefundTabType.cash,
];
return Column(
@ -237,6 +274,9 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
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<RefundTabType>(
@ -247,6 +287,29 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
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),
],
@ -254,14 +317,193 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
},
),
),
if (vm.selectedPaymentMethod == RefundTabType.cash && vm.isCashRefundAmountExceeded) ...[
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,
),
),
],
),
),
],
if (vm.selectedPaymentMethod == RefundTabType.invoices) ...[
SizedBox(height: 16.h),
_buildBankInputDetails(vm),
],
if (vm.selectedPaymentMethod != null) ...[
SizedBox(height: 16.h),
_buildImageUploadSection(vm),
],
],
);
}
Widget _buildImageUploadSection(RefundRequestViewModel vm) {
return Consumer<RefundRequestViewModel>(
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) {
@ -331,11 +573,12 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
);
}
// 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 +592,88 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
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),
]
],
),
),
),
),
@ -491,8 +741,12 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
isDisabled: isStep2
? (refundRequestViewModel.selectedPaymentMethod == null ||
(refundRequestViewModel.selectedPaymentMethod == RefundTabType.invoices &&
(_ibanController.text.trim().isEmpty || _descriptionController.text.trim().isEmpty)))
: (refundRequestViewModel.selectedProcedures.isEmpty || !refundRequestViewModel.allSelectedProceduresHaveReasons),
(_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
(_canSelectIndividualProcedures && !refundRequestViewModel.allSelectedProceduresHaveReasons)),
).paddingSymmetrical(24.h, 24.h),
],
),
@ -523,12 +777,24 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
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',
@ -545,10 +811,11 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
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,
@ -564,7 +831,7 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
request: request,
onSuccess: (response) {
LoaderBottomSheet.hideLoader();
_showSuccessBottomSheet(response.refNo ?? '');
_showSuccessBottomSheet(response.refNo ?? '', refundDestination);
},
onError: (err) {
LoaderBottomSheet.hideLoader();
@ -583,7 +850,11 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
);
}
void _showSuccessBottomSheet(String refNo) {
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,
child: Padding(
@ -604,6 +875,16 @@ class _RefundRequestCreatePageState extends State<RefundRequestCreatePage> {
],
).paddingSymmetrical(24.w, 0),
],
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(
children: [

@ -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),
@ -195,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(
@ -213,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,
),
],
),
],
],
],
),
@ -221,18 +273,64 @@ 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),
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.end,
),
),
],
),
SizedBox(width: 8.w),
(procedure.patientPaidAmount?.toString() ?? '-').toText12(color: AppColors.textColor, isBold: true),
if (hasRejectionReason) ...[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 10.h),
"${LocaleKeys.procedureRejectionReason.tr(context: context)}:".toText10(
color: AppColors.greyInfoTextColor,
isBold: true,
),
Row(
children: [
Expanded(
child: procedure.rejectionReason!.toText10(
color: AppColors.greyInfoTextColor,
),
),
],
),
],
),
],
],
),
);

@ -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<void> 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 {

@ -22,6 +22,7 @@ class ImageOptions {
bool showFilesOption,
Function(String, File) image, {
String? title,
bool allowMultiple = false, // Allow multiple file/image selection
Future<bool> Function()? checkCameraPermission,
Future<bool> 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<XFile> 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,9 +125,22 @@ class ImageOptions {
'zip',
],
);
if (result == null) return;
List<File> 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: () {},
@ -218,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<XFile> 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');
}
@ -257,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<Widget> children;

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

Loading…
Cancel
Save