diff --git a/assets/images/svg/open_file_icon.svg b/assets/images/svg/open_file_icon.svg
new file mode 100644
index 00000000..91d64fe5
--- /dev/null
+++ b/assets/images/svg/open_file_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json
index d39e8bd7..512a74f7 100644
--- a/assets/langs/ar-SA.json
+++ b/assets/langs/ar-SA.json
@@ -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) أدناه عند بوابة الخروج من موقف السيارات:"
}
diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json
index 61a6da03..71894964 100644
--- a/assets/langs/en-US.json
+++ b/assets/langs/en-US.json
@@ -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:"
}
+
diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart
index d2ea81f9..eaecaab3 100644
--- a/lib/core/api/api_client.dart
+++ b/lib/core/api/api_client.dart
@@ -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;
diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart
index aa51a5da..1542ce82 100644
--- a/lib/core/api_consts.dart
+++ b/lib/core/api_consts.dart
@@ -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';
diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart
index 2e27d633..29bf2fd7 100644
--- a/lib/core/app_assets.dart
+++ b/lib/core/app_assets.dart
@@ -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';
diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart
index 2731cc1a..45b4aecf 100644
--- a/lib/core/app_state.dart
+++ b/lib/core/app_state.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import 'dart:developer';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
@@ -9,8 +10,8 @@ import 'package:hmg_patient_app_new/core/common_models/privilege/VidaPlusProject
import 'package:hmg_patient_app_new/features/authentication/models/resp_models/authenticated_user_resp_model.dart';
import 'package:hmg_patient_app_new/features/authentication/models/resp_models/check_user_staus_nhic_response_model.dart';
import 'package:hmg_patient_app_new/features/authentication/models/resp_models/select_device_by_imei.dart';
-import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/services/cache_service.dart';
+import 'package:hmg_patient_app_new/services/navigation_service.dart';
import '../features/authentication/models/request_models/registration_payload_model.dart';
@@ -20,7 +21,7 @@ class AppState {
static const String _profileImageKey = 'profile_image_data';
- AppState({required this.navigationService, required this.cacheService}){
+ AppState({required this.navigationService, required this.cacheService}) {
checkIfGMSAvailableOrNot();
_loadProfileImageFromCache();
}
@@ -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 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;
diff --git a/lib/features/blood_donation/blood_donation_repo.dart b/lib/features/blood_donation/blood_donation_repo.dart
index c709b038..2cbcd916 100644
--- a/lib/features/blood_donation/blood_donation_repo.dart
+++ b/lib/features/blood_donation/blood_donation_repo.dart
@@ -116,7 +116,7 @@ class BloodDonationRepoImp implements BloodDonationRepo {
GenericApiModel>? apiResponse;
Failure? failure;
await apiClient.post(
- GET_PROJECT_LIST,
+ ApiConsts.getProjectsList,
body: request,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
diff --git a/lib/features/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart
index 9afa113e..8bbb596c 100644
--- a/lib/features/book_appointments/book_appointments_repo.dart
+++ b/lib/features/book_appointments/book_appointments_repo.dart
@@ -271,7 +271,7 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo {
GenericApiModel? apiResponse;
Failure? failure;
await apiClient.post(
- GET_DOCTOR_PROFILE,
+ ApiConsts.getDoctorProfiles,
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
@@ -527,7 +527,7 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo {
GenericApiModel>? apiResponse;
Failure? failure;
await apiClient.post(
- GET_PROJECT_LIST,
+ ApiConsts.getProjectsList,
body: request,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
diff --git a/lib/features/contact_us/contact_us_view_model.dart b/lib/features/contact_us/contact_us_view_model.dart
index 648fab6b..1b460a03 100644
--- a/lib/features/contact_us/contact_us_view_model.dart
+++ b/lib/features/contact_us/contact_us_view_model.dart
@@ -299,7 +299,7 @@ class ContactUsViewModel extends ChangeNotifier {
requestInsertCOCItem.projectID = 0;
requestInsertCOCItem.tokenID = "C0c@@dm!n?T&A&A@Barcha202029582948";
requestInsertCOCItem.identificationNo = int.parse(appState.getAuthenticatedUser()!.patientIdentificationNo!);
- if (BASE_URL.contains('uat')) {
+ if (ApiConsts.baseUrl.contains('uat')) {
requestInsertCOCItem.forDemo = true;
} else {
requestInsertCOCItem.forDemo = false;
diff --git a/lib/features/emergency_services/emergency_services_repo.dart b/lib/features/emergency_services/emergency_services_repo.dart
index 582c4a58..5fca01a8 100644
--- a/lib/features/emergency_services/emergency_services_repo.dart
+++ b/lib/features/emergency_services/emergency_services_repo.dart
@@ -189,7 +189,7 @@ class EmergencyServicesRepoImp implements EmergencyServicesRepo {
GenericApiModel>? apiResponse;
Failure? failure;
await apiClient.post(
- GET_PROJECT_LIST,
+ ApiConsts.getProjectsList,
body: request,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
diff --git a/lib/features/habib_wallet/habib_wallet_repo.dart b/lib/features/habib_wallet/habib_wallet_repo.dart
index 57d5af20..c4a356bd 100644
--- a/lib/features/habib_wallet/habib_wallet_repo.dart
+++ b/lib/features/habib_wallet/habib_wallet_repo.dart
@@ -13,7 +13,12 @@ abstract class HabibWalletRepo {
Future>>> getProjectList();
Future>> HISCreateAdvancePayment(
- {required String paymentMethodName, required num paidAmount, required String paymentReference, required String patientID, required int projectID, required String depositorName});
+ {required String paymentMethodName,
+ required num paidAmount,
+ required String paymentReference,
+ required String patientID,
+ required int projectID,
+ required String depositorName});
Future>> addAdvanceNumberRequest({required String advanceNumber, required String paymentReference});
@@ -22,6 +27,8 @@ abstract class HabibWalletRepo {
Future>> getLakumAccountInformation({required String identificationNumber});
Future>> getLakumAccountInquiryInformation({required String lakumAccountNumber});
+
+ Future>> checkIsPatientAdmittedOrAdmissionRequest({required bool isActiveAppointment});
}
class HabibWalletRepoImp implements HabibWalletRepo {
@@ -50,7 +57,10 @@ class HabibWalletRepoImp implements HabibWalletRepo {
// throw Exception("lab list is empty");
// }
- final List balanceAmountList = list.map((item) => PatientAdvanceBalanceResponseModel.fromJson(item as Map)).toList().cast();
+ final List balanceAmountList = list
+ .map((item) => PatientAdvanceBalanceResponseModel.fromJson(item as Map))
+ .toList()
+ .cast();
for (var element in balanceAmountList) {
element.totalAmount = response['TotalAdvanceBalanceAmount'];
@@ -83,7 +93,7 @@ class HabibWalletRepoImp implements HabibWalletRepo {
GenericApiModel>? apiResponse;
Failure? failure;
await apiClient.post(
- GET_PROJECT_LIST,
+ ApiConsts.getProjectsList,
body: request,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
@@ -115,7 +125,12 @@ class HabibWalletRepoImp implements HabibWalletRepo {
@override
Future> HISCreateAdvancePayment(
- {required String paymentMethodName, required num paidAmount, required String paymentReference, required String patientID, required int projectID, required String depositorName}) async {
+ {required String paymentMethodName,
+ required num paidAmount,
+ required String paymentReference,
+ required String patientID,
+ required int projectID,
+ required String depositorName}) async {
Map request = {
"CustName": depositorName,
"CustID": patientID,
@@ -306,4 +321,40 @@ class HabibWalletRepoImp implements HabibWalletRepo {
return Left(UnknownFailure(e.toString()));
}
}
+
+ @override
+ Future>> checkIsPatientAdmittedOrAdmissionRequest({required bool isActiveAppointment}) async {
+ Map requestBody = {
+ "IsActiveAppointment": isActiveAppointment,
+ };
+
+ try {
+ GenericApiModel? apiResponse;
+ Failure? failure;
+ await apiClient.post(
+ ApiConsts.checkIfPatientAdmitted,
+ body: requestBody,
+ onFailure: (error, statusCode, {messageStatus, failureType}) {
+ failure = failureType;
+ },
+ onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
+ try {
+ apiResponse = GenericApiModel(
+ messageStatus: messageStatus,
+ statusCode: statusCode,
+ errorMessage: errorMessage,
+ data: response,
+ );
+ } catch (e) {
+ failure = DataParsingFailure(e.toString());
+ }
+ },
+ );
+ if (failure != null) return Left(failure!);
+ if (apiResponse == null) return Left(ServerFailure("Unknown error"));
+ return Right(apiResponse!);
+ } catch (e) {
+ return Left(UnknownFailure(e.toString()));
+ }
+ }
}
diff --git a/lib/features/habib_wallet/habib_wallet_view_model.dart b/lib/features/habib_wallet/habib_wallet_view_model.dart
index 3bee29de..514bac83 100644
--- a/lib/features/habib_wallet/habib_wallet_view_model.dart
+++ b/lib/features/habib_wallet/habib_wallet_view_model.dart
@@ -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 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 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 ?? "",
+ )
+ ]
+ : [];
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 addAdvanceNumberRequest({required String advanceNumber, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async {
+ Future addAdvanceNumberRequest(
+ {required String advanceNumber, required String paymentReference, Function(dynamic)? onSuccess, Function(String)? onError}) async {
final result = await habibWalletRepo.addAdvanceNumberRequest(advanceNumber: advanceNumber, paymentReference: paymentReference);
result.fold(
@@ -548,7 +637,8 @@ class HabibWalletViewModel extends ChangeNotifier {
lakumAccountInfo = LakumInquiryInformationResponseModel();
notifyListeners();
- final result = await habibWalletRepo.getLakumAccountInformation(identificationNumber: getIt.get().getAuthenticatedUser()!.patientIdentificationNo ?? "");
+ final result = await habibWalletRepo.getLakumAccountInformation(
+ identificationNumber: getIt.get().getAuthenticatedUser()!.patientIdentificationNo ?? "");
result.fold(
// (failure) async => await errorHandlerService.handleError(failure: failure),
@@ -589,4 +679,89 @@ class HabibWalletViewModel extends ChangeNotifier {
},
);
}
+
+ // ── Check Is Patient Admitted Or Admission Request ────────────────────────
+ bool isCheckingPatientAdmitted = false;
+
+ Future checkIsPatientAdmittedOrAdmissionRequest({
+ required bool isActiveAppointment,
+ Function(dynamic)? onSuccess,
+ Function(String)? onError,
+ }) async {
+ isCheckingPatientAdmitted = true;
+ notifyListeners();
+
+ final result = await habibWalletRepo.checkIsPatientAdmittedOrAdmissionRequest(
+ isActiveAppointment: isActiveAppointment,
+ );
+
+ result.fold(
+ (failure) {
+ isCheckingPatientAdmitted = false;
+ notifyListeners();
+ if (onError != null) {
+ onError(failure.message);
+ } else {
+ errorHandlerService.handleError(failure: failure);
+ }
+ },
+ (apiResponse) {
+ isCheckingPatientAdmitted = false;
+ notifyListeners();
+ if (apiResponse.messageStatus == 1) {
+ if (onSuccess != null) {
+ onSuccess(apiResponse.data);
+ }
+ } else {
+ if (onError != null) {
+ onError(apiResponse.errorMessage ?? LocaleKeys.failed.tr());
+ }
+ }
+ },
+ );
+ }
+
+ // ── Threshold Amount ──────────────────────────────────────────────────────
+ double thresholdAmount = 5000.0;
+ bool isThresholdAmountLoading = false;
+
+ Future getThresholdAmount({
+ required int projectId,
+ Function(double)? onSuccess,
+ Function(String)? onError,
+ }) async {
+ isThresholdAmountLoading = true;
+ notifyListeners();
+
+ final result = await refundRequestRepo.getThresholdAmount(projectId: projectId);
+
+ result.fold(
+ (failure) {
+ isThresholdAmountLoading = false;
+ thresholdAmount = 5000.0;
+ notifyListeners();
+ if (onError != null) {
+ onError(failure.message);
+ }
+ },
+ (apiResponse) {
+ isThresholdAmountLoading = false;
+ if (apiResponse.messageStatus == 1 && apiResponse.data != null && apiResponse.data!.thresholdAmountList != null) {
+ final thresholdItem = apiResponse.data!.thresholdAmountList!.firstWhere(
+ (item) => item.projectID == projectId,
+ orElse: () => ThresholdAmountItem(thresholdAvailableAmountByProject: 5000.0),
+ );
+ thresholdAmount = thresholdItem.thresholdAvailableAmountByProject ?? 5000.0;
+ notifyListeners();
+ if (onSuccess != null) onSuccess(thresholdAmount);
+ } else {
+ thresholdAmount = 5000.0;
+ notifyListeners();
+ if (onError != null) {
+ onError(apiResponse.errorMessage ?? "Failed to get threshold amount");
+ }
+ }
+ },
+ );
+ }
}
diff --git a/lib/features/my_appointments/my_appointments_repo.dart b/lib/features/my_appointments/my_appointments_repo.dart
index d7a025fc..c89e3776 100644
--- a/lib/features/my_appointments/my_appointments_repo.dart
+++ b/lib/features/my_appointments/my_appointments_repo.dart
@@ -525,7 +525,7 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo {
GenericApiModel>? apiResponse;
Failure? failure;
await apiClient.post(
- GET_MY_DOCTOR,
+ ApiConsts.getMyDoctor,
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
diff --git a/lib/features/my_invoices/models/get_invoices_list_response_model.dart b/lib/features/my_invoices/models/get_invoices_list_response_model.dart
index a3c9858c..1aa580e7 100644
--- a/lib/features/my_invoices/models/get_invoices_list_response_model.dart
+++ b/lib/features/my_invoices/models/get_invoices_list_response_model.dart
@@ -19,6 +19,10 @@ class GetInvoicesListResponseModel {
String? doctorImageUrl;
int? doctorRate;
bool? isAllowRefund;
+ int? invoiceType;
+ String? invoiceTypeDescription;
+ bool? isPackageInvoice;
+ bool? isOutPatient;
int? patientNumber;
String? projectName;
@@ -41,6 +45,10 @@ class GetInvoicesListResponseModel {
this.doctorImageUrl,
this.doctorRate,
this.isAllowRefund,
+ this.invoiceType,
+ this.invoiceTypeDescription,
+ this.isPackageInvoice,
+ this.isOutPatient,
this.patientNumber,
this.projectName,
});
@@ -68,6 +76,10 @@ class GetInvoicesListResponseModel {
doctorImageUrl: json["DoctorImageURL"],
doctorRate: json["DoctorRate"],
isAllowRefund: json["IsAllowRefund"],
+ invoiceType: json["InvoiceType"],
+ invoiceTypeDescription: json["InvoiceTypeDescription"],
+ isPackageInvoice: json["IsPackageInvoice"],
+ isOutPatient: json['InvoiceType'] != 75,
patientNumber: json["PatientNumber"],
projectName: json["ProjectName"],
);
@@ -91,6 +103,10 @@ class GetInvoicesListResponseModel {
"DoctorImageURL": doctorImageUrl,
"DoctorRate": doctorRate,
"IsAllowRefund": isAllowRefund,
+ "invoiceType": invoiceType,
+ "invoiceDescription": invoiceTypeDescription,
+ "isOutPatient": isOutPatient,
+ "isPackageInvoice": isPackageInvoice,
"PatientNumber": patientNumber,
"ProjectName": projectName,
};
diff --git a/lib/features/my_invoices/my_invoices_repo.dart b/lib/features/my_invoices/my_invoices_repo.dart
index 23f5d06a..c0f60049 100644
--- a/lib/features/my_invoices/my_invoices_repo.dart
+++ b/lib/features/my_invoices/my_invoices_repo.dart
@@ -199,7 +199,7 @@ class MyInvoicesRepoImp implements MyInvoicesRepo {
GenericApiModel? apiResponse;
Failure? failure;
await apiClient.post(
- DOWNLOAD_INVOICE_PDF,
+ downloadInvoicePdf,
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
@@ -238,7 +238,7 @@ class MyInvoicesRepoImp implements MyInvoicesRepo {
GenericApiModel? apiResponse;
Failure? failure;
await apiClient.post(
- DOWNLOAD_PHARMACY_INVOICE_PDF,
+ downloadPharmacyInvoicePdf,
body: mapDevice,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
diff --git a/lib/features/profile_settings/profile_settings_repo.dart b/lib/features/profile_settings/profile_settings_repo.dart
index 113a52c0..5c16e646 100644
--- a/lib/features/profile_settings/profile_settings_repo.dart
+++ b/lib/features/profile_settings/profile_settings_repo.dart
@@ -207,7 +207,7 @@ class ProfileSettingsRepoImp implements ProfileSettingsRepo {
Failure? failure;
await apiClient.post(
- UPLOAD_PROFILE_IMAGE,
+ uploadProfileImageData,
body: body,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
diff --git a/lib/features/refund_request/models/req_models/submit_refund_request_model.dart b/lib/features/refund_request/models/req_models/submit_refund_request_model.dart
index 93eaadca..ae8ac20e 100644
--- a/lib/features/refund_request/models/req_models/submit_refund_request_model.dart
+++ b/lib/features/refund_request/models/req_models/submit_refund_request_model.dart
@@ -79,6 +79,9 @@ class SubmitRefundRequestModel {
/// Populated only for wallet withdrawal refunds (refundType == '2').
final List refundAdvancesList;
+ /// List of base64 encoded images for refund attachments (max 4 files, each max 2MB)
+ final List refundAttachmentsBase64List;
+
const SubmitRefundRequestModel({
required this.projectId,
required this.refundType,
@@ -101,6 +104,7 @@ class SubmitRefundRequestModel {
required this.isRefundFastTrack,
required this.refundProceduresList,
this.refundAdvancesList = const [],
+ this.refundAttachmentsBase64List = const [],
});
/// refundType '1' = invoice refund → sends Refund_ProceduresList
@@ -122,6 +126,7 @@ class SubmitRefundRequestModel {
"Refund_Remarks": refundRemarks,
"Refund_TotalRefundAmount": refundTotalAmount,
"Refund_IsFastTrack": isRefundFastTrack,
+ "Refund_AttachmentsBase64List": refundAttachmentsBase64List,
};
if (refundType == '2') {
diff --git a/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart b/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart
index b17dd5aa..bf8b2ec1 100644
--- a/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart
+++ b/lib/features/refund_request/models/resp_models/refund_request_list_response_model.dart
@@ -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 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) : null,
invoiceRequestPayload:
diff --git a/lib/features/refund_request/models/resp_models/threshold_amount_response_model.dart b/lib/features/refund_request/models/resp_models/threshold_amount_response_model.dart
new file mode 100644
index 00000000..698e6b01
--- /dev/null
+++ b/lib/features/refund_request/models/resp_models/threshold_amount_response_model.dart
@@ -0,0 +1,50 @@
+class ThresholdAmountResponseModel {
+ final List? thresholdAmountList;
+
+ ThresholdAmountResponseModel({this.thresholdAmountList});
+
+ factory ThresholdAmountResponseModel.fromJson(Map json) {
+ return ThresholdAmountResponseModel(
+ thresholdAmountList: json['ThresholdAmountList'] != null
+ ? (json['ThresholdAmountList'] as List)
+ .map((item) => ThresholdAmountItem.fromJson(item as Map))
+ .toList()
+ : null,
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'ThresholdAmountList': thresholdAmountList?.map((item) => item.toJson()).toList(),
+ };
+ }
+}
+
+class ThresholdAmountItem {
+ final int? projectID;
+ final double? thresholdAvailableAmountByProject;
+
+ ThresholdAmountItem({
+ this.projectID,
+ this.thresholdAvailableAmountByProject,
+ });
+
+ factory ThresholdAmountItem.fromJson(Map json) {
+ return ThresholdAmountItem(
+ projectID: json['ProjectID'] as int?,
+ thresholdAvailableAmountByProject: json['ThresholdAvailableAmountByProject'] != null
+ ? (json['ThresholdAvailableAmountByProject'] is int
+ ? (json['ThresholdAvailableAmountByProject'] as int).toDouble()
+ : json['ThresholdAvailableAmountByProject'] as double)
+ : null,
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'ProjectID': projectID,
+ 'ThresholdAvailableAmountByProject': thresholdAvailableAmountByProject,
+ };
+ }
+}
+
diff --git a/lib/features/refund_request/refund_request_repo.dart b/lib/features/refund_request/refund_request_repo.dart
index eaf7d9e0..cd07a25e 100644
--- a/lib/features/refund_request/refund_request_repo.dart
+++ b/lib/features/refund_request/refund_request_repo.dart
@@ -7,6 +7,7 @@ import 'package:hmg_patient_app_new/features/refund_request/models/req_models/su
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/is_refundable_invoice_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refundable_invoices_response_model.dart';
+import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/threshold_amount_response_model.dart';
import 'package:hmg_patient_app_new/services/logger_service.dart';
abstract class RefundRequestRepo {
@@ -33,6 +34,8 @@ abstract class RefundRequestRepo {
required int invoiceNo,
required int projectId,
});
+
+ Future>> getThresholdAmount({required int projectId});
}
class RefundRequestRepoImp implements RefundRequestRepo {
@@ -357,4 +360,46 @@ class RefundRequestRepoImp implements RefundRequestRepo {
return Left(UnknownFailure(e.toString()));
}
}
+
+ @override
+ Future>> getThresholdAmount({required int projectId}) async {
+ final Map body = {
+ "ProjectID": projectId,
+ };
+
+ try {
+ GenericApiModel? apiResponse;
+ Failure? failure;
+
+ await apiClient.post(
+ ApiConsts.getThresholdAmount,
+ body: body,
+ onFailure: (error, statusCode, {messageStatus, failureType}) {
+ failure = failureType;
+ },
+ onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
+ try {
+ final ThresholdAmountResponseModel model = ThresholdAmountResponseModel.fromJson(response);
+
+ apiResponse = GenericApiModel(
+ messageStatus: messageStatus,
+ statusCode: statusCode,
+ errorMessage: errorMessage,
+ data: model,
+ );
+ } catch (e) {
+ loggerService.logError("getThresholdAmount parsing error: $e");
+ failure = DataParsingFailure(e.toString());
+ }
+ },
+ );
+
+ if (failure != null) return Left(failure!);
+ if (apiResponse == null) return Left(ServerFailure("Unknown error"));
+ return Right(apiResponse!);
+ } catch (e) {
+ loggerService.logError("getThresholdAmount exception: $e");
+ return Left(UnknownFailure(e.toString()));
+ }
+ }
}
diff --git a/lib/features/refund_request/refund_request_view_model.dart b/lib/features/refund_request/refund_request_view_model.dart
index 828e3d0a..e19ee464 100644
--- a/lib/features/refund_request/refund_request_view_model.dart
+++ b/lib/features/refund_request/refund_request_view_model.dart
@@ -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 procedures = [];
bool isProceduresLoading = false;
bool needITGApproval = false;
+ bool isWholeInvoiceRefund = false; // For Inpatient/Package invoices
- void setProcedures(List list, {bool needsITGApproval = false}) {
+ void setProcedures(List list, {bool needsITGApproval = false, bool isWholeInvoiceRefund = false}) {
procedures = list;
needITGApproval = needsITGApproval;
+ this.isWholeInvoiceRefund = isWholeInvoiceRefund;
isProceduresLoading = false;
notifyListeners();
}
void toggleProcedureSelection(RefundableProcedure procedure) {
+ // Prevent selection toggle for whole invoice refunds
+ if (isWholeInvoiceRefund) return;
+
procedure.isSelected = !procedure.isSelected;
notifyListeners();
}
@@ -229,6 +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 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 getThresholdAmount({
+ required int projectId,
+ Function(double)? onSuccess,
+ Function(String)? onError,
+ }) async {
+ isThresholdAmountLoading = true;
+ notifyListeners();
+
+ final result = await refundRequestRepo.getThresholdAmount(projectId: projectId);
+
+ result.fold(
+ (failure) {
+ isThresholdAmountLoading = false;
+ thresholdAmount = 5000.0;
+ notifyListeners();
+ if (onError != null) {
+ onError(failure.message);
+ }
+ },
+ (apiResponse) {
+ isThresholdAmountLoading = false;
+ if (apiResponse.messageStatus == 1 && apiResponse.data != null && apiResponse.data!.thresholdAmountList != null) {
+ final thresholdItem = apiResponse.data!.thresholdAmountList!.firstWhere(
+ (item) => item.projectID == projectId,
+ orElse: () => ThresholdAmountItem(thresholdAvailableAmountByProject: 5000.0),
+ );
+ thresholdAmount = thresholdItem.thresholdAvailableAmountByProject ?? 5000.0;
+ notifyListeners();
+ if (onSuccess != null) onSuccess(thresholdAmount);
+ } else {
+ thresholdAmount = 5000.0;
+ notifyListeners();
+ if (onError != null) {
+ onError(apiResponse.errorMessage ?? "Failed to get threshold amount");
+ }
+ }
+ },
+ );
+ }
}
diff --git a/lib/features/weather/weather_repo.dart b/lib/features/weather/weather_repo.dart
index 26bf0a32..4777713f 100644
--- a/lib/features/weather/weather_repo.dart
+++ b/lib/features/weather/weather_repo.dart
@@ -25,7 +25,7 @@ class WeatherRepoImp implements WeatherRepo {
GenericApiModel>? apiResponse;
Failure? failure;
await apiClient.post(
- WEATHER_INDICATOR,
+ ApiConsts.weatherIndicator,
body: request,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart
index 8d29d818..ac2c1ea0 100644
--- a/lib/generated/locale_keys.g.dart
+++ b/lib/generated/locale_keys.g.dart
@@ -2,7 +2,7 @@
// ignore_for_file: constant_identifier_names
-abstract class LocaleKeys {
+abstract class LocaleKeys {
static const english = 'english';
static const arabic = 'arabic';
static const login = 'login';
@@ -1911,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';
-
}
diff --git a/lib/presentation/contact_us/feedback_page.dart b/lib/presentation/contact_us/feedback_page.dart
index 32359fef..f3579cda 100644
--- a/lib/presentation/contact_us/feedback_page.dart
+++ b/lib/presentation/contact_us/feedback_page.dart
@@ -355,8 +355,7 @@ class FeedbackPage extends StatelessWidget {
context,
true,
(String image, file) {
- print(image);
- print(file);
+
// Navigator.pop(context);
contactUsViewModel.addFeedbackAttachment(image);
},
diff --git a/lib/presentation/e_referral/widget/e_referral_other_details.dart b/lib/presentation/e_referral/widget/e_referral_other_details.dart
index 043dda21..64dc3730 100644
--- a/lib/presentation/e_referral/widget/e_referral_other_details.dart
+++ b/lib/presentation/e_referral/widget/e_referral_other_details.dart
@@ -217,6 +217,7 @@ class _OtherDetailsStepState extends State {
(String image, File file) {
_addInsuranceDocument(image, file, formManager);
},
+ allowMultiple: true, // Enable multiple selection
);
},
),
diff --git a/lib/presentation/habib_wallet/habib_wallet_page.dart b/lib/presentation/habib_wallet/habib_wallet_page.dart
index c4ee9356..5463d94e 100644
--- a/lib/presentation/habib_wallet/habib_wallet_page.dart
+++ b/lib/presentation/habib_wallet/habib_wallet_page.dart
@@ -136,23 +136,58 @@ class _HabibWalletState extends State {
},
),
),
- // SizedBox(width: 8.w),
- // Flexible(
- // child: CustomButton(
- // height: 40.h,
- // icon: AppAssets.refundIcon,
- // iconSize: 24.w,
- // backgroundColor: AppColors.successColor,
- // textColor: Colors.white,
- // text: LocaleKeys.withdraw.tr(context: context),
- // borderWidth: 0.w,
- // fontWeight: FontWeight.w600,
- // borderColor: Colors.transparent,
- // padding: EdgeInsets.fromLTRB(4, 0, 12, 0),
- // fontSize: 14.f,
- // onPressed: () => Navigator.of(context).push(CustomPageRoute(page: WithdrawRequestCreatePage())),
- // ),
- // ),
+ SizedBox(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 {
);
}
}
-
-
diff --git a/lib/presentation/habib_wallet/withdraw_request_create_page.dart b/lib/presentation/habib_wallet/withdraw_request_create_page.dart
index cc0c9731..b8ace1a9 100644
--- a/lib/presentation/habib_wallet/withdraw_request_create_page.dart
+++ b/lib/presentation/habib_wallet/withdraw_request_create_page.dart
@@ -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 {
_appState = getIt.get();
_habibWalletViewModel = context.read();
WidgetsBinding.instance.addPostFrameCallback((_) {
+ _habibWalletViewModel.currentWithdrawStep = 0;
+ _habibWalletViewModel.selectedAdvance = null;
_habibWalletViewModel.getProjectsList();
});
}
@@ -59,12 +68,262 @@ class _WithdrawRequestCreatePageState extends State {
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(
+ builder: (context, walletVM, _) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ LocaleKeys.selectAdvance.tr(context: context).toText20(isBold: true),
+ SizedBox(height: 24.h),
+ if (vm.isAdvancesLoading)
+ ListView.separated(
+ padding: EdgeInsets.zero,
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ separatorBuilder: (context, index) => SizedBox(height: 16.h),
+ itemCount: 4,
+ itemBuilder: (_, index) => Container(
+ height: 80.h,
+ decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
+ color: AppColors.whiteColor,
+ borderRadius: 16.h,
+ ),
+ ).toShimmer2(isShow: true, radius: 16.r),
+ )
+ else if (vm.refundableAdvancesList.isEmpty)
+ Center(
+ child: Padding(
+ padding: EdgeInsets.symmetric(vertical: 24.h),
+ child: LocaleKeys.noAdvancesAvailable.tr().toText14(color: AppColors.greyTextColor),
+ ),
+ )
+ else
+ AnimationLimiter(
+ child: Container(
+ decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
+ color: AppColors.whiteColor,
+ borderRadius: 24.h,
+ hasShadow: false,
+ ),
+ child: ListView.separated(
+ padding: EdgeInsets.zero,
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ separatorBuilder: (context, index) => Divider(
+ color: AppColors.bottomNAVBorder,
+ height: 1,
+ thickness: 1,
+ ).paddingSymmetrical(16.w, 0.w),
+ itemCount: vm.refundableAdvancesList.length,
+ itemBuilder: (context, index) {
+ final advance = vm.refundableAdvancesList[index];
+ return _buildAdvanceCard(advance, index);
+ },
+ ),
+ ),
+ ),
+ ],
+ );
+ },
+ );
+ }
+
+ Widget _buildAdvanceCard(RefundableAdvancesItem advance, int index) {
+ return AnimationConfiguration.staggeredList(
+ position: index,
+ duration: const Duration(milliseconds: 400),
+ child: SlideAnimation(
+ verticalOffset: 50.0,
+ child: FadeInAnimation(
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: () => _habibWalletViewModel.setSelectedAdvance(advance),
+ child: Padding(
+ padding: EdgeInsets.all(16.h),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ SizedBox(
+ width: 24.w,
+ height: 24.w,
+ child: Radio(
+ value: advance.advanceNo ?? 0,
+ groupValue: _habibWalletViewModel.selectedAdvance?.advanceNo ?? -1,
+ onChanged: (_) => _habibWalletViewModel.setSelectedAdvance(advance),
+ activeColor: AppColors.primaryRedColor,
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ visualDensity: VisualDensity.compact,
+ ),
+ ),
+ SizedBox(width: 12.w),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ "${LocaleKeys.advanceNo.tr()}: ${advance.advanceNo ?? 0}".toText16(isBold: true),
+ SizedBox(height: 4.h),
+ _buildAdvanceDate(advance.advanceDate),
+ ],
+ ),
+ ),
+ SizedBox(width: 8.w),
+ Utils.getPaymentAmountWithSymbol(
+ (advance.balanceAmount ?? 0.0).toStringAsFixed(2).toText20(
+ isBold: true,
+ isEnglishOnly: true,
+ color: AppColors.textColor,
+ ),
+ AppColors.textColor,
+ 10.f,
+ isSaudiCurrency: true,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildAdvanceDate(String? dateString) {
+ if (dateString == null || dateString.isEmpty) {
+ return const SizedBox.shrink();
+ }
+
+ try {
+ final date = DateUtil.convertStringToDate(dateString);
+ final formattedDate = DateUtil.formatDateToDate(date, false);
+ return formattedDate.toText12(color: AppColors.greyTextColor);
+ } catch (e) {
+ return const SizedBox.shrink();
+ }
+ }
+
+ // ── Step 2 — 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(
+ 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 {
Widget _buildBottomBar() {
return Consumer(
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 {
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 {
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 {
]),
],
),
- 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 {
);
}
- 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(
+ 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 {
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 {
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 {
);
}
- 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 {
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 {
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 {
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 {
),
),
child: SingleChildScrollView(
- padding: EdgeInsets.only(bottom: 32.h, left: 24.w, right: 24.w),
+ padding: EdgeInsets.only(bottom: 32.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- _buildProjectSelectionAndBalance(habibWalletViewModel),
+ // Hospital selection card - always visible
+ Padding(
+ padding: EdgeInsets.symmetric(horizontal: 24.w),
+ child: _buildProjectSelectionAndBalance(habibWalletViewModel),
+ ),
+
+ // Stepper - show only after hospital selected
if (habibWalletViewModel.selectedHospital != null) ...[
SizedBox(height: 16.h),
- _buildBankInputDetails(),
- // SizedBox(height: 16.h),
- // _buildImagePickerDropDown(),
+ _buildStepper(habibWalletViewModel),
+ SizedBox(height: 16.h),
],
+
+ // Step content - show only after hospital selected
+ if (habibWalletViewModel.selectedHospital != null)
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 350),
+ transitionBuilder: (child, animation) => FadeTransition(
+ opacity: animation,
+ child: SlideTransition(
+ position: Tween(
+ begin: const Offset(0.05, 0),
+ end: Offset.zero,
+ ).animate(animation),
+ child: child,
+ ),
+ ),
+ child: habibWalletViewModel.currentWithdrawStep == 0
+ ? KeyedSubtree(
+ key: const ValueKey('step1'),
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 24.w),
+ child: _buildStep1Content(habibWalletViewModel),
+ ),
+ )
+ : KeyedSubtree(
+ key: const ValueKey('step2'),
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 24.w),
+ child: 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 {
);
}
}
-
-// I/flutter (13750): uri: https://uat.hmgwebservices.com/Services/COCWS.svc/REST/SubmitRefund
-// [log] body: {"ProjectID":15,"Refund_RefundType":"2","Refund_Project":"مستشفى الريان","Refund_ProjectId":15,"Refund_PatientId":"4770714","PatientMobileNumber":"0504278212","Refund_PatientName":"FAIZ UR REHMAN REHMAN","Refund_NationalId":"2530976584","Refund_DoctorId":0,"Refund_DoctorName":"","Refund_RefundDestination":"2","Refund_IBAN":"SA0380000000608010167519","Refund_Remarks":"teskchjvs","Refund_TotalRefundAmount":1714.75,"Refund_AdvancesList":[{"AdvanceNo":"2026000444","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1775682000000+0300)/"},{"AdvanceNo":"2026000392","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1774645200000+0300)/"},{"AdvanceNo":"2026000066","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1770152400000+0300)/"},{"AdvanceNo":"2026000047","PaidAmount":30.0,"BalanceAmount":30.0,"AdvanceDate":"/Date(1769547600000+0300)/"},{"AdvanceNo":"2025000751","PaidAmount":362.25,"BalanceAmount":362.25,"AdvanceDate":"/Date(1765746000000+0300)/"},{"AdvanceNo":"2025000414","PaidAmount":115.0,"BalanceAmount":115.0,"AdvanceDate":"/Date(1747083600000+0300)/"},{"AdvanceNo":"2025000210","PaidAmount":120.75,"BalanceAmount":120.75,"AdvanceDate":"/Date(1741726800000+0300)/"}],"PatientType":1,"PatientTypeID":1,"TokenID":"@dm!n","PatientID":4770714,"PatientOutSA":0,"SessionID":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOjIsIm9iaiI6eyJUaW1lIjoiMjAyNi0wNS0xOFQwNzo1MzoyMS4wMjQ5MDM2WiIsIlBhdGllbnRJRCI6IjQ3NzIxNjMiLCJQYXRpZW50TWVyZ2VkSURzIjoiNDc3MjE2MyIsIlBhdGllbnRPdXRTQSI6ZmFsc2UsIlNoYXJlZElkcyI6IiJ9LCJleHAiOjE3NzkxMzgwMDB9.VHd_kCya33GfK-DywTHwSU1PMewbQB7UhQD1zXS7X5w","VersionID":"21.2","Channel":"3","IPAdress":"10.20.10.20","generalid":"Cs2020@2016$2958","LanguageID":"1","Latitude":"0.0","Longitude":"0.0","DeviceTypeID":"2","IsNewFlutterApp":true}
-// [log] response: {"Date":null,"LanguageID":0,"ServiceName":0,"Time":null,"AndroidLink":null,"AuthenticationTokenID":null,"Data":null,"Dataw":false,"DietType":0,"DietTypeID":0,"ErrorCode":null,"ErrorEndUserMessage":"Internal server error occurred.","ErrorEndUserMessageN":null,"ErrorMessage":null,"ErrorStatusCode":0,"ErrorType":0,"FoodCategory":0,"IOSLink":null,"IsAuthenticated":true,"MealOrderStatus":0,"MealType":0,"MessageStatus":2,"NumberOfResultRecords":0,"PatientBlodType":null,"RequestId":null,"SuccessMsg":null,"SuccessMsgN":null,"VidaUpdatedResponse":null,"ENSAT_GetPatientRequestListResponse":null,"ENSAT_Patient_External_Response":null,"ETQAN_CreateIncident_Response":null,"ETQAN_GetEmployeeOVRRequests_Response":null,"ETQAN_GetIncidentRequest_Response":null,"ETQAN_GetProjects_Response":null,"ITGRequest":null,"Itg_FormAttachmentsList":null,"ListCOCItems":null,"Message":"Refunds API returned status 500. Body: {\"message\":\"Internal server error occurred.\"}","Mohemm_ENSAT_GetItgToken_Response":null,"Mohemm_ITG_DepartmentSectionsList":null,"Mohemm_ITG_Pending_Task_ResponseItem":null,"Mohemm_ITG_ProjectDepartmentsList":null,"Mohemm_ITG_ResponseItem":null,"Mohemm_ITG_SectionTopicsList":null,"Mohemm_ITG_TicketDetailsList":null,"Mohemm_ITG_TicketTransactionsList":null,"Mohemm_ITG_TicketsByEmployeeList":null,"Mohemm_Itg_ProjectsList":null,"Mohemm_Itg_TicketTypesList":null,"ReferenceNumber":null,"Refund_InvoicesList":null,"Refund_PatientIdDisplay":null,"Refund_RefNo":null,"Refund_RefundTypeDisplay":null,"Refund_StatusItem":null,"Refund_TotalInvoices":0,"Refund_VerifyResult":null,"RequestType":null,"TotalCount":0,"statuseCode":500}
diff --git a/lib/presentation/home/lakum_wallet_details.dart b/lib/presentation/home/lakum_wallet_details.dart
index d2ef15f4..10de8eac 100644
--- a/lib/presentation/home/lakum_wallet_details.dart
+++ b/lib/presentation/home/lakum_wallet_details.dart
@@ -250,7 +250,7 @@ class LakumWalletDetails extends StatelessWidget {
padding: EdgeInsets.fromLTRB(4, 0, 12, 0),
fontSize: 14.f,
onPressed: () {
- Uri uri = Uri.parse(PHARMACY_REDIRECT_URL);
+ Uri uri = Uri.parse(ApiConsts.pharmacyRedirectUrl);
launchUrl(uri, mode: LaunchMode.externalApplication);
},
);
diff --git a/lib/presentation/home/widgets/lakum_wallet_card.dart b/lib/presentation/home/widgets/lakum_wallet_card.dart
index 3645a2d3..f2ee7a26 100644
--- a/lib/presentation/home/widgets/lakum_wallet_card.dart
+++ b/lib/presentation/home/widgets/lakum_wallet_card.dart
@@ -137,7 +137,7 @@ class LakumWalletCard extends StatelessWidget {
padding: EdgeInsets.fromLTRB(4, 0, 12, 0),
fontSize: 14.f,
onPressed: () {
- Uri uri = Uri.parse(PHARMACY_REDIRECT_URL);
+ Uri uri = Uri.parse(ApiConsts.pharmacyRedirectUrl);
launchUrl(uri, mode: LaunchMode.externalApplication);
// Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage()));
},
diff --git a/lib/presentation/home/widgets/large_service_card.dart b/lib/presentation/home/widgets/large_service_card.dart
index 7c037604..57c2dbbd 100644
--- a/lib/presentation/home/widgets/large_service_card.dart
+++ b/lib/presentation/home/widgets/large_service_card.dart
@@ -165,7 +165,7 @@ class LargeServiceCard extends StatelessWidget {
}
case "pharmacy":
{
- Uri uri = Uri.parse(PHARMACY_REDIRECT_URL);
+ Uri uri = Uri.parse(ApiConsts.pharmacyRedirectUrl);
launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
@@ -312,7 +312,7 @@ class FadedLargeServiceCard extends StatelessWidget {
}
case "pharmacy":
{
- Uri uri = Uri.parse(PHARMACY_REDIRECT_URL);
+ Uri uri = Uri.parse(ApiConsts.pharmacyRedirectUrl);
launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
diff --git a/lib/presentation/my_invoices/widgets/invoice_list_card.dart b/lib/presentation/my_invoices/widgets/invoice_list_card.dart
index 8fea72fc..d1a8f718 100644
--- a/lib/presentation/my_invoices/widgets/invoice_list_card.dart
+++ b/lib/presentation/my_invoices/widgets/invoice_list_card.dart
@@ -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,
+ ),
+ ),
+ ]
],
),
],
diff --git a/lib/presentation/refund_request/refund_history_screen.dart b/lib/presentation/refund_request/refund_history_screen.dart
index 630c5dc0..f689ed4a 100644
--- a/lib/presentation/refund_request/refund_history_screen.dart
+++ b/lib/presentation/refund_request/refund_history_screen.dart
@@ -12,7 +12,7 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/refund_request/models/resp_models/refund_request_list_response_model.dart';
import 'package:hmg_patient_app_new/features/refund_request/refund_request_view_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
-import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_detail_screen.dart';
+import 'package:hmg_patient_app_new/presentation/refund_request/refund_request_detail_screen.dart' show RefundRequestDetailScreen;
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
diff --git a/lib/presentation/refund_request/refund_request_create_page.dart b/lib/presentation/refund_request/refund_request_create_page.dart
index 1d0f723f..b645307e 100644
--- a/lib/presentation/refund_request/refund_request_create_page.dart
+++ b/lib/presentation/refund_request/refund_request_create_page.dart
@@ -1,3 +1,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 {
_ibanController.addListener(() => _refundRequestViewModel.triggerRebuild());
_descriptionController.addListener(() => _refundRequestViewModel.triggerRebuild());
_loadInvoiceDetails();
+ _loadThresholdAmount();
});
}
@@ -74,6 +79,20 @@ class _RefundRequestCreatePageState extends State {
super.dispose();
}
+ /// Determines if the invoice allows individual procedure selection
+ /// Returns true for Outpatient and ER invoices only
+ /// Returns false for Inpatient and Package invoices (whole invoice refund only)
+ bool get _canSelectIndividualProcedures {
+ // Package invoices: no individual procedure selection
+ if (widget.invoice.isPackageInvoice == true) return false;
+
+ // Inpatient invoices: no individual procedure selection
+ if (widget.invoice.isOutPatient == false) return false;
+
+ // Outpatient and ER invoices: allow individual procedure selection
+ return true;
+ }
+
void _loadInvoiceDetails() {
final invoiceNo = widget.invoice.invoiceNo;
final projectId = widget.invoice.projectId;
@@ -90,13 +109,29 @@ class _RefundRequestCreatePageState extends State {
patientPaidAmount: item.patientPaidAmount ?? 0.0,
apiLineItemNo: item.lineItemNo,
apiProcedureId: item.procedureId,
+ // For Inpatient/Package invoices: auto-select all procedures (can't deselect)
+ isSelected: !_canSelectIndividualProcedures,
+ // For Inpatient/Package invoices: set empty string as refund reason (mandatory field)
+ refundReason: !_canSelectIndividualProcedures ? "Inpatient/Package invoice" : null,
))
.toList();
- _refundRequestViewModel.setProcedures(procedures, needsITGApproval: details.needITGApproval ?? false);
+
+ _refundRequestViewModel.setProcedures(
+ procedures,
+ needsITGApproval: details.needITGApproval ?? false,
+ isWholeInvoiceRefund: !_canSelectIndividualProcedures,
+ );
},
);
}
+ 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 {
);
}
- // ── Step 1 — exactly mirrors RechargeWalletPage hospital row ─────────────
+ // ── Step 1 — Select Procedures (or whole invoice for Inpatient/Package) ──
Widget _buildStep1Content(RefundRequestViewModel vm) {
return Consumer(
builder: (context, refundVM, _) {
- final isArabic = _appState.isArabic();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -164,39 +198,41 @@ class _RefundRequestCreatePageState extends State {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- // Select All row
- Material(
- color: Colors.transparent,
- child: InkWell(
- onTap: () => _refundRequestViewModel.toggleSelectAll(),
- child: Padding(
- padding: EdgeInsets.all(16.h),
- child: Row(
- children: [
- SizedBox(
- width: 24.w,
- height: 24.w,
- child: Transform.scale(
- scale: 24.w / 22,
- child: Checkbox(
- value: vm.isAllSelected,
- onChanged: (_) => _refundRequestViewModel.toggleSelectAll(),
- activeColor: AppColors.primaryRedColor,
- side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1),
- materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
- visualDensity: VisualDensity.compact,
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.r)),
+ // Select All row - only show for Outpatient/ER invoices
+ if (_canSelectIndividualProcedures)
+ Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: () => _refundRequestViewModel.toggleSelectAll(),
+ child: Padding(
+ padding: EdgeInsets.all(16.h),
+ child: Row(
+ children: [
+ SizedBox(
+ width: 24.w,
+ height: 24.w,
+ child: Transform.scale(
+ scale: 24.w / 22,
+ child: Checkbox(
+ value: vm.isAllSelected,
+ onChanged: (_) => _refundRequestViewModel.toggleSelectAll(),
+ activeColor: AppColors.primaryRedColor,
+ side: BorderSide(color: AppColors.checkBoxBorderColor, width: 1),
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ visualDensity: VisualDensity.compact,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.r)),
+ ),
),
),
- ),
- SizedBox(width: 8.w),
- LocaleKeys.selectAll.tr(context: context).toText16(isBold: true),
- ],
+ SizedBox(width: 8.w),
+ LocaleKeys.selectAll.tr(context: context).toText16(isBold: true),
+ ],
+ ),
),
),
),
- ),
- Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w),
+ if (_canSelectIndividualProcedures)
+ Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w),
...List.generate(
vm.procedures.length,
(index) => _buildProcedureCard(vm.procedures[index], index, vm.procedures.length),
@@ -217,6 +253,7 @@ class _RefundRequestCreatePageState extends State {
final paymentOptions = [
RefundTabType.hmgWallet,
RefundTabType.invoices,
+ RefundTabType.cash,
];
return Column(
@@ -237,6 +274,9 @@ class _RefundRequestCreatePageState extends State {
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final type = paymentOptions[index];
+ final isCashOption = type == RefundTabType.cash;
+ final isCashSelected = vm.selectedPaymentMethod == RefundTabType.cash;
+
return Column(
children: [
RadioListTileWidget(
@@ -247,6 +287,29 @@ class _RefundRequestCreatePageState extends State {
if (value != null) vm.setPaymentMethod(value);
},
),
+ // Show cash instructions inside cash tile when selected
+ if (isCashOption && isCashSelected) ...[
+ Padding(
+ padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 16.h),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Utils.buildSvgWithAssets(
+ icon: AppAssets.alertSquare,
+ height: 24.h,
+ width: 24.h,
+ iconColor: AppColors.textColor,
+ ),
+ SizedBox(width: 12.w),
+ Expanded(
+ child: LocaleKeys.cashRefundApprovalInstructions.tr(context: context).toText12(
+ color: AppColors.greyInfoTextColor,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
if (index != paymentOptions.length - 1)
Divider(color: AppColors.bottomNAVBorder, height: 1, thickness: 1).paddingSymmetrical(16.w, 0.w),
],
@@ -254,14 +317,193 @@ class _RefundRequestCreatePageState extends State {
},
),
),
+ if (vm.selectedPaymentMethod == RefundTabType.cash && vm.isCashRefundAmountExceeded) ...[
+ SizedBox(height: 16.h),
+ Container(
+ padding: EdgeInsets.all(16.h),
+ decoration: BoxDecoration(
+ color: AppColors.warningColorYellow.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(
+ 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 {
);
}
- // ── 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 {
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 {
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 {
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 {
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 {
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 {
);
}
- 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 {
],
).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: [
diff --git a/lib/presentation/refund_request/refund_request_detail_screen.dart b/lib/presentation/refund_request/refund_request_detail_screen.dart
index a28aa4dd..a163456d 100644
--- a/lib/presentation/refund_request/refund_request_detail_screen.dart
+++ b/lib/presentation/refund_request/refund_request_detail_screen.dart
@@ -186,8 +186,28 @@ class RefundRequestDetailScreen extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- LocaleKeys.procedure.tr(context: context).toText12(color: AppColors.textColor, isBold: true),
- LocaleKeys.amountOnly.tr(context: context).toText12(color: AppColors.textColor, isBold: true),
+ Expanded(
+ flex: 3,
+ child: LocaleKeys.procedure.tr(context: context).toText12(color: AppColors.textColor, isBold: true),
+ ),
+ SizedBox(width: 8.w),
+ SizedBox(
+ width: 70.w,
+ child: LocaleKeys.approved.tr(context: context).toText12(
+ color: AppColors.textColor,
+ isBold: true,
+ textAlignment: TextAlign.center,
+ ),
+ ),
+ SizedBox(width: 8.w),
+ SizedBox(
+ width: 60.w,
+ child: LocaleKeys.amountOnly.tr(context: context).toText12(
+ color: AppColors.textColor,
+ isBold: true,
+ textAlignment: TextAlign.end,
+ ),
+ ),
],
),
SizedBox(height: 16.h),
@@ -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,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
],
),
);
diff --git a/lib/widgets/attachment_options.dart b/lib/widgets/attachment_options.dart
index bbc0bc6d..9fe70ddd 100644
--- a/lib/widgets/attachment_options.dart
+++ b/lib/widgets/attachment_options.dart
@@ -1,4 +1,5 @@
import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
@@ -8,7 +9,6 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
-import 'package:flutter/material.dart';
class AttachmentOptions extends StatelessWidget {
final Future Function() onCameraTap;
@@ -16,13 +16,8 @@ class AttachmentOptions extends StatelessWidget {
final VoidCallback onFilesTap;
final bool showFilesOption;
- const AttachmentOptions({
- Key? key,
- required this.onCameraTap,
- required this.onGalleryTap,
- required this.onFilesTap,
- this.showFilesOption = true
- }) : super(key: key);
+ const AttachmentOptions({Key? key, required this.onCameraTap, required this.onGalleryTap, required this.onFilesTap, this.showFilesOption = true})
+ : super(key: key);
@override
Widget build(BuildContext context) {
@@ -52,7 +47,7 @@ class AttachmentOptions extends StatelessWidget {
if (showFilesOption)
checkInOptionCard(
appState,
- AppAssets.file_icon,
+ AppAssets.openFileIcon,
LocaleKeys.openFiles.tr(context: context),
LocaleKeys.selectDocumentFiles.tr(context: context),
).onPress(() async {
diff --git a/lib/widgets/image_picker.dart b/lib/widgets/image_picker.dart
index 6ab2b439..e92ef282 100644
--- a/lib/widgets/image_picker.dart
+++ b/lib/widgets/image_picker.dart
@@ -22,6 +22,7 @@ class ImageOptions {
bool showFilesOption,
Function(String, File) image, {
String? title,
+ bool allowMultiple = false, // Allow multiple file/image selection
Future Function()? checkCameraPermission,
Future Function()? checkGalleryPermission,
}) {
@@ -51,7 +52,11 @@ class ImageOptions {
String fileName = _image.path;
var bytes = File(fileName).readAsBytesSync();
String base64Encode = base64.encode(bytes);
- image(base64Encode, _image);
+
+ // Convert to data URI format
+ String mimeType = _getMimeType(fileName);
+ String dataUri = 'data:$mimeType;base64,$base64Encode';
+ image(dataUri, _image);
}
},
onGalleryTap: () async {
@@ -66,21 +71,46 @@ class ImageOptions {
Navigator.pop(context); // Close bottom sheet
if (Platform.isAndroid) {
- galleryImageAndroid(image);
+ galleryImageAndroid(image, allowMultiple: allowMultiple);
} else {
- XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery, imageQuality: 20);
- if (pickedFile == null) return;
+ if (allowMultiple) {
+ // iOS/macOS: Pick multiple images
+ List pickedFiles = await picker.pickMultiImage(imageQuality: 20);
+ if (pickedFiles.isEmpty) return;
- File _image = File(pickedFile.path);
- String fileName = _image.path;
- var bytes = File(fileName).readAsBytesSync();
- String base64Encode = base64.encode(bytes);
- image(base64Encode, _image);
+ for (var pickedFile in pickedFiles) {
+ File _image = File(pickedFile.path);
+ var bytes = await _image.readAsBytes();
+ String base64Encode = base64.encode(bytes);
+
+ // Convert to data URI format
+ String mimeType = _getMimeType(pickedFile.path);
+ String dataUri = 'data:$mimeType;base64,$base64Encode';
+ image(dataUri, _image);
+ }
+ } else {
+ // Single image selection
+ XFile? pickedFile = await picker.pickImage(source: ImageSource.gallery, imageQuality: 20);
+ if (pickedFile == null) return;
+
+ File _image = File(pickedFile.path);
+ String fileName = _image.path;
+ var bytes = File(fileName).readAsBytesSync();
+ String base64Encode = base64.encode(bytes);
+
+ // Convert to data URI format
+ String mimeType = _getMimeType(fileName);
+ String dataUri = 'data:$mimeType;base64,$base64Encode';
+ image(dataUri, _image);
+ }
}
},
onFilesTap: () async {
+ Navigator.pop(context); // Close bottom sheet
+
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
+ allowMultiple: allowMultiple, // Use the parameter
allowedExtensions: [
'jpg',
'jpeg',
@@ -95,9 +125,22 @@ class ImageOptions {
'zip',
],
);
- if (result == null) return;
- List files = result.paths.map((path) => File(path!)).toList();
- image(result.files.first.path.toString(), files.first);
+
+ if (result == null || result.files.isEmpty) return;
+
+ // Process selected file(s)
+ for (var fileData in result.files) {
+ if (fileData.path != null) {
+ File selectedFile = File(fileData.path!);
+ var bytes = await selectedFile.readAsBytes();
+ String base64Encode = base64.encode(bytes);
+
+ // Convert to data URI format
+ String mimeType = _getMimeType(fileData.path!);
+ String dataUri = 'data:$mimeType;base64,$base64Encode';
+ image(dataUri, selectedFile);
+ }
+ }
},
),
callBackFunc: () {},
@@ -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 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 children;
diff --git a/pubspec.lock b/pubspec.lock
index 4f2c35ac..78176753 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -499,6 +499,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_amazonpaymentservices:
+ dependency: "direct main"
+ description:
+ name: flutter_amazonpaymentservices
+ sha256: "23f45fd3b1e12a0933da3704d66cebd19c4c41e28a50d4fd458d3e3e6311f332"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.0.13"
flutter_cache_manager:
dependency: transitive
description:
@@ -1641,6 +1649,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
+ screen_brightness:
+ dependency: "direct main"
+ description:
+ name: screen_brightness
+ sha256: "7d4ac84ae26b37c01d6f5db7123a72db7933e1f2a2a8c369a51e08f81b3178d8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.1"
+ screen_brightness_android:
+ dependency: transitive
+ description:
+ name: screen_brightness_android
+ sha256: "8c69d3ac475e4d625e7fa682a3a51a69ff59abe5b4a9e57f6ec7d830a6c69bd6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.1"
+ screen_brightness_ios:
+ dependency: transitive
+ description:
+ name: screen_brightness_ios
+ sha256: f08f70ca1ac3e30719764b5cfb8b3fe1e28163065018a41b3e6f243ab146c2f1
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.1"
+ screen_brightness_macos:
+ dependency: transitive
+ description:
+ name: screen_brightness_macos
+ sha256: "70c2efa4534e22b927e82693488f127dd4a0f008469fccf4f0eefe9061bbdd6a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.1"
+ screen_brightness_platform_interface:
+ dependency: transitive
+ description:
+ name: screen_brightness_platform_interface
+ sha256: "9f3ebf7f22d5487e7676fe9ddaf3fc55b6ff8057707cf6dc0121c7dfda346a16"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.1"
+ screen_brightness_windows:
+ dependency: transitive
+ description:
+ name: screen_brightness_windows
+ sha256: c8e12a91cf6dd912a48bd41fcf749282a51afa17f536c3460d8d05702fb89ffa
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.1"
scrollable_positioned_list:
dependency: "direct main"
description: